From de43a02a1b19b28f88e6e5cd0cdb58a953b630f5 Mon Sep 17 00:00:00 2001 From: Devin Wendt Date: Sun, 26 Feb 2023 04:59:04 +0000 Subject: [PATCH] Add support for TP-Link's Kasa Smart light bulbs --- .../KasaSmartController.cpp | 329 ++++++++++++++++++ .../KasaSmartController/KasaSmartController.h | 64 ++++ .../KasaSmartControllerDetect.cpp | 55 +++ .../RGBController_KasaSmart.cpp | 113 ++++++ .../RGBController_KasaSmart.h | 31 ++ OpenRGB.pro | 11 + qt/OpenRGBDialog2.cpp | 33 ++ qt/OpenRGBDialog2.h | 3 + .../OpenRGBKasaSmartSettingsEntry.cpp | 24 ++ .../OpenRGBKasaSmartSettingsEntry.h | 24 ++ .../OpenRGBKasaSmartSettingsEntry.ui | 59 ++++ .../OpenRGBKasaSmartSettingsPage.cpp | 120 +++++++ .../OpenRGBKasaSmartSettingsPage.h | 35 ++ .../OpenRGBKasaSmartSettingsPage.ui | 49 +++ 14 files changed, 950 insertions(+) create mode 100644 Controllers/KasaSmartController/KasaSmartController.cpp create mode 100644 Controllers/KasaSmartController/KasaSmartController.h create mode 100644 Controllers/KasaSmartController/KasaSmartControllerDetect.cpp create mode 100644 Controllers/KasaSmartController/RGBController_KasaSmart.cpp create mode 100644 Controllers/KasaSmartController/RGBController_KasaSmart.h create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.cpp create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.h create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.ui create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.cpp create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h create mode 100644 qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.ui diff --git a/Controllers/KasaSmartController/KasaSmartController.cpp b/Controllers/KasaSmartController/KasaSmartController.cpp new file mode 100644 index 00000000..a1d49e4b --- /dev/null +++ b/Controllers/KasaSmartController/KasaSmartController.cpp @@ -0,0 +1,329 @@ +/*----------------------------------------------*\ +| Driver for Kasa Smart light bulbs | +| | +| Devin Wendt (umbreon222@gmail.com), 2/16/2023 | +\*----------------------------------------------*/ + +#include "KasaSmartController.h" +#include "json.hpp" +#include "hsv.h" + +using json = nlohmann::json; + +KasaSmartController::KasaSmartController(std::string ipAddress, std::string name) +{ + this->name = name; + + /*------------------------------------------------*\ + | Fill in location string with device's IP address | + \*------------------------------------------------*/ + location = "IP: " + ipAddress; + + /*---------------------------------------------------------*\ + | Create a TCP client sending to the device's IP, port 9999 | + \*---------------------------------------------------------*/ + port.tcp_client(ipAddress.c_str(), "9999"); +} + +bool KasaSmartController::Initialize() +{ + is_initialized = false; + retry_count = 0; + + /*--------------*\ + | Try to connect | + \*--------------*/ + while(!port.connected && !port.tcp_client_connect() && retry_count < KASA_SMART_MAX_CONNECTION_ATTEMPTS) + { + ++retry_count; + } + if(!port.connected) + { + /*----------------*\ + | Couldn't connect | + \*----------------*/ + return is_initialized; + } + + const std::string system_info_query(KASA_SMART_SYSTEM_INFO_QUERY); + std::string system_info_json; + bool command_sent = KasaSmartController::SendCommand(system_info_query, system_info_json); + port.tcp_close(); + if(!command_sent || system_info_json.empty()) + { + /*---------------------------------------*\ + | Send command failed or no data returned | + \*---------------------------------------*/ + return is_initialized; + } + + json system_information; + try + { + system_information = json::parse(system_info_json); + } + catch (json::parse_error&) + { + /*-----------------------*\ + | Can't parse system info | + \*-----------------------*/ + return is_initialized; + } + + std::string device_type; + if(system_information["system"]["get_sysinfo"].contains("type")) + { + device_type = system_information["system"]["get_sysinfo"]["type"]; + } + else if(system_information["system"]["get_sysinfo"].contains("mic_type")) + { + device_type = system_information["system"]["get_sysinfo"]["mic_type"]; + } + else + { + /*----------------------*\ + | Can't find device type | + \*----------------------*/ + return is_initialized; + } + + std::transform(device_type.begin(), device_type.end(), device_type.begin(), + [](unsigned char c){ return std::tolower(c); }); + if(device_type.find("smartbulb") == std::string::npos) + { + /*----------------------------*\ + | Device type not a smart bulb | + \*----------------------------*/ + return is_initialized; + } + + if(system_information["system"]["get_sysinfo"].contains("is_color") && system_information["system"]["get_sysinfo"]["is_color"] != 1) + { + /*--------------------------------*\ + | Smart bulb doesn't support color | + \*--------------------------------*/ + return is_initialized; + } + + firmware_version = system_information["system"]["get_sysinfo"]["sw_ver"]; + module_name = system_information["system"]["get_sysinfo"]["model"]; + device_id = system_information["system"]["get_sysinfo"]["deviceId"]; + + is_initialized = true; + return is_initialized; +} + +KasaSmartController::~KasaSmartController() +{ + if(port.connected) + { + port.tcp_close(); + } +} + +std::string KasaSmartController::GetLocation() +{ + return(location); +} + +std::string KasaSmartController::GetName() +{ + return(name); +} + +std::string KasaSmartController::GetVersion() +{ + return(module_name + " " + firmware_version); +} + +std::string KasaSmartController::GetManufacturer() +{ + return("Kasa Smart"); +} + +std::string KasaSmartController::GetUniqueID() +{ + return(device_id); +} + +void KasaSmartController::SetColor(unsigned char red, unsigned char green, unsigned char blue) +{ + if(!is_initialized) + { + return; + } + + RGBColor color = ToRGBColor(red, green, blue); + hsv_t hsv; + rgb2hsv(color, &hsv); + + /*------------------------------------------*\ + | Normalize case where hue is "-1" undefined | + \*------------------------------------------*/ + unsigned int normalized_hue = hsv.hue; + if(hsv.hue == -1) + { + normalized_hue = 0; + } + /*--------------------------------------------------*\ + | Kasa smart lights take values out of 100 for these | + \*--------------------------------------------------*/ + unsigned int normalized_saturation = hsv.saturation * 100 / 255; + unsigned int normalized_value = hsv.value * 100 / 255; + + + /*-------------------*\ + | Open TCP connection | + \*-------------------*/ + if(!port.connected && !port.tcp_client_connect() && ++retry_count >= KASA_SMART_MAX_CONNECTION_ATTEMPTS) + { + is_initialized = false; + return; + } + + /*----------------------------*\ + | Hack to handle/emulate black | + \*----------------------------*/ + if(normalized_saturation == 0 && normalized_value == 0) + { + TurnOff(); + return; + } + + /*------------------------------*\ + | Format set light state command | + \*------------------------------*/ + const std::string set_lightstate_command_format(KASA_SMART_SET_LIGHT_STATE_COMMAND_FORMAT); + int size = std::snprintf(nullptr, 0, set_lightstate_command_format.c_str(), normalized_hue, normalized_saturation, normalized_value) + 1; + if(size <= 0) + { + port.tcp_close(); + return; + } + char* buf = new char[size]; + std::snprintf(buf, size, set_lightstate_command_format.c_str(), normalized_hue, normalized_saturation, normalized_value); + std::string set_lightstate_command(buf, buf + size - 1); + delete[] buf; + + /*-----------------------------*\ + | Send command, ignore response | + \*-----------------------------*/ + std::string response; + KasaSmartController::SendCommand(set_lightstate_command, response); + port.tcp_close(); +} + +void KasaSmartController::TurnOff() +{ + if(!is_initialized) + { + return; + } + + const std::string turn_off_command(KASA_SMART_OFF_COMMAND); + + if(!port.connected && !port.tcp_client_connect() && ++retry_count >= KASA_SMART_MAX_CONNECTION_ATTEMPTS) + { + is_initialized = false; + return; + } + std::string response; + KasaSmartController::SendCommand(turn_off_command, response); + port.tcp_close(); +} + +bool KasaSmartController::SendCommand(std::string command, std::string &response) +{ + const unsigned char* encrypted_payload = KasaSmartController::Encrypt(command); + port.tcp_client_write((char*)encrypted_payload, command.length() + sizeof(unsigned long)); + delete[] encrypted_payload; + + unsigned char* receive_buffer = new unsigned char[KASA_SMART_RECEIVE_BUFFER_SIZE]; + int response_length = port.tcp_listen((char*)receive_buffer, KASA_SMART_RECEIVE_BUFFER_SIZE); + if(response_length > KASA_SMART_RECEIVE_BUFFER_SIZE) { + /*-------------------------------------------------------------*\ + | Small fail safes to prevent decrypting bad or empty responses | + \*-------------------------------------------------------------*/ + return false; + } + + unsigned long received_length = response_length; + unsigned long response_full_length = 0; + if(response_length > 0) + { + response_full_length = ntohl(*(unsigned long*)receive_buffer); + } + + if(response_full_length > KASA_SMART_RECEIVE_BUFFER_SIZE) { + return false; + } + + /*--------------------------*\ + | Fetch entirety of response | + \*--------------------------*/ + while(received_length < response_full_length) + { + received_length += port.tcp_listen((char*)receive_buffer + received_length, KASA_SMART_RECEIVE_BUFFER_SIZE - received_length); + } + + if(received_length > 0) + { + /*------------------------------------------------*\ + | Decrypt payload data preceeding the payload size | + \*------------------------------------------------*/ + KasaSmartController::Decrypt(receive_buffer + sizeof(unsigned long), received_length - sizeof(unsigned long), response); + } + delete[] receive_buffer; + return true; +} + +unsigned char* KasaSmartController::Encrypt(const std::string request) +{ + /*----------------------------------------------------------------*\ + | "Encrypted" payload consists of size as a uint32 + XOR'd payload | + \*----------------------------------------------------------------*/ + unsigned long size = htonl(request.length()); + int payload_size = request.length() + sizeof(size); + unsigned char* payload = new unsigned char[payload_size]; + memcpy(payload, &size, sizeof(size)); + unsigned char* request_data = new unsigned char[request.length()]; + memcpy(request_data, request.data(), request.length()); + KasaSmartController::XorPayload(request_data, request.length()); + memcpy(payload + sizeof(size), request_data, request.length()); + delete[] request_data; + return payload; +} + +std::string KasaSmartController::Decrypt(const unsigned char* encrypted, int length, std::string &response) +{ + unsigned char* temp_encrypted = new unsigned char[length]; + memcpy(temp_encrypted, encrypted, length); + KasaSmartController::XorEncryptedPayload(temp_encrypted, length); + for(int i = 0; i < length; ++i) + { + response += temp_encrypted[i]; + } + delete[] temp_encrypted; + return response; +} + +void KasaSmartController::XorPayload(unsigned char* encrypted, int length) +{ + unsigned char key = KASA_SMART_INITIALIZATION_VECTOR; + for(int i = 0; i < length; ++i) + { + key ^= encrypted[i]; + encrypted[i] = key; + } +} + +void KasaSmartController::XorEncryptedPayload(unsigned char* encrypted, int length) +{ + unsigned char key = KASA_SMART_INITIALIZATION_VECTOR; + for(int i = 0; i < length; ++i) + { + unsigned char plain_byte = key ^ encrypted[i]; + key = encrypted[i]; + encrypted[i] = plain_byte; + } +} diff --git a/Controllers/KasaSmartController/KasaSmartController.h b/Controllers/KasaSmartController/KasaSmartController.h new file mode 100644 index 00000000..b97d1dca --- /dev/null +++ b/Controllers/KasaSmartController/KasaSmartController.h @@ -0,0 +1,64 @@ +/*---------------------------------------------------------*\ +| Definitions for Kasa Smart | +| | +| Devin Wendt (umbreon222@gmail.com), 2/16/2023 | +\*---------------------------------------------------------*/ + +#include "RGBController.h" +#include "net_port.h" + +#include +#include +#include + +#pragma once + +enum +{ + KASA_SMART_MODE_DIRECT = 0x00, + KASA_SMART_MODE_OFF = 0x01 +}; + +#define KASA_SMART_INITIALIZATION_VECTOR 0xAB +#define KASA_SMART_RECEIVE_BUFFER_SIZE 4096 +#define KASA_SMART_MAX_CONNECTION_ATTEMPTS 3 + +/*-------------------------*\ +| Kasa Smart Light Commands | +\*-------------------------*/ +#define KASA_SMART_SYSTEM_INFO_QUERY "{\"system\": {\"get_sysinfo\": {}}}" +#define KASA_SMART_OFF_COMMAND "{\"smartlife.iot.smartbulb.lightingservice\": {\"transition_light_state\": {\"transition_period\": 0, \"on_off\":0, \"mode\":\"normal\"}}}" +const char KASA_SMART_SET_LIGHT_STATE_COMMAND_FORMAT[] = "{\"smartlife.iot.smartbulb.lightingservice\": {\"transition_light_state\": {\"transition_period\": 0, \"on_off\"" + ":1, \"mode\":\"normal\", \"hue\": %u, \"saturation\": %u, \"brightness\": %u, \"color_temp\": 0}}}"; + +class KasaSmartController +{ +public: + KasaSmartController(std::string ipAddress, std::string name); + ~KasaSmartController(); + + std::string GetLocation(); + std::string GetName(); + std::string GetVersion(); + std::string GetManufacturer(); + std::string GetUniqueID(); + + bool Initialize(); + void SetColor(unsigned char red, unsigned char green, unsigned char blue); + void TurnOff(); + +private: + net_port port; + std::string name; + bool is_initialized; + unsigned int retry_count; + std::string firmware_version; + std::string module_name; + std::string device_id; + std::string location; + bool SendCommand(std::string command, std::string &response); + static unsigned char* Encrypt(const std::string request); + static std::string Decrypt(const unsigned char*, int length, std::string &response); + static void XorPayload(unsigned char* encrypted, int length); + static void XorEncryptedPayload(unsigned char* encrypted, int length); +}; diff --git a/Controllers/KasaSmartController/KasaSmartControllerDetect.cpp b/Controllers/KasaSmartController/KasaSmartControllerDetect.cpp new file mode 100644 index 00000000..1d274db6 --- /dev/null +++ b/Controllers/KasaSmartController/KasaSmartControllerDetect.cpp @@ -0,0 +1,55 @@ +#include "Detector.h" +#include "KasaSmartController.h" +#include "RGBController.h" +#include "RGBController_KasaSmart.h" +#include "SettingsManager.h" +#include +#include +#include +#include +#include + +/******************************************************************************************\ +* * +* DetectKasaSmartControllers * +* * +* Detect Kasa Smart devices * +* * +\******************************************************************************************/ + +void DetectKasaSmartControllers() +{ + json kasa_smart_settings; + + /*---------------------------------------------*\ + | Get Kasa Smart settings from settings manager | + \*---------------------------------------------*/ + kasa_smart_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("KasaSmartDevices"); + + /*---------------------------------------------*\ + | If the Wiz settings contains devices, process | + \*---------------------------------------------*/ + if(kasa_smart_settings.contains("devices")) + { + for(unsigned int device_idx = 0; device_idx < kasa_smart_settings["devices"].size(); device_idx++) + { + if(kasa_smart_settings["devices"][device_idx].contains("ip")) + { + std::string kasa_smart_ip = kasa_smart_settings["devices"][device_idx]["ip"]; + std::string name = kasa_smart_settings["devices"][device_idx]["name"]; + + KasaSmartController* controller = new KasaSmartController(kasa_smart_ip, name); + if(!controller->Initialize()) + { + continue; + } + + RGBController_KasaSmart* rgb_controller = new RGBController_KasaSmart(controller); + ResourceManager::get()->RegisterRGBController(rgb_controller); + } + } + } + +} /* DetectKasaSmartControllers() */ + +REGISTER_DETECTOR("KasaSmart", DetectKasaSmartControllers); diff --git a/Controllers/KasaSmartController/RGBController_KasaSmart.cpp b/Controllers/KasaSmartController/RGBController_KasaSmart.cpp new file mode 100644 index 00000000..462315d8 --- /dev/null +++ b/Controllers/KasaSmartController/RGBController_KasaSmart.cpp @@ -0,0 +1,113 @@ +/*-----------------------------------------*\ +| RGBController_KasaSmart.cpp | +| | +| Generic RGB Interface for KasaSmart | +| | +| Devin Wendt (umbreon222) 2/16/2023 | +\*-----------------------------------------*/ + +#include "RGBController_KasaSmart.h" + +/**------------------------------------------------------------------*\ + @name Kasa Smart Bulbs + @category Light + @type USB + @save :x: + @direct :white_check_mark: + @effects :x: + @detectors DetectKasaSmartControllers + @comment +\*-------------------------------------------------------------------*/ + +RGBController_KasaSmart::RGBController_KasaSmart(KasaSmartController* controller_ptr) +{ + controller = controller_ptr; + + name = controller->GetManufacturer() + " " + controller->GetName(); + vendor = controller->GetManufacturer(); + type = DEVICE_TYPE_LIGHT; + version = controller->GetVersion(); + description = "Kasa Smart Device"; + serial = controller->GetUniqueID(); + location = controller->GetLocation(); + + mode Direct; + Direct.name = "Direct"; + Direct.value = KASA_SMART_MODE_DIRECT; + Direct.flags = MODE_FLAG_HAS_PER_LED_COLOR; + Direct.color_mode = MODE_COLORS_PER_LED; + modes.push_back(Direct); + + mode Off; + Off.name = "Off"; + Off.value = KASA_SMART_MODE_OFF; + Off.flags = 0; + Off.color_mode = MODE_COLORS_NONE; + modes.push_back(Off); + + SetupZones(); +} + +RGBController_KasaSmart::~RGBController_KasaSmart() +{ + delete controller; +} + +void RGBController_KasaSmart::SetupZones() +{ + zone led_zone; + led_zone.name = "RGB Light"; + led_zone.type = ZONE_TYPE_SINGLE; + led_zone.leds_min = 1; + led_zone.leds_max = 1; + led_zone.leds_count = 1; + led_zone.matrix_map = NULL; + zones.push_back(led_zone); + + led new_led; + new_led.name = "RGB Light"; + + leds.push_back(new_led); + + SetupColors(); +} + +void RGBController_KasaSmart::ResizeZone(int /*zone*/, int /*new_size*/) +{ + /*-------------------------------------------*\ + | This device does not support resizing zones | + \*-------------------------------------------*/ +} + +void RGBController_KasaSmart::DeviceUpdateLEDs() +{ + if(modes[active_mode].value != KASA_SMART_MODE_DIRECT) + { + return; + } + + unsigned char red = RGBGetRValue(colors[0]); + unsigned char grn = RGBGetGValue(colors[0]); + unsigned char blu = RGBGetBValue(colors[0]); + + controller->SetColor(red, grn, blu); +} + +void RGBController_KasaSmart::UpdateZoneLEDs(int /*zone*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_KasaSmart::UpdateSingleLED(int /*led*/) +{ + DeviceUpdateLEDs(); +} + +void RGBController_KasaSmart::DeviceUpdateMode() +{ + if(modes[active_mode].value == KASA_SMART_MODE_OFF) + { + controller->TurnOff(); + return; + } +} diff --git a/Controllers/KasaSmartController/RGBController_KasaSmart.h b/Controllers/KasaSmartController/RGBController_KasaSmart.h new file mode 100644 index 00000000..58d71bc2 --- /dev/null +++ b/Controllers/KasaSmartController/RGBController_KasaSmart.h @@ -0,0 +1,31 @@ +/*-----------------------------------------*\ +| RGBController_KasaSmart.h | +| | +| Generic RGB Interface for Kasa Smart | +| | +| Devin Wendt (umbreon222) 2/16/2023 | +\*-----------------------------------------*/ + +#pragma once +#include "RGBController.h" +#include "KasaSmartController.h" + +class RGBController_KasaSmart : public RGBController +{ +public: + RGBController_KasaSmart(KasaSmartController* controller_ptr); + ~RGBController_KasaSmart(); + + void SetupZones(); + + void ResizeZone(int zone, int new_size); + + void DeviceUpdateLEDs(); + void UpdateZoneLEDs(int zone); + void UpdateSingleLED(int led); + + void DeviceUpdateMode(); + +private: + KasaSmartController* controller; +}; diff --git a/OpenRGB.pro b/OpenRGB.pro index f5f84123..edfe790c 100644 --- a/OpenRGB.pro +++ b/OpenRGB.pro @@ -219,6 +219,8 @@ HEADERS += qt/OpenRGBDialog2.h \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsEntry.h \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsPage.h \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.h \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h \ qt/OpenRGBPluginContainer.h \ qt/OpenRGBPluginsPage/OpenRGBPluginsEntry.h \ qt/OpenRGBPluginsPage/OpenRGBPluginsList.h \ @@ -469,6 +471,8 @@ HEADERS += Controllers/IntelArcA770LEController/RGBController_IntelArcA770LE.h \ Controllers/IonicoController/IonicoController.h \ Controllers/IonicoController/RGBController_Ionico.h \ + Controllers/KasaSmartController/KasaSmartController.h \ + Controllers/KasaSmartController/RGBController_KasaSmart.h \ Controllers/KeychronKeyboardController/KeychronKeyboardController.h \ Controllers/KeychronKeyboardController/RGBController_KeychronKeyboard.h \ Controllers/LEDStripController/LEDStripController.h \ @@ -748,6 +752,8 @@ SOURCES += qt/OpenRGBDialog2.cpp \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsEntry.cpp \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsPage.cpp \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.cpp \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.cpp \ qt/OpenRGBPluginContainer.cpp \ qt/OpenRGBPluginsPage/OpenRGBPluginsEntry.cpp \ qt/OpenRGBPluginsPage/OpenRGBPluginsList.cpp \ @@ -1062,6 +1068,9 @@ SOURCES += Controllers/IonicoController/IonicoControllerDetect.cpp \ Controllers/IonicoController/RGBController_Ionico.cpp \ Controllers/IntelArcA770LEController/RGBController_IntelArcA770LE.cpp \ + Controllers/KasaSmartController/KasaSmartController.cpp \ + Controllers/KasaSmartController/KasaSmartControllerDetect.cpp \ + Controllers/KasaSmartController/RGBController_KasaSmart.cpp \ Controllers/KeychronKeyboardController/KeychronKeyboardController.cpp \ Controllers/KeychronKeyboardController/KeychronKeyboardControllerDetect.cpp \ Controllers/KeychronKeyboardController/RGBController_KeychronKeyboard.cpp \ @@ -1364,6 +1373,8 @@ FORMS += qt/OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.ui \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsEntry.ui \ qt/OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsPage.ui \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.ui \ + qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.ui \ qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsEntry.ui \ qt/OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.ui \ qt/OpenRGBNanoleafSettingsPage/OpenRGBNanoleafSettingsPage.ui \ diff --git a/qt/OpenRGBDialog2.cpp b/qt/OpenRGBDialog2.cpp index 50f6e574..f88648fc 100644 --- a/qt/OpenRGBDialog2.cpp +++ b/qt/OpenRGBDialog2.cpp @@ -437,6 +437,11 @@ OpenRGBDialog2::OpenRGBDialog2(QWidget *parent) : QMainWindow(parent), ui(new Op \*-----------------------------------------------------*/ AddE131SettingsPage(); + /*-----------------------------------------------------*\ + | Add the Kasa Smart settings page | + \*-----------------------------------------------------*/ + AddKasaSmartSettingsPage(); + /*-----------------------------------------------------*\ | Add the LIFX settings page | \*-----------------------------------------------------*/ @@ -766,6 +771,34 @@ void OpenRGBDialog2::AddE131SettingsPage() ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel); } +void OpenRGBDialog2::AddKasaSmartSettingsPage() +{ + /*-----------------------------------------------------*\ + | Create the Settings page | + \*-----------------------------------------------------*/ + KasaSmartSettingsPage = new OpenRGBKasaSmartSettingsPage(); + + ui->SettingsTabBar->addTab(KasaSmartSettingsPage, ""); + + QString SettingsLabelString; + + if(OpenRGBThemeManager::IsDarkTheme()) + { + SettingsLabelString = "light_dark.png"; + } + else + { + SettingsLabelString = "light.png"; + } + + /*-----------------------------------------------------*\ + | Create the tab label | + \*-----------------------------------------------------*/ + TabLabel* SettingsTabLabel = new TabLabel(SettingsLabelString, tr("Kasa Smart Devices"), (char *)"Kasa Smart Devices", (char *)context); + + ui->SettingsTabBar->tabBar()->setTabButton(ui->SettingsTabBar->tabBar()->count() - 1, QTabBar::LeftSide, SettingsTabLabel); +} + void OpenRGBDialog2::AddLIFXSettingsPage() { /*-----------------------------------------------------*\ diff --git a/qt/OpenRGBDialog2.h b/qt/OpenRGBDialog2.h index b8edc831..32781937 100644 --- a/qt/OpenRGBDialog2.h +++ b/qt/OpenRGBDialog2.h @@ -11,6 +11,7 @@ #include "OpenRGBSettingsPage.h" #include "OpenRGBE131SettingsPage/OpenRGBE131SettingsPage.h" #include "OpenRGBElgatoKeyLightSettingsPage/OpenRGBElgatoKeyLightSettingsPage.h" +#include "OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h" #include "OpenRGBLIFXSettingsPage/OpenRGBLIFXSettingsPage.h" #include "OpenRGBPhilipsHueSettingsPage/OpenRGBPhilipsHueSettingsPage.h" #include "OpenRGBPhilipsWizSettingsPage/OpenRGBPhilipsWizSettingsPage.h" @@ -84,6 +85,7 @@ private: OpenRGBSettingsPage *SettingsPage; OpenRGBE131SettingsPage *E131SettingsPage; OpenRGBElgatoKeyLightSettingsPage *ElgatoKeyLightSettingsPage; + OpenRGBKasaSmartSettingsPage *KasaSmartSettingsPage; OpenRGBLIFXSettingsPage *LIFXSettingsPage; OpenRGBPhilipsHueSettingsPage *PhilipsHueSettingsPage; OpenRGBPhilipsWizSettingsPage *PhilipsWizSettingsPage; @@ -111,6 +113,7 @@ private: void AddSettingsPage(); void AddE131SettingsPage(); void AddElgatoKeyLightSettingsPage(); + void AddKasaSmartSettingsPage(); void AddLIFXSettingsPage(); void AddPhilipsHueSettingsPage(); void AddPhilipsWizSettingsPage(); diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.cpp b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.cpp new file mode 100644 index 00000000..b6a8b236 --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.cpp @@ -0,0 +1,24 @@ +#include "OpenRGBKasaSmartSettingsEntry.h" +#include "ui_OpenRGBKasaSmartSettingsEntry.h" + +using namespace Ui; + +OpenRGBKasaSmartSettingsEntry::OpenRGBKasaSmartSettingsEntry(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBKasaSmartSettingsEntryUi) +{ + ui->setupUi(this); +} + +OpenRGBKasaSmartSettingsEntry::~OpenRGBKasaSmartSettingsEntry() +{ + delete ui; +} + +void OpenRGBKasaSmartSettingsEntry::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } +} diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.h b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.h new file mode 100644 index 00000000..49d789dc --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.h @@ -0,0 +1,24 @@ +#ifndef OPENRGBKASASMARTSETTINGSENTRY_H +#define OPENRGBKASASMARTSETTINGSENTRY_H + +#include "ui_OpenRGBKasaSmartSettingsEntry.h" +#include + +namespace Ui { +class OpenRGBKasaSmartSettingsEntry; +} + +class Ui::OpenRGBKasaSmartSettingsEntry : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBKasaSmartSettingsEntry(QWidget *parent = nullptr); + ~OpenRGBKasaSmartSettingsEntry(); + Ui::OpenRGBKasaSmartSettingsEntryUi *ui; + +private slots: + void changeEvent(QEvent *event); +}; + +#endif // OPENRGBKASASMARTSETTINGSENTRY_H diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.ui b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.ui new file mode 100644 index 00000000..92bdcf6b --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsEntry.ui @@ -0,0 +1,59 @@ + + + OpenRGBKasaSmartSettingsEntryUi + + + + 0 + 0 + 190 + 89 + + + + + 0 + 0 + + + + Form + + + + + + + + + + + + + + + IP: + + + + + + + + + + Name + + + + + + + + + + IPEdit + + + + diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.cpp b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.cpp new file mode 100644 index 00000000..eed8d95c --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.cpp @@ -0,0 +1,120 @@ +#include "OpenRGBKasaSmartSettingsPage.h" +#include "ui_OpenRGBKasaSmartSettingsPage.h" +#include "ResourceManager.h" + +using namespace Ui; + +OpenRGBKasaSmartSettingsPage::OpenRGBKasaSmartSettingsPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::OpenRGBKasaSmartSettingsPageUi) +{ + ui->setupUi(this); + + json KasaSmart_settings; + + /*-------------------------------------------------*\ + | Get KasaSmart settings | + \*-------------------------------------------------*/ + KasaSmart_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("KasaSmartDevices"); + + /*-------------------------------------------------*\ + | If the Wiz settings contains devices, process | + \*-------------------------------------------------*/ + if(KasaSmart_settings.contains("devices")) + { + for(unsigned int device_idx = 0; device_idx < KasaSmart_settings["devices"].size(); device_idx++) + { + OpenRGBKasaSmartSettingsEntry* entry = new OpenRGBKasaSmartSettingsEntry; + + if(KasaSmart_settings["devices"][device_idx].contains("ip")) + { + entry->ui->IPEdit->setText(QString::fromStdString(KasaSmart_settings["devices"][device_idx]["ip"])); + } + + if(KasaSmart_settings["devices"][device_idx].contains("name")) + { + entry->ui->IPEdit->setText(QString::fromStdString(KasaSmart_settings["devices"][device_idx]["name"])); + } + + entries.push_back(entry); + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->KasaSmartDeviceList->addItem(item); + ui->KasaSmartDeviceList->setItemWidget(item, entry); + ui->KasaSmartDeviceList->show(); + } + } +} + +OpenRGBKasaSmartSettingsPage::~OpenRGBKasaSmartSettingsPage() +{ + delete ui; +} + +void OpenRGBKasaSmartSettingsPage::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } +} + +void Ui::OpenRGBKasaSmartSettingsPage::on_AddKasaSmartDeviceButton_clicked() +{ + OpenRGBKasaSmartSettingsEntry* entry = new OpenRGBKasaSmartSettingsEntry; + entry->ui->NameEdit->setText(QString("KasaSmart%1").arg(entries.size())); + entries.push_back(entry); + + QListWidgetItem* item = new QListWidgetItem; + + item->setSizeHint(entry->sizeHint()); + + ui->KasaSmartDeviceList->addItem(item); + ui->KasaSmartDeviceList->setItemWidget(item, entry); + ui->KasaSmartDeviceList->show(); +} + +void Ui::OpenRGBKasaSmartSettingsPage::on_RemoveKasaSmartDeviceButton_clicked() +{ + int cur_row = ui->KasaSmartDeviceList->currentRow(); + + if(cur_row < 0) + { + return; + } + + QListWidgetItem* item = ui->KasaSmartDeviceList->takeItem(cur_row); + + ui->KasaSmartDeviceList->removeItemWidget(item); + delete item; + + delete entries[cur_row]; + entries.erase(entries.begin() + cur_row); +} + +void Ui::OpenRGBKasaSmartSettingsPage::on_SaveKasaSmartConfigurationButton_clicked() +{ + json KasaSmart_settings; + + /*-------------------------------------------------*\ + | Get KasaSmart settings | + \*-------------------------------------------------*/ + KasaSmart_settings = ResourceManager::get()->GetSettingsManager()->GetSettings("KasaSmartDevices"); + + KasaSmart_settings["devices"].clear(); + + for(unsigned int device_idx = 0; device_idx < entries.size(); device_idx++) + { + /*-------------------------------------------------*\ + | Required parameters | + \*-------------------------------------------------*/ + KasaSmart_settings["devices"][device_idx]["ip"] = entries[device_idx]->ui->IPEdit->text().toStdString(); + KasaSmart_settings["devices"][device_idx]["name"] = entries[device_idx]->ui->NameEdit->text().toStdString(); + } + + ResourceManager::get()->GetSettingsManager()->SetSettings("KasaSmartDevices", KasaSmart_settings); + ResourceManager::get()->GetSettingsManager()->SaveSettings(); +} diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h new file mode 100644 index 00000000..9d9b8cce --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.h @@ -0,0 +1,35 @@ +#ifndef OPENRGBKASASMARTSETTINGSPAGE_H +#define OPENRGBKASASMARTSETTINGSPAGE_H + +#include "ui_OpenRGBKasaSmartSettingsPage.h" +#include + +#include "OpenRGBKasaSmartSettingsEntry.h" + +namespace Ui { +class OpenRGBKasaSmartSettingsPage; +} + +class Ui::OpenRGBKasaSmartSettingsPage : public QWidget +{ + Q_OBJECT + +public: + explicit OpenRGBKasaSmartSettingsPage(QWidget *parent = nullptr); + ~OpenRGBKasaSmartSettingsPage(); + +private slots: + void changeEvent(QEvent *event); + void on_AddKasaSmartDeviceButton_clicked(); + + void on_RemoveKasaSmartDeviceButton_clicked(); + + void on_SaveKasaSmartConfigurationButton_clicked(); + +private: + Ui::OpenRGBKasaSmartSettingsPageUi *ui; + std::vector entries; + +}; + +#endif // OPENRGBKASASMARTSETTINGSPAGE_H diff --git a/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.ui b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.ui new file mode 100644 index 00000000..1bee94c8 --- /dev/null +++ b/qt/OpenRGBKasaSmartSettingsPage/OpenRGBKasaSmartSettingsPage.ui @@ -0,0 +1,49 @@ + + + OpenRGBKasaSmartSettingsPageUi + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Add + + + + + + + Remove + + + + + + + Save + + + + + + + QAbstractItemView::ScrollPerPixel + + + + + + + +