Publish LumaOps source

This commit is contained in:
LumaOps release export
2026-09-03 01:18:36 +02:00
commit 7f1c0e5f71
2363 changed files with 501543 additions and 0 deletions
@@ -0,0 +1,403 @@
/*---------------------------------------------------------*\
| KasaSmartController.cpp |
| |
| Driver for Kasa Smart bulbs |
| |
| Devin Wendt (umbreon222@gmail.com) 16 Feb 2023 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include <cstdint>
#include "KasaSmartController.h"
#include <nlohmann/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;
}
std::string model;
if(system_information["system"]["get_sysinfo"].contains("model"))
{
model = system_information["system"]["get_sysinfo"]["model"];
}
else
{
/*-----------------------*\
| Can't find device model |
\*-----------------------*/
return is_initialized;
}
if(model.find("KL420") != std::string::npos)
{
kasa_type = KASA_SMART_TYPE_KL420;
}
else if(model.find("KL4") != std::string::npos)
{
kasa_type = KASA_SMART_TYPE_OTHER_LEDSTRIP;
}
else
{
kasa_type = KASA_SMART_TYPE_LIGHT;
}
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);
}
int KasaSmartController::GetKasaType()
{
return(kasa_type);
}
void KasaSmartController::SetColor(unsigned char red, unsigned char green, unsigned char blue, int device_type)
{
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 == (unsigned int)-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(device_type);
return;
}
/*------------------------------*\
| Format set light state command |
\*------------------------------*/
std::string set_lightstate_command_format;
if(device_type == DEVICE_TYPE_LIGHT)
{
set_lightstate_command_format = KASA_SMART_LIGHT_SET_LIGHT_STATE_COMMAND_FORMAT;
}
else if(device_type == DEVICE_TYPE_LEDSTRIP)
{
set_lightstate_command_format = KASA_SMART_LEDSTRIP_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::SetEffect(std::string effect)
{
if(!is_initialized)
{
return;
}
/*-------------------*\
| Open TCP connection |
\*-------------------*/
if(!port.connected && !port.tcp_client_connect() && ++retry_count >= KASA_SMART_MAX_CONNECTION_ATTEMPTS)
{
is_initialized = false;
return;
}
std::string response;
KasaSmartController::SendCommand(effect, response);
port.tcp_close();
}
void KasaSmartController::TurnOff(int device_type)
{
if(!is_initialized)
{
return;
}
std::string turn_off_command;
if(device_type == DEVICE_TYPE_LIGHT)
{
turn_off_command = KASA_SMART_LIGHT_OFF_COMMAND;
}
else if(device_type == DEVICE_TYPE_LEDSTRIP)
{
turn_off_command = KASA_SMART_LEDSTRIP_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, (int)(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 || response_length <= 0) {
/*-------------------------------------------------------------*\
| 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(*(uint32_t*)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(uint32_t), received_length - sizeof(uint32_t), 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 |
\*----------------------------------------------------------------*/
uint32_t size = htonl((uint32_t)request.length());
int payload_size = (int)(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, (int)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;
}
}
@@ -0,0 +1,84 @@
/*---------------------------------------------------------*\
| KasaSmartController.h |
| |
| Driver for Kasa Smart bulbs |
| |
| Devin Wendt (umbreon222@gmail.com) 16 Feb 2023 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#pragma once
#include <string>
#include <thread>
#include <vector>
#include "RGBController.h"
#include "net_port.h"
enum
{
KASA_SMART_MODE_DIRECT = 0x00,
KASA_SMART_MODE_OFF = 0x01,
KASA_SMART_MODE_RAINBOW = 0x02
};
enum
{
KASA_SMART_TYPE_KL420 = 0x00,
KASA_SMART_TYPE_OTHER_LEDSTRIP = 0x01,
KASA_SMART_TYPE_LIGHT = 0x02
};
#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_LIGHT_OFF_COMMAND "{\"smartlife.iot.smartbulb.lightingservice\": {\"transition_light_state\": {\"transition_period\": 0, \"on_off\":0, \"mode\":\"normal\"}}}"
const char KASA_SMART_LIGHT_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}}}";
#define KASA_SMART_LEDSTRIP_OFF_COMMAND "{\"smartlife.iot.lightStrip\": {\"set_light_state\": {\"transition\": 0, \"on_off\":0, \"mode\":\"normal\"}}}"
const char KASA_SMART_LEDSTRIP_SET_LIGHT_STATE_COMMAND_FORMAT[] = "{\"smartlife.iot.lightStrip\": {\"set_light_state\": {\"transition\": 0, \"on_off\""
":1, \"mode\":\"normal\", \"hue\": %u, \"saturation\": %u, \"brightness\": %u, \"color_temp\": 0}}}";
#define KASA_SMART_EFFECT_RAINBOW_COMMAND "{\"smartlife.iot.lighting_effect\":{\"set_lighting_effect\":{\"custom\":0,\"direction\":1,\"duration\":0,\"enable\":1,\"expansion_strategy\":1,\"name\":\"Rainbow\",\"repeat_times\":0,\"segments\":[0],\"sequence\":[[0,100,100],[100,100,100],[200,100,100],[300,100,100]],\"spread\":12,\"transition\":1500,\"type\":\"sequence\"}}}}"
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();
int GetKasaType();
bool Initialize();
void SetColor(unsigned char red, unsigned char green, unsigned char blue, int device_type);
void SetEffect(std::string effect);
void TurnOff(int device_type);
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;
int kasa_type;
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);
};
@@ -0,0 +1,60 @@
/*---------------------------------------------------------*\
| KasaSmartControllerDetect.cpp |
| |
| Detector for Kasa Smart bulbs |
| |
| Devin Wendt (umbreon222@gmail.com) 16 Feb 2023 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#include "Detector.h"
#include "KasaSmartController.h"
#include "RGBController_KasaSmart.h"
#include "SettingsManager.h"
/******************************************************************************************\
* *
* 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);
@@ -0,0 +1,139 @@
/*---------------------------------------------------------*\
| RGBController_KasaSmart.cpp |
| |
| RGBController for Kasa Smart bulbs |
| |
| Devin Wendt (umbreon222) 16 Feb 2023 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#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();
version = controller->GetVersion();
description = "Kasa Smart Device";
serial = controller->GetUniqueID();
location = controller->GetLocation();
if(controller->GetKasaType() == KASA_SMART_TYPE_LIGHT)
{
type = DEVICE_TYPE_LIGHT;
}
else if(controller->GetKasaType() == KASA_SMART_TYPE_OTHER_LEDSTRIP
|| controller->GetKasaType() == KASA_SMART_TYPE_KL420)
{
type = DEVICE_TYPE_LEDSTRIP;
}
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);
if(controller->GetKasaType() == KASA_SMART_TYPE_KL420)
{
mode Rainbow;
Rainbow.name = "Rainbow";
Rainbow.value = KASA_SMART_MODE_RAINBOW;
Rainbow.flags = MODE_FLAG_HAS_PER_LED_COLOR;
Rainbow.color_mode = MODE_COLORS_PER_LED;
modes.push_back(Rainbow);
}
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, type);
}
void RGBController_KasaSmart::UpdateZoneLEDs(int /*zone*/)
{
DeviceUpdateLEDs();
}
void RGBController_KasaSmart::UpdateSingleLED(int /*led*/)
{
DeviceUpdateLEDs();
}
void RGBController_KasaSmart::DeviceUpdateMode()
{
switch(modes[active_mode].value)
{
case KASA_SMART_MODE_OFF:
controller->TurnOff(type);
break;
case KASA_SMART_MODE_RAINBOW:
controller->SetEffect(KASA_SMART_EFFECT_RAINBOW_COMMAND);
break;
}
}
@@ -0,0 +1,35 @@
/*---------------------------------------------------------*\
| RGBController_KasaSmart.h |
| |
| RGBController for Kasa Smart bulbs |
| |
| Devin Wendt (umbreon222) 16 Feb 2023 |
| |
| This file is part of the OpenRGB project |
| SPDX-License-Identifier: GPL-2.0-or-later |
\*---------------------------------------------------------*/
#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;
};