├── .gitignore ├── demo.png ├── demo2.png ├── .github └── FUNDING.yml ├── LICENSE.md ├── README.md ├── main.py └── GoveeBleLight.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | __pycache__/ -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardcpp/GoveeBleMqttServer/HEAD/demo.png -------------------------------------------------------------------------------- /demo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardcpp/GoveeBleMqttServer/HEAD/demo2.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | patreon: BeatSaberPlus 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 HardCPP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GoveeBleMqttServer 2 | GoveeBleMqttServer is a Govee Bluetooth Low Energy Mqtt light controller. 3 | * Free software: MIT license 4 | 5 | ![demo](demo.png) 6 | 7 | # Requirements 8 | A Bluetooth Low Energy compatible adapter 9 | 10 | # Installation 11 | Requires Python 3. 12 | Tested under Python 3.7 on Linux, Windows 13 | 14 | ```bash 15 | pip install paho-mqtt 16 | pip install bleak 17 | ``` 18 | 19 | # Features 20 | * Control Govee lights over Bluetooth Low Energy 21 | * Mqtt support for connecting to Home Assistant 22 | * Multi-zone support (To cover wider Bluetooth area) 23 | * Auto configuration from Home Assistant Mqtt objects 24 | * Keep alive BLE for fast response time 25 | 26 | # Beware: dongles with the same MAC address! 27 | Multiple dongles of the same brand will share the same MAC address and violates the specifications. 28 | Having multiple dongles with the same MAC address will cause interference and will make the connections not working. 29 | 30 | If you are lucky, you can re-program the dongles to change its MAC address (RUN AT YOUR OWN RISKS) 31 | - Windows: [Bluetooth MAC Address Changer](https://macaddresschanger.com/) 32 | - Linux: [bdaddr](https://github.com/thxomas/bdaddr) 33 | 34 | # Tested hardware 35 | - H6008 Fully functional 36 | - H6107 Fully functional 37 | - H6138 Fully functional 38 | - H6139 Fully functional 39 | - H613A Fully functional 40 | - H613B Fully functional 41 | - H6159 Fully functional 42 | - H6159r2 (Rev 2) Fully functional 43 | - H6712 Fully functional (With segment control, but not Cold/Warm) 44 | - H618F Fully functional (With segment control) 45 | 46 | 47 | # Configuration 48 | 49 | Configuration is located at top of file main.py 50 | ```python 51 | SERVER_ZONE_ID: int = 1; 52 | MQTT_SERVER: str = "192.168.10.170"; 53 | MQTT_PORT: int = 1883; 54 | MQTT_USER: str = None; 55 | MQTT_PASS: str = None; 56 | ``` 57 | 58 | # Home Assistant 59 | In configuration.yaml, for each of your light add the following: 60 | ```yaml 61 | mqtt: 62 | light: 63 | - schema: json 64 | name: "NAME OF THE LIGHT" 65 | object_id: "NAME OF THE LIGHT" 66 | state_topic: "goveeblemqtt/zone1/light/MacAddressLowerNoDots_ModelNumber/state" 67 | command_topic: "goveeblemqtt/zone1/light/MacAddressLowerNoDots_ModelNumber/command" 68 | brightness: true 69 | rgb: true 70 | optimistic: false 71 | qos: 0 72 | unique_id: "RANDOM_UNIQUE_ID_HERE" 73 | device: 74 | identifiers: "RANDOM_UNIQUE_ID_HERE" 75 | name: "NAME OF THE LIGHT" 76 | ``` 77 | Create a random unique id and replace RANDOM_UNIQUE_ID_HERE twice (this is used internally by Home Assistant). 78 | 79 | Replace NAME OF THE LIGHT three times by the name of your light 80 | 81 | Replace MacAddressLowerNoDots twice by the Bluetooth mac address of your light, for instance a4c13825cd56 82 | Replace ModelNumber twice by the model number of your light, for instance H6008 83 | 84 | Multiple lights example: 85 | 86 | ![demo2](demo2.png) 87 | 88 | # Credits 89 | - [chvolkmann](https://github.com/chvolkmann/govee_btled/tree/master/govee_btled) 90 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import asyncio; 3 | import json; 4 | import paho.mqtt.client as mqtt; 5 | import GoveeBleLight; 6 | import sys; 7 | import getopt; 8 | import time; 9 | import signal; 10 | 11 | SERVER_ZONE_ID: int = 1; 12 | ADAPTER: str = None; 13 | MQTT_SERVER: str = "192.168.14.12"; 14 | MQTT_PORT: int = 1883; 15 | MQTT_USER: str = None; 16 | MQTT_PASS: str = None; 17 | 18 | # //////////////////////////////////////////////////////////////////////////// 19 | # //////////////////////////////////////////////////////////////////////////// 20 | 21 | CLIENTS = {}; 22 | MESSAGE_QUEUE = []; 23 | RUNNING = True; 24 | 25 | # //////////////////////////////////////////////////////////////////////////// 26 | # //////////////////////////////////////////////////////////////////////////// 27 | 28 | # Entry point 29 | async def main(argv): 30 | global SERVER_ZONE_ID; 31 | global ADAPTER; 32 | global CLIENTS; 33 | global MESSAGE_QUEUE; 34 | global RUNNING; 35 | 36 | l_Options, _ = getopt.getopt(argv,"hz:a:",["adapter=","zone="]) 37 | for l_Option, l_Argument in l_Options: 38 | if l_Option == '-h': 39 | print('main.py -a -z '); 40 | sys.exit(); 41 | 42 | elif l_Option in ("-a", "--adapter"): 43 | ADAPTER = l_Argument 44 | 45 | elif l_Option in ("-z", "--zone"): 46 | SERVER_ZONE_ID = l_Argument 47 | 48 | print("[Main] Starting with zone " + str(SERVER_ZONE_ID)); 49 | if ADAPTER is not None: 50 | print("[Main] Starting with adapter " + ADAPTER); 51 | 52 | signal.signal(signal.SIGINT, Signal_OnSigInt); 53 | 54 | l_MqttClient = mqtt.Client(); 55 | l_MqttClient.on_connect = Mqtt_OnConnect; 56 | l_MqttClient.on_message = Mqtt_OnMessage; 57 | 58 | if MQTT_USER != None and MQTT_PASS != None: 59 | l_MqttClient.username_pw_set(MQTT_USER, MQTT_PASS); 60 | 61 | l_MqttClient.connect(MQTT_SERVER, MQTT_PORT, 60); 62 | 63 | while RUNNING: 64 | try: 65 | if l_MqttClient.loop() != mqtt.MQTT_ERR_SUCCESS: 66 | print("[Main] Disconnected from Mqtt, trying to reconnect in 5 seconds..."); 67 | time.sleep(5); 68 | 69 | if l_MqttClient.connect(MQTT_SERVER, MQTT_PORT, 60) == mqtt.MQTT_ERR_SUCCESS: 70 | pass; 71 | 72 | while len(MESSAGE_QUEUE) > 0: 73 | l_Message = MESSAGE_QUEUE.pop(0); 74 | l_Topic = l_Message.topic; 75 | l_Prefix = "goveeblemqtt/zone" + str(SERVER_ZONE_ID) + "/light/"; 76 | l_Suffix = "/command"; 77 | 78 | if not l_Topic.startswith(l_Prefix) and not l_Topic.endwith(l_Suffix): 79 | continue; 80 | 81 | l_DeviceID = l_Topic[len(l_Prefix):len(l_Topic)-len(l_Suffix)]; 82 | l_Model = "generic"; 83 | l_Payload = json.loads(l_Message.payload.decode("utf-8","ignore")); 84 | 85 | if "_" in l_DeviceID: 86 | l_Model = l_DeviceID[l_DeviceID.find('_')+1:]; 87 | l_DeviceID = l_DeviceID[:l_DeviceID.find('_')]; 88 | 89 | OnPayloadReceived(l_MqttClient, l_Topic, l_DeviceID, l_Model, l_Payload); 90 | 91 | except: 92 | pass; 93 | 94 | print("[Main] Exiting..."); 95 | 96 | for l_Client in CLIENTS: 97 | CLIENTS[l_Client].Close(); 98 | 99 | sys.exit(0); 100 | 101 | # //////////////////////////////////////////////////////////////////////////// 102 | # //////////////////////////////////////////////////////////////////////////// 103 | 104 | # On signal Int 105 | def Signal_OnSigInt(p_Signal, p_Frame): 106 | global RUNNING; 107 | 108 | RUNNING = False; 109 | 110 | print("[Signal_OnSigInt] Exiting..."); 111 | 112 | # //////////////////////////////////////////////////////////////////////////// 113 | # //////////////////////////////////////////////////////////////////////////// 114 | 115 | # On Mqtt connect 116 | def Mqtt_OnConnect(p_MqttClient, _, __, ___): 117 | l_Topic = "goveeblemqtt/zone" + str(SERVER_ZONE_ID) + "/light/+/command"; 118 | 119 | print("[Mqtt_OnConnect] Connected to Mqtt broker") 120 | print("[Mqtt_OnConnect] Subscribing to topic: " + l_Topic); 121 | 122 | p_MqttClient.subscribe(l_Topic) 123 | # On Mqtt message 124 | def Mqtt_OnMessage(p_MqttClient, _, p_Message): 125 | global MESSAGE_QUEUE; 126 | MESSAGE_QUEUE.append(p_Message); 127 | 128 | # //////////////////////////////////////////////////////////////////////////// 129 | # //////////////////////////////////////////////////////////////////////////// 130 | 131 | def OnPayloadReceived(p_MqttClient, p_Topic, p_DeviceID, p_Model, p_Paypload): 132 | global CLIENTS; 133 | global MESSAGE_QUEUE; 134 | 135 | l_RequestedDeviceID = p_DeviceID; 136 | l_Device = None; 137 | 138 | print(p_DeviceID + " " + str(p_Paypload)); 139 | 140 | try: 141 | p_DeviceID = ':'.join(p_DeviceID[i:i+2] for i in range(0, len(p_DeviceID), 2)); 142 | 143 | if not p_DeviceID in CLIENTS: 144 | l_Topic = p_Topic[0:p_Topic.rfind("/") + 1] + "state"; 145 | CLIENTS[p_DeviceID] = GoveeBleLight.Client(p_DeviceID, p_Model, p_MqttClient, l_Topic, ADAPTER); 146 | time.sleep(2); 147 | 148 | l_Device = CLIENTS[p_DeviceID]; 149 | 150 | if "state" in p_Paypload: 151 | l_ExpectedState = 1 if p_Paypload["state"] == "ON" else 0; 152 | 153 | if l_Device.State != l_ExpectedState: 154 | l_Device.SetPower(l_ExpectedState); 155 | 156 | if "brightness" in p_Paypload: 157 | l_Device.SetBrightness(p_Paypload["brightness"] / 255); 158 | 159 | if "segment" in p_Paypload: 160 | l_Device.SetSegment(p_Paypload["segment"]); 161 | 162 | if "color_temp" in p_Paypload: 163 | l_Device.SetColorTempMired(p_Paypload["color_temp"]); 164 | 165 | if "color" in p_Paypload: 166 | l_R = p_Paypload["color"]["r"]; 167 | l_G = p_Paypload["color"]["g"]; 168 | l_B = p_Paypload["color"]["b"]; 169 | 170 | if l_Device.R != l_R or l_Device.G != l_G or l_Device.B != l_B: 171 | l_Device.SetColorRGB(l_R, l_G, l_B); 172 | 173 | except Exception as l_Exception: 174 | print(f"[OnPayloadReceived] OnPayloadReceived: Something Bad happened: {l_Exception}") 175 | 176 | # //////////////////////////////////////////////////////////////////////////// 177 | # //////////////////////////////////////////////////////////////////////////// 178 | 179 | if __name__ == "__main__": 180 | asyncio.run(main(sys.argv[1:])); 181 | -------------------------------------------------------------------------------- /GoveeBleLight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import asyncio; 3 | import json; 4 | import threading; 5 | import time; 6 | import math; 7 | 8 | from enum import IntEnum; 9 | from bleak import BleakClient; 10 | 11 | # //////////////////////////////////////////////////////////////////////////// 12 | # //////////////////////////////////////////////////////////////////////////// 13 | 14 | UUID_CONTROL_CHARACTERISTIC = '00010203-0405-0607-0809-0a0b0c0d2b11' 15 | 16 | # //////////////////////////////////////////////////////////////////////////// 17 | # //////////////////////////////////////////////////////////////////////////// 18 | 19 | class ELedCommand(IntEnum): 20 | SetPower = 0x01 21 | SetBrightness = 0x04 22 | SetColor = 0x05 23 | 24 | class ELedMode(IntEnum): 25 | Manual = 0x02 26 | Microphone = 0x06 27 | Scenes = 0x05 28 | Manual2 = 0x0D 29 | Segment = 0x15 30 | 31 | class EControlMode(IntEnum): 32 | Color = 0x01 33 | Temperature = 0x02 34 | 35 | def convert_K_to_RGB(colour_temperature): 36 | """ 37 | Converts from K to RGB, algorithm courtesy of 38 | http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ 39 | """ 40 | #range check 41 | if colour_temperature < 1000: 42 | colour_temperature = 1000 43 | elif colour_temperature > 40000: 44 | colour_temperature = 40000 45 | 46 | tmp_internal = colour_temperature / 100.0 47 | 48 | # red 49 | if tmp_internal <= 66: 50 | red = 255 51 | else: 52 | tmp_red = 329.698727446 * math.pow(tmp_internal - 60, -0.1332047592) 53 | if tmp_red < 0: 54 | red = 0 55 | elif tmp_red > 255: 56 | red = 255 57 | else: 58 | red = tmp_red 59 | 60 | # green 61 | if tmp_internal <=66: 62 | tmp_green = 99.4708025861 * math.log(tmp_internal) - 161.1195681661 63 | if tmp_green < 0: 64 | green = 0 65 | elif tmp_green > 255: 66 | green = 255 67 | else: 68 | green = tmp_green 69 | else: 70 | tmp_green = 288.1221695283 * math.pow(tmp_internal - 60, -0.0755148492) 71 | if tmp_green < 0: 72 | green = 0 73 | elif tmp_green > 255: 74 | green = 255 75 | else: 76 | green = tmp_green 77 | 78 | # blue 79 | if tmp_internal >=66: 80 | blue = 255 81 | elif tmp_internal <= 19: 82 | blue = 0 83 | else: 84 | tmp_blue = 138.5177312231 * math.log(tmp_internal - 10) - 305.0447927307 85 | if tmp_blue < 0: 86 | blue = 0 87 | elif tmp_blue > 255: 88 | blue = 255 89 | else: 90 | blue = tmp_blue 91 | 92 | return int(red), int(green), int(blue); 93 | 94 | # //////////////////////////////////////////////////////////////////////////// 95 | # //////////////////////////////////////////////////////////////////////////// 96 | 97 | class Client: 98 | # Constructor 99 | def __init__(self, p_DeviceID, p_Model, p_MqttClient, p_MqttTopic, p_Adapter): 100 | self.ControlMode = EControlMode.Color; 101 | self.State = 0; 102 | self.Brightness = 1; 103 | self.Segment = -1; 104 | self.Temperature = 4000; 105 | self.R = 255; 106 | self.G = 255; 107 | self.B = 255; 108 | 109 | self._DeviceID = p_DeviceID; 110 | self._Model = p_Model; 111 | self._Client = None; 112 | self._Adapter = p_Adapter; 113 | self._Reconnect = 0; 114 | self._MqttClient = p_MqttClient; 115 | self._MqttTopic = p_MqttTopic; 116 | self._DirtyState = False; 117 | self._DirtyBrightness = False; 118 | self._DirtyColor = False; 119 | self._LastSent = time.time(); 120 | self._PingRoll = 0; 121 | self._ThreadCond = True; 122 | self._Thread = threading.Thread(target= self._ThreadStarter); 123 | 124 | self._Thread.start(); 125 | # Destructor 126 | def __del__(self): 127 | self.Close(); 128 | 129 | # //////////////////////////////////////////////////////////////////////////// 130 | # //////////////////////////////////////////////////////////////////////////// 131 | 132 | # Properly close the client 133 | def Close(self): 134 | if self._Thread is None: 135 | return; 136 | 137 | print("[GoveeBleLight.Client::Close] Closing device " + self._DeviceID + "..."); 138 | 139 | try: 140 | self._ThreadCond = False; 141 | self._Thread.join(10); 142 | except: 143 | pass; 144 | 145 | self._Thread = None; 146 | 147 | # //////////////////////////////////////////////////////////////////////////// 148 | # //////////////////////////////////////////////////////////////////////////// 149 | 150 | def SetPower(self, p_State): 151 | if not isinstance(p_State, int) or p_State < 0 or p_State > 1: 152 | raise ValueError('Invalid command') 153 | 154 | self.State = 1 if p_State else 0; 155 | self._DirtyState = True; 156 | 157 | def SetBrightness(self, p_Value): 158 | if not 0 <= float(p_Value) <= 1: 159 | raise ValueError(f'SetBrightness: Brightness out of range: {p_Value}') 160 | 161 | self.Brightness = p_Value; 162 | self._DirtyBrightness = True; 163 | 164 | def SetSegment(self, p_Segment): 165 | if p_Segment == -1 or p_Segment == 0: 166 | self.Segment = -1; 167 | else: 168 | self.Segment = p_Segment; 169 | 170 | def SetColorTempMired(self, p_Value): 171 | l_ColorTempK = 1000000 / p_Value; 172 | 173 | self.ControlMode = EControlMode.Temperature; 174 | self.Temperature = l_ColorTempK; 175 | self._DirtyColor = True; 176 | 177 | def SetColorRGB(self, p_R, p_G, p_B): 178 | if not isinstance(p_R, int) or p_R < 0 or p_R > 255: 179 | raise ValueError(f'SetColorRGB: p_R out of range {p_R}'); 180 | if not isinstance(p_G, int) or p_G < 0 or p_G > 255: 181 | raise ValueError(f'SetColorRGB: p_G out of range {p_G}'); 182 | if not isinstance(p_B, int) or p_B < 0 or p_B > 255: 183 | raise ValueError(f'SetColorRGB: p_B out of range {p_B}'); 184 | 185 | self.ControlMode = EControlMode.Color; 186 | self.R = p_R; 187 | self.G = p_G; 188 | self.B = p_B; 189 | self._DirtyColor = True; 190 | 191 | # //////////////////////////////////////////////////////////////////////////// 192 | # //////////////////////////////////////////////////////////////////////////// 193 | 194 | # Thread main aync coroutine 195 | async def _ThreadCoroutine(self): 196 | while self._ThreadCond: 197 | try: 198 | if not await self._Connect(): 199 | time.sleep(2); 200 | continue; 201 | 202 | l_Changed = True; 203 | 204 | if self._DirtyState: 205 | if not await self._Send_SetPower(self.State): 206 | time.sleep(1); 207 | continue; 208 | 209 | self._DirtyState = False; 210 | elif self._DirtyBrightness: 211 | if not await self._Send_SetBrightness(self.Brightness): 212 | time.sleep(1); 213 | continue; 214 | 215 | self._DirtyBrightness = False; 216 | elif self._DirtyColor: 217 | if not await self._Send_SetColor(): 218 | time.sleep(1); 219 | continue; 220 | 221 | self._DirtyColor = False; 222 | else: 223 | l_Changed = False; 224 | 225 | # Keep alive 226 | if (time.time() - self._LastSent) >= 1: 227 | l_AsyncRes = False; 228 | self._PingRoll += 1; 229 | 230 | if self._PingRoll % 3 == 0 or self.State == 0: 231 | l_AsyncRes = await self._Send_SetPower(self.State); 232 | elif self._PingRoll % 3 == 1: 233 | l_AsyncRes = await self._Send_SetBrightness(self.Brightness); 234 | elif self._PingRoll % 3 == 2: 235 | l_AsyncRes = await self._Send_SetColor(); 236 | 237 | time.sleep(0.1); 238 | continue; 239 | 240 | if l_Changed: 241 | print(self.BuildMqttPayload()); 242 | self._MqttClient.publish(self._MqttTopic, self.BuildMqttPayload()); 243 | 244 | time.sleep(0.01); 245 | 246 | except Exception as l_Exception: 247 | print(f"[GoveeBleLight.Client::_ThreadCoroutine] Error: {l_Exception}"); 248 | 249 | try: 250 | if self._Client is not None: 251 | await self._Client.disconnect(); 252 | except Exception: 253 | pass; 254 | 255 | self._Client = None; 256 | 257 | time.sleep(2); 258 | 259 | try: 260 | if self._Client is not None: 261 | print("[GoveeBleLight.Client::_ThreadCoroutine] Disconnecting device " + self._DeviceID); 262 | await self._Client.disconnect(); 263 | 264 | except Exception: 265 | pass; 266 | 267 | self._Client = None; 268 | 269 | # Thread starter function 270 | def _ThreadStarter(self): 271 | while self._ThreadCond: 272 | print("[GoveeBleLight.Client::_ThreadStarter] Starting device " + self._DeviceID + " event loop..."); 273 | 274 | time.sleep(0.5); 275 | 276 | l_ThreadCoroutine = asyncio.new_event_loop(); 277 | asyncio.set_event_loop(l_ThreadCoroutine); 278 | l_ThreadCoroutine.run_until_complete(self._ThreadCoroutine()); 279 | l_ThreadCoroutine.close(); 280 | 281 | # //////////////////////////////////////////////////////////////////////////// 282 | # //////////////////////////////////////////////////////////////////////////// 283 | 284 | # Handle connect/reconnect 285 | async def _Connect(self): 286 | if self._Client != None and self._Client.is_connected: 287 | return True; 288 | 289 | print("[GoveeBleLight.Client::Connect] re/connecting to device " + self._DeviceID); 290 | 291 | try: 292 | if self._Client is not None: 293 | await self._Client.disconnect(); 294 | 295 | except Exception: 296 | pass; 297 | 298 | self._Client = None; 299 | 300 | try: 301 | if self._Adapter is not None: 302 | self._Client = BleakClient(self._DeviceID, adapter= self._Adapter); 303 | else: 304 | self._Client = BleakClient(self._DeviceID); 305 | 306 | await self._Client.connect(); 307 | self._Reconnect = 0; 308 | 309 | print("[GoveeBleLight.Client::Connect] Connected to device " + self._DeviceID); 310 | 311 | return self._Client.is_connected; 312 | 313 | except Exception as l_Exception: 314 | self._Client = None; 315 | print(f"[GoveeBleLight.Client::_Connect] Error: {l_Exception}"); 316 | 317 | return False; 318 | 319 | # //////////////////////////////////////////////////////////////////////////// 320 | # //////////////////////////////////////////////////////////////////////////// 321 | 322 | async def _Send_SetPower(self, p_State): 323 | if not isinstance(p_State, int) or p_State < 0 or p_State > 1: 324 | raise ValueError('Invalid command') 325 | 326 | try: 327 | return await self._Send(ELedCommand.SetPower, [1 if p_State else 0]) 328 | 329 | except Exception as l_Exception: 330 | print(f"[GoveeBleLight.Client::_Send_SetPower] Error: {l_Exception}"); 331 | 332 | return False; 333 | 334 | async def _Send_SetBrightness(self, p_Value): 335 | if not 0 <= float(p_Value) <= 1: 336 | raise ValueError(f'SetBrightness: Brightness out of range: {p_Value}') 337 | 338 | l_Brightness = round(p_Value * 0xFF); 339 | 340 | if self._Model == "H6008" or self._Model == "H613A" or self._Model == "H613D" or self._Model == "H6172" or self._Model == "H618F": 341 | l_Brightness = int(p_Value * 100); 342 | 343 | try: 344 | return await self._Send(ELedCommand.SetBrightness, [l_Brightness]); 345 | 346 | except Exception as l_Exception: 347 | print(f"[GoveeBleLight.Client::_Send_SetBrightness] Error: {l_Exception}"); 348 | 349 | return False; 350 | 351 | async def _Send_SetColor(self): 352 | l_R = self.R; 353 | l_G = self.G; 354 | l_B = self.B; 355 | 356 | l_TK = 0 357 | l_WR = 0; 358 | l_WG = 0; 359 | l_WB = 0; 360 | 361 | if self.ControlMode == EControlMode.Temperature: 362 | l_R = l_G = l_B = 0xFF; 363 | l_TK = int(self.Temperature); 364 | 365 | if not isinstance(l_R, int) or l_R < 0 or l_R > 255: 366 | raise ValueError(f'SetColorRGB: l_R out of range {l_R}'); 367 | if not isinstance(l_G, int) or l_G < 0 or l_G > 255: 368 | raise ValueError(f'SetColorRGB: l_G out of range {l_G}'); 369 | if not isinstance(l_B, int) or l_B < 0 or l_B > 255: 370 | raise ValueError(f'SetColorRGB: l_B out of range {l_B}'); 371 | 372 | l_LedMode = ELedMode.Manual; 373 | 374 | try: 375 | if self._Model == "H6008" or self._Model == "H613A" or self._Model == "H613D" or self._Model == "H6159r2": 376 | l_LedMode = ELedMode.Manual2; 377 | 378 | return await self._Send(ELedCommand.SetColor, [l_LedMode, l_R, l_G, l_B, (l_TK >> 8) & 0xFF, l_TK & 0xFF, l_WR, l_WG, l_WB]); 379 | elif self._Model == "H6172" or self._Model == "H618F": 380 | l_LedMode = ELedMode.Segment; 381 | 382 | return await self._Send(ELedCommand.SetColor, [l_LedMode, 0x01, l_R, l_G, l_B, (l_TK >> 8) & 0xFF, l_TK & 0xFF, l_WR, l_WG, l_WB, (self.Segment >> 8) & 0xFF, self.Segment & 0xFF]); 383 | else: 384 | # Todo figure out WW control 385 | return await self._Send(ELedCommand.SetColor, [l_LedMode, l_R, l_G, l_B]); 386 | 387 | except Exception as l_Exception: 388 | print(f"[GoveeBleLight.Client::_Send_SetColor] Error: {l_Exception}"); 389 | 390 | return False; 391 | 392 | # //////////////////////////////////////////////////////////////////////////// 393 | # //////////////////////////////////////////////////////////////////////////// 394 | 395 | def BuildMqttPayload(self): 396 | if self.ControlMode == EControlMode.Color: 397 | return json.dumps({ 398 | "state": "ON" if self.State == 1 else "OFF", 399 | "color": { 400 | "r": self.R, 401 | "g": self.G, 402 | "b": self.B 403 | }, 404 | "brightness": round(self.Brightness * 255) 405 | }); 406 | elif self.ControlMode == EControlMode.Temperature: 407 | l_TempColorR, l_TempColorG, l_TempColorB = convert_K_to_RGB(self.Temperature); 408 | 409 | return json.dumps({ 410 | "state": "ON" if self.State == 1 else "OFF", 411 | "brightness": round(self.Brightness * 255), 412 | "color": { 413 | "r": l_TempColorR, 414 | "g": l_TempColorG, 415 | "b": l_TempColorB 416 | }, 417 | "color_temp": int(1000000 / self.Temperature) 418 | }); 419 | 420 | # //////////////////////////////////////////////////////////////////////////// 421 | # //////////////////////////////////////////////////////////////////////////// 422 | 423 | async def _Send(self, p_CMD, p_Payload): 424 | if not isinstance(p_CMD, int): 425 | raise ValueError('[GoveeBleLight.Client::_Send] Invalid command'); 426 | if not isinstance(p_Payload, bytes) and not (isinstance(p_Payload, list) and all(isinstance(x, int) for x in p_Payload)): 427 | raise ValueError('[GoveeBleLight.Client::_Send] Invalid payload'); 428 | if len(p_Payload) > 17: 429 | raise ValueError('[GoveeBleLight.Client::_Send] Payload too long'); 430 | 431 | p_CMD = p_CMD & 0xFF; 432 | p_Payload = bytes(p_Payload); 433 | 434 | l_Frame = bytes([0x33, p_CMD]) + bytes(p_Payload); 435 | l_Frame += bytes([0] * (19 - len(l_Frame))); 436 | 437 | l_Checksum = 0; 438 | for l_Byte in l_Frame: 439 | l_Checksum ^= l_Byte; 440 | 441 | l_Frame += bytes([l_Checksum & 0xFF]); 442 | 443 | try: 444 | await self._Client.write_gatt_char(UUID_CONTROL_CHARACTERISTIC, l_Frame); 445 | self._LastSent = time.time(); 446 | 447 | return True; 448 | 449 | except Exception as l_Exception: 450 | print(f"[GoveeBleLight.Client::_Send] Error: {l_Exception}"); 451 | 452 | try: 453 | if self._Client is not None: 454 | print("[GoveeBleLight.Client::_Send] Disconnecting device " + self._DeviceID); 455 | await self._Client.disconnect(); 456 | 457 | except: 458 | pass; 459 | 460 | self._Reconnect += 1; 461 | self._Client = None; 462 | 463 | return False; 464 | --------------------------------------------------------------------------------