Skip to content

Commit b1a8734

Browse files
committed
Add support for device configuration to config_flow
1 parent aaae28c commit b1a8734

File tree

10 files changed

+382
-79
lines changed

10 files changed

+382
-79
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,6 @@ custom_components/uniled/lib/zng/__pycache__
136136
custom_components/uniled/lib/__pycache__
137137
custom_components/uniled/__pycache__
138138
custom_components/zengge_mesh
139+
custom_components/uniled/significant_change.py
140+
custom_components/uniled/reproduce_state.py
141+
.gitignore

custom_components/uniled/config_flow.py

+215-13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
CONF_PORT,
2929
CONF_PROTOCOL,
3030
CONF_USERNAME,
31+
Platform,
3132
)
3233
from .lib.net.device import UNILED_TRANSPORT_NET, UniledNetDevice, UniledNetModel
3334
from .lib.ble.device import UNILED_TRANSPORT_BLE, UniledBleDevice, UniledBleModel
@@ -43,6 +44,8 @@
4344
)
4445
from .const import (
4546
DOMAIN,
47+
ATTR_UL_CHIP_TYPE,
48+
ATTR_UL_LIGHT_TYPE,
4649
CONF_UL_RETRY_COUNT as CONF_RETRY_COUNT,
4750
CONF_UL_TRANSPORT as CONF_TRANSPORT,
4851
CONF_UL_UPDATE_INTERVAL as CONF_UPDATE_INTERVAL,
@@ -52,7 +55,11 @@
5255
UNILED_MIN_UPDATE_INTERVAL as MIN_UPDATE_INTERVAL,
5356
UNILED_DEF_UPDATE_INTERVAL as DEFAULT_UPDATE_INTERVAL,
5457
UNILED_MAX_UPDATE_INTERVAL as MAX_UPDATE_INTERVAL,
58+
UNILED_OPTIONS_ATTRIBUTES,
5559
)
60+
from .coordinator import UniledUpdateCoordinator
61+
62+
import homeassistant.helpers.config_validation as cv
5663
import voluptuous as vol
5764
import functools
5865
import operator
@@ -103,6 +110,21 @@ def _mesh_get_context(self) -> dict:
103110
"options": options,
104111
}
105112

113+
async def async_step_mesh_menu(
114+
self, user_input: dict[str, Any] | None = None
115+
) -> FlowResult:
116+
"""Menu step"""
117+
mesh_uuid = self.context.get(CONF_MESH_UUID, 0)
118+
119+
return self.async_show_menu(
120+
step_id="mesh_menu",
121+
menu_options=["mesh_cloud", "tune_comms"],
122+
description_placeholders={
123+
"mesh_title": self._mesh_title(mesh_uuid),
124+
"mesh_uuid": hex(mesh_uuid),
125+
},
126+
)
127+
106128
async def async_step_mesh_cloud(
107129
self, user_input: dict[str, Any] | None = None
108130
) -> FlowResult:
@@ -149,13 +171,13 @@ async def async_step_mesh_cloud(
149171
if has_changed:
150172
# _LOGGER.info(f"Updating configuration: {info['data']}")
151173
self.hass.config_entries.async_update_entry(
152-
self.config_entry, data=info['data']
174+
self.config_entry, data=info["data"]
153175
)
154-
return self.async_create_entry(title="", data=info['options'])
176+
return self.async_create_entry(title="", data=info["options"])
155177
return self.async_create_entry(
156178
title=self.context["title_placeholders"]["name"],
157-
data=info['data'],
158-
options=info['options']
179+
data=info["data"],
180+
options=info["options"],
159181
)
160182
else:
161183
errors[CONF_COUNTRY] = "mesh_no_devices"
@@ -197,26 +219,206 @@ async def async_step_init(
197219
self, user_input: dict[str, Any] | None = None
198220
) -> FlowResult:
199221
"""Manage the options."""
200-
self._mesh_set_context()
222+
self.coordinator: UniledUpdateCoordinator = self.hass.data[DOMAIN][
223+
self.config_entry.entry_id
224+
]
225+
201226
if self.config_entry.data.get(CONF_TRANSPORT) == UNILED_TRANSPORT_ZNG:
227+
self._mesh_set_context()
202228
return await self.async_step_mesh_menu()
229+
230+
config_options = 0
231+
for channel in self.coordinator.device.channel_list:
232+
if not channel.features:
233+
continue
234+
for feature in channel.features:
235+
if feature.attr in UNILED_OPTIONS_ATTRIBUTES:
236+
config_options += 1
237+
238+
if config_options:
239+
return await self.async_step_conf_menu()
203240
return await self.async_step_tune_comms()
204241

205-
async def async_step_mesh_menu(
242+
async def async_step_conf_menu(
206243
self, user_input: dict[str, Any] | None = None
207244
) -> FlowResult:
208-
"""Menu step"""
209-
mesh_uuid = self.context.get(CONF_MESH_UUID, 0)
210-
245+
"""Configuration menu step"""
246+
if not self.coordinator.device.available:
247+
return self.async_abort(reason="not_available")
211248
return self.async_show_menu(
212-
step_id="mesh_menu",
213-
menu_options=["tune_comms", "mesh_cloud"],
249+
step_id="conf_menu",
250+
menu_options=["channels", "tune_comms"],
251+
)
252+
253+
async def async_step_channels(
254+
self, user_input: dict[str, Any] | None = None
255+
) -> FlowResult:
256+
"""Channels Menu"""
257+
if self.coordinator.device.channels > 1:
258+
if user_input is not None:
259+
channel_id = int(user_input.get("channel", 0))
260+
else:
261+
channels = {}
262+
for channel in self.coordinator.device.channel_list:
263+
if not channel.features:
264+
continue
265+
for feature in channel.features:
266+
if feature.attr in UNILED_OPTIONS_ATTRIBUTES:
267+
channels[channel.number] = channel.name
268+
break
269+
270+
if len(channels) > 1:
271+
data_schema = vol.Schema(
272+
{
273+
vol.Required("channel"): vol.In(
274+
{number: name for number, name in channels.items()}
275+
),
276+
}
277+
)
278+
return self.async_show_form(
279+
step_id="channels",
280+
data_schema=data_schema,
281+
)
282+
elif len(channels) == 1:
283+
channel_id = next(iter(channels))
284+
else:
285+
return self.async_abort(reason="no_configurable")
286+
else:
287+
channel_id = 0
288+
channel = self.coordinator.device.channel(channel_id)
289+
self.context["channel"] = channel
290+
if channel.has(ATTR_UL_LIGHT_TYPE) or channel.has(ATTR_UL_CHIP_TYPE):
291+
return await self.async_step_conf_type()
292+
return await self.async_step_conf_channel()
293+
294+
async def async_step_conf_type(
295+
self, user_input: dict[str, Any] | None = None
296+
) -> FlowResult:
297+
"""Configure type"""
298+
errors: dict[str, str] = {}
299+
channel = self.context.get("channel", None)
300+
301+
if (conf_value := channel.get(ATTR_UL_LIGHT_TYPE, None)) is not None:
302+
conf_attr = ATTR_UL_LIGHT_TYPE
303+
elif (conf_value := channel.get(ATTR_UL_CHIP_TYPE, None)) is not None:
304+
conf_attr = ATTR_UL_CHIP_TYPE
305+
else:
306+
return await self.async_step_conf_channel()
307+
308+
if not self.coordinator.device.available:
309+
errors[conf_attr] = "not_available"
310+
311+
if user_input is not None and not errors:
312+
conf_value = user_input.get(conf_attr, conf_value)
313+
if conf_value != channel.get(conf_attr, conf_value):
314+
if await self.coordinator.device.async_set_state(
315+
channel, conf_attr, conf_value
316+
):
317+
self.options[conf_attr] = conf_value
318+
return self.async_create_entry(title="", data=self.options)
319+
else:
320+
errors[conf_attr] = "unknown"
321+
else:
322+
return await self.async_step_conf_channel()
323+
324+
data_schema = vol.Schema(
325+
{
326+
vol.Required(conf_attr, default=conf_value): SelectSelector(
327+
SelectSelectorConfig(
328+
mode=SelectSelectorMode.DROPDOWN,
329+
options=self.coordinator.device.get_list(channel, conf_attr),
330+
)
331+
),
332+
}
333+
)
334+
return self.async_show_form(
335+
step_id="conf_type",
336+
data_schema=data_schema,
214337
description_placeholders={
215-
"mesh_title": self._mesh_title(mesh_uuid),
216-
"mesh_uuid": hex(mesh_uuid),
338+
"channel_name": channel.name,
217339
},
340+
errors=errors,
218341
)
219342

343+
async def async_step_conf_channel(
344+
self, user_input: dict[str, Any] | None = None
345+
) -> FlowResult:
346+
"""Configure a channel"""
347+
errors: dict[str, str] = {}
348+
channel = self.context.get("channel", None)
349+
350+
if user_input is not None and not errors:
351+
for conf_attr, conf_value in user_input.items():
352+
_LOGGER.warn("%s = %s", conf_attr, conf_value)
353+
if not self.coordinator.device.available:
354+
errors[conf_attr] = "not_available"
355+
elif conf_value != channel.get(conf_attr, conf_value):
356+
if await self.coordinator.device.async_set_state(
357+
channel, conf_attr, conf_value
358+
):
359+
self.options[conf_attr] = conf_value
360+
else:
361+
errors[conf_attr] = "unknown"
362+
363+
if user_input is None or errors:
364+
schema = None
365+
for conf_attr in UNILED_OPTIONS_ATTRIBUTES:
366+
if conf_attr == ATTR_UL_LIGHT_TYPE or conf_attr == ATTR_UL_CHIP_TYPE:
367+
continue
368+
for feature in channel.features:
369+
if feature.attr != conf_attr:
370+
continue
371+
if (conf_value := channel.get(conf_attr, None)) is None:
372+
break
373+
if feature.platform == Platform.NUMBER:
374+
option = {
375+
vol.Required(conf_attr, default=conf_value): vol.All(
376+
vol.Coerce(int),
377+
vol.Range(min=feature.min_value, max=feature.max_value),
378+
),
379+
}
380+
elif feature.platform == Platform.SELECT:
381+
option = {
382+
vol.Required(conf_attr, default=conf_value): SelectSelector(
383+
SelectSelectorConfig(
384+
mode=SelectSelectorMode.DROPDOWN,
385+
options=self.coordinator.device.get_list(
386+
channel, conf_attr
387+
),
388+
)
389+
),
390+
}
391+
elif feature.platform == Platform.SWITCH:
392+
option = {vol.Required(conf_attr, default=conf_value): cv.boolean}
393+
else:
394+
_LOGGER.warning(
395+
"Unsupported feature platform: '%s' for '%s'.",
396+
feature.platform,
397+
feature.attr,
398+
)
399+
break
400+
if option and schema is None:
401+
schema = vol.Schema(option)
402+
elif option and schema:
403+
schema = schema.extend(option)
404+
break
405+
406+
if schema is not None:
407+
return self.async_show_form(
408+
step_id="conf_channel",
409+
data_schema=schema,
410+
description_placeholders={
411+
"channel_name": channel.name,
412+
"light_type": channel.get(ATTR_UL_LIGHT_TYPE, None),
413+
"chip_type": channel.get(ATTR_UL_CHIP_TYPE, None),
414+
},
415+
errors=errors,
416+
)
417+
418+
if not self.coordinator.device.available:
419+
return self.async_abort(reason="not_available")
420+
return self.async_create_entry(title="", data=self.options)
421+
220422
async def async_step_tune_comms(
221423
self, user_input: dict[str, Any] | None = None
222424
) -> FlowResult:

custom_components/uniled/entity.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
ATTR_UL_STATUS,
5050
ATTR_UL_SUGGESTED_AREA,
5151
ATTR_UL_TOTAL_PIXELS,
52+
UNILED_ENTITY_ATTRIBUTES,
53+
UNILED_OPTIONS_ATTRIBUTES,
5254
UNILED_STATE_CHANGE_LATENCY,
5355
)
5456

@@ -132,6 +134,7 @@ def async_uniled_entity_update(
132134
if (
133135
not feature.platform.startswith(platform)
134136
or feature.group == UniledGroup.OPTION
137+
or feature.attr in UNILED_OPTIONS_ATTRIBUTES
135138
):
136139
continue
137140
if entity := async_add_entity(coordinator, channel, feature):
@@ -181,19 +184,18 @@ def __init__(
181184
self._device: UniledDevice = coordinator.device
182185
self._channel: UniledChannel = channel
183186
self._feature: UniledAttribute = feature
187+
# TODO: This is buggy: self.platform is not set, throwing errors?
188+
# self._attr_translation_key = feature.attr
184189
self._attr_has_entity_name = True
185190

186191
if channel.name:
187192
if feature.name.lower() not in channel.name.lower():
188193
self._attr_name = f"{channel.name} {feature.name}"
189194
else:
190195
self._attr_name = f"{channel.name}"
191-
else:
196+
elif feature.name and feature.name.lower() != feature.platform:
192197
self._attr_name = f"{feature.name}"
193198

194-
if self._attr_name.lower() == feature.platform:
195-
self._attr_name = None
196-
197199
base_unique_id = coordinator.entry.unique_id or coordinator.entry.entry_id
198200
self._attr_unique_id = f"_{base_unique_id}"
199201

@@ -205,6 +207,7 @@ def __init__(
205207
self._attr_unique_id = f"{self._attr_unique_id}_{key}"
206208

207209
# _LOGGER.debug("%s: %s (%s)", self._attr_unique_id, self._attr_name, channel.name)
210+
# _LOGGER.warn("%s: %s %s %s", self._attr_unique_id, self.name, self.feature.platform, self.platform)
208211

209212
self._attr_entity_registry_enabled_default = feature.enabled
210213
self._attr_entity_category = None
@@ -291,6 +294,8 @@ def extra_state_attributes(self):
291294
extra = {}
292295
if self.feature and self.feature.extra:
293296
for x in self.feature.extra:
297+
if x in UNILED_ENTITY_ATTRIBUTES:
298+
continue
294299
if (value := self.device.get_state(self.channel, x)) is not None:
295300
extra[x] = value
296301
return extra

custom_components/uniled/lib/ble/banlanx2.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,7 @@ def parse_notifications(
404404

405405
if not device.master.features:
406406
features = [
407-
LightStripFeature(extra=(
408-
ATTR_UL_LIGHT_MODE,
409-
ATTR_UL_LIGHT_MODE_NUMBER,
410-
ATTR_UL_EFFECT_NUMBER,
411-
ATTR_UL_EFFECT_SPEED,
412-
)),
407+
LightStripFeature(extra=UNILED_CONTROL_ATTRIBUTES),
413408
EffectTypeFeature(),
414409
EffectSpeedFeature(BANLANX2_MAX_EFFECT_SPEED),
415410
EffectLengthFeature(BANLANX2_MAX_EFFECT_LENGTH),

custom_components/uniled/lib/ble/banlanx3.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,7 @@ def parse_notifications(
256256

257257
if not device.master.features:
258258
features = [
259-
LightStripFeature(
260-
extra=(
261-
ATTR_UL_LIGHT_MODE,
262-
ATTR_UL_LIGHT_MODE_NUMBER,
263-
ATTR_UL_EFFECT_NUMBER,
264-
ATTR_UL_EFFECT_SPEED,
265-
)
266-
),
259+
LightStripFeature(extra=UNILED_CONTROL_ATTRIBUTES),
267260
EffectTypeFeature(),
268261
EffectSpeedFeature(BANLANX3_MAX_EFFECT_SPEED),
269262
ChipOrderFeature(),

0 commit comments

Comments
 (0)