This repository has been archived by the owner on Dec 22, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
light.py
458 lines (390 loc) · 15 KB
/
light.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
"""Platform for light integration."""
from __future__ import annotations
import json
import socket
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import area_registry as ar
import voluptuous as vol
from homeassistant.util.color import (
color_temperature_kelvin_to_mired,
color_temperature_mired_to_kelvin,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_TEMP,
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGB,
COLOR_MODE_RGBWW,
ENTITY_ID_FORMAT,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_TRANSITION,
SUPPORT_WHITE_VALUE,
LightEntity,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
STATE_UNAVAILABLE,
STATE_OK,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
# Import the device class from the component that you want to support
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo, Entity, generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import homeassistant.util.color as color_util
from homeassistant.config_entries import ConfigEntry
from .api import SCENES, Klyqa, KlyqaLightDevice
from .const import DOMAIN, LOGGER, CONF_SYNC_ROOMS
# all deprecated, still here for testing, color_mode is the modern way to go ...
SUPPORT_KLYQA = (
SUPPORT_BRIGHTNESS
| SUPPORT_COLOR
| SUPPORT_COLOR_TEMP
| SUPPORT_TRANSITION
| SUPPORT_EFFECT
)
from datetime import timedelta
import functools as ft
from homeassistant.helpers.area_registry import AreaEntry, AreaRegistry
import homeassistant.helpers.area_registry as area_registry
SCAN_INTERVAL = timedelta(seconds=3)
async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
return True
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
await async_setup_klyqa(
hass,
entry.data,
async_add_entities,
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
await async_setup_klyqa(
hass,
config,
add_entities,
discovery_info,
)
async def async_setup_klyqa(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Klyqa Light platform."""
if not DOMAIN in hass.data:
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
host = config.get(CONF_HOST)
sync_rooms = (
config.get(CONF_SYNC_ROOMS) if config.get(CONF_SYNC_ROOMS) else False
)
hass.data[DOMAIN] = Klyqa(username, password, host, hass, sync_rooms=sync_rooms)
if not await hass.async_add_executor_job(hass.data[DOMAIN].login):
return
klyqa: Klyqa = hass.data[DOMAIN]
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, klyqa.shutdown)
await hass.async_add_executor_job(klyqa.load_settings)
await hass.async_add_executor_job(
ft.partial(klyqa.search_lights, seconds_to_discover=1)
)
entities = []
for device_settings in klyqa._settings["devices"]:
entity_id = generate_entity_id(
ENTITY_ID_FORMAT,
device_settings["localDeviceId"],
hass=hass,
)
u_id = device_settings["localDeviceId"]
light_state = klyqa.lights[u_id] if u_id in klyqa.lights else KlyqaLightDevice()
rooms = []
for room in klyqa._settings["rooms"]:
for device in room["devices"]:
if device["localDeviceId"] == u_id:
rooms.append(room)
# TODO: perhaps the routines can be put into automations or scenes in HA
routines = []
for routine in klyqa._settings["routines"]:
for task in routine["tasks"]:
for device in task["devices"]:
if device == u_id:
routines.append(routine)
# TODO: same for timers.
timers = []
for timer in klyqa._settings["timers"]:
for task in timer["tasks"]:
for device in task["devices"]:
if device == u_id:
timers.append(timer)
entities.append(
KlyqaLight(
device_settings,
light_state,
klyqa,
entity_id,
should_poll=True,
rooms=rooms,
timers=timers,
routines=routines,
)
)
add_entities(entities, True)
class KlyqaLight(LightEntity):
"""Representation of a Klyqa Light."""
_attr_supported_features = SUPPORT_KLYQA
_attr_transition_time = 500
_klyqa_api: Klyqa
_klyqa_device: KlyqaLightDevice
settings = {}
"""synchrononise rooms to HA"""
sync_rooms: bool = True
def __init__(
self,
settings,
device: KlyqaLightDevice,
klyqa_api,
entity_id,
should_poll=True,
rooms=None,
timers=None,
routines=None,
):
"""Initialize a Klyqa Light Bulb."""
self._klyqa_api = klyqa_api
self.u_id = settings["localDeviceId"]
self._klyqa_device = device
self.entity_id = entity_id
self._attr_should_poll = should_poll
self._attr_device_class = "light"
self._attr_icon = "mdi:lightbulb"
self.rooms = rooms
self.timers = timers
self.routines = routines
self._attr_supported_color_modes = {
COLOR_MODE_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_RGB,
# COLOR_MODE_RGBWW
}
self._attr_effect_list = [x["label"] for x in SCENES]
"""Entity state will be updated after adding the entity."""
async def async_update_settings(self):
"""Set device specific settings from the klyqa settings cloud."""
devices_settings = self._klyqa_api._settings["devices"]
device_result = [
x for x in devices_settings if str(x["localDeviceId"]) == self.u_id
]
if len(device_result) < 1:
return
response_object = await self.hass.async_add_executor_job(
self._klyqa_api.request_get_beared,
"/config/product/" + device_result[0]["productId"],
)
self.device_config = json.loads(response_object.text)
self.settings = device_result[0]
self._attr_name = self.settings["name"]
self._attr_unique_id = self.settings["localDeviceId"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self.name,
manufacturer="QConnex GmbH",
model=self.settings["productId"], # TODO: Maybe exclude.
sw_version=self.settings["firmwareVersion"],
hw_version=self.settings["hardwareRevision"], # TODO: Maybe exclude.
configuration_url="https://www.klyqa.de/produkte/e27-color-lampe", # TODO: Maybe exclude. Or make rest call for device url.
)
if len(self.rooms) > 0:
area_reg = ar.async_get(self.hass)
area = area_reg.async_get_area_by_name(self.rooms[0]["name"])
if area:
self._attr_device_info["suggested_area"] = area.name
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return True
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn off."""
entity_registry = er.async_get(self.hass)
await self.async_update_klyqa()
args = ["--power", "on"]
if ATTR_HS_COLOR in kwargs:
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
self._attr_rgb_color = (rgb[0], rgb[1], rgb[2])
self._attr_hs_color = kwargs[ATTR_HS_COLOR]
if ATTR_RGB_COLOR in kwargs:
self._attr_rgb_color = kwargs[ATTR_RGB_COLOR]
if ATTR_RGB_COLOR in kwargs or ATTR_HS_COLOR in kwargs:
args.extend(["--color", *([str(rgb) for rgb in self._attr_rgb_color])])
if ATTR_RGBWW_COLOR in kwargs:
self._attr_rgbww_color = kwargs[ATTR_RGBWW_COLOR]
args.extend(
["--percent_color", *([str(rgb) for rgb in self._attr_rgbww_color])]
)
if ATTR_EFFECT in kwargs:
scene_result = [x for x in SCENES if x["label"] == kwargs[ATTR_EFFECT]]
if len(scene_result) > 0:
scene = scene_result[0]
self._attr_effect = kwargs[ATTR_EFFECT]
commands = scene["commands"]
if len(commands.split(";")) > 2:
commands += "l 0;"
ret = self._klyqa_api.send_to_bulb(
"--routine_id",
"0",
"--routine_scene",
str(scene["id"]),
"--routine_put",
"--routine_command",
commands,
u_id=self.u_id,
)
if ret:
args.extend(
[
"--routine_id",
"0",
"--routine_start",
]
)
if ATTR_COLOR_TEMP in kwargs:
self._attr_color_temp = kwargs[ATTR_COLOR_TEMP]
args.extend(
[
"--temperature",
str(
color_temperature_mired_to_kelvin(self._attr_color_temp)
if self._attr_color_temp
else 0
),
]
)
if ATTR_TRANSITION in kwargs:
self._attr_transition_time = kwargs[ATTR_TRANSITION]
if self._attr_transition_time:
args.extend(["--transitionTime", str(self._attr_transition_time)])
if ATTR_BRIGHTNESS in kwargs:
self._attr_brightness = kwargs[ATTR_BRIGHTNESS]
args.extend(
["--brightness", str(round((self._attr_brightness / 255.0) * 100.0))]
)
if ATTR_BRIGHTNESS_PCT in kwargs:
self._attr_brightness = int(
round((kwargs[ATTR_BRIGHTNESS_PCT] / 100) * 255)
)
args.extend(["--brightness", str(ATTR_BRIGHTNESS_PCT)])
LOGGER.info(
"Send to bulb " + str(self.entity_id) + "%s: %s",
" (" + self.name + ")" if self.name else "",
" ".join(args),
)
ret = self._klyqa_api.send_to_bulb(*(args), u_id=self.u_id)
await self.async_update()
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
args = ["--power", "off"]
if self._attr_transition_time:
args.extend(["--transitionTime", str(self._attr_transition_time)])
await self.async_update_klyqa()
LOGGER.info(
"Send to bulb " + str(self.entity_id) + "%s: %s",
" (" + self.name + ")" if self.name else "",
" ".join(args),
)
ret = self._klyqa_api.send_to_bulb(*(args), u_id=self.u_id)
await self.async_update()
async def async_update_klyqa(self):
"""Fetch settings from klyqa cloud account."""
await self.hass.async_add_executor_job(self._klyqa_api.load_settings)
await self.async_update_settings()
# if self._attr_state == STATE_UNAVAILABLE:
# await self.hass.async_add_executor_job(self._klyqa_api.search_missing_bulbs)
async def async_update(self):
"""Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
await self.async_update_klyqa()
ret = self._klyqa_api.send_to_bulb("--request", u_id=self.u_id)
self._update_state(ret)
def _update_state(self, state_complete):
"""Process state request response from the bulb to the entity state."""
# self.state = STATE_OK if state_complete else STATE_UNAVAILABLE
self._attr_state = STATE_OK if state_complete else STATE_UNAVAILABLE
if self._attr_state == STATE_UNAVAILABLE:
self._attr_is_on = False
# self._attr_available = False
else:
self._attr_available = True
if not self._attr_state:
LOGGER.info(
"Bulb " + str(self.entity_id) + "%s unavailable.",
" (" + self.name + ")" if self.name else "",
)
if not state_complete or not isinstance(state_complete, dict):
return
LOGGER.info(
"Update bulb " + str(self.entity_id) + "%s.",
" (" + self.name + ")" if self.name else "",
)
if "error" in state_complete:
LOGGER.error(state_complete["error"])
return
if state_complete.get("type") == "error":
LOGGER.error(state_complete["type"])
return
state_type = state_complete.get("type")
if not state_type or state_type != "status":
return
self._klyqa_device.state = state_complete
self._attr_color_temp = (
color_temperature_kelvin_to_mired(state_complete["temperature"])
if state_complete["temperature"]
else 0
)
self._attr_rgb_color = (
state_complete["color"]["red"],
state_complete["color"]["green"],
state_complete["color"]["blue"],
)
self._attr_hs_color = color_util.color_RGB_to_hs(*self._attr_rgb_color)
# interpolate brightness from klyqa bulb 0 - 100 percent to homeassistant 0 - 255 points
self._attr_brightness = (
float(state_complete["brightness"]["percentage"]) / 100
) * 255
self._attr_is_on = state_complete["status"] == "on"
self._attr_color_mode = (
COLOR_MODE_COLOR_TEMP
if state_complete["mode"] == "cct"
else "effect"
if state_complete["mode"] == "cmd"
else state_complete["mode"]
)
self._attr_effect = ""
if "active_scene" in state_complete and state_complete["mode"] == "cmd":
scene_result = [
x for x in SCENES if str(x["id"]) == state_complete["active_scene"]
]
if len(scene_result) > 0:
self._attr_effect = scene_result[0]["label"]