|
28 | 28 | CONF_PORT,
|
29 | 29 | CONF_PROTOCOL,
|
30 | 30 | CONF_USERNAME,
|
| 31 | + Platform, |
31 | 32 | )
|
32 | 33 | from .lib.net.device import UNILED_TRANSPORT_NET, UniledNetDevice, UniledNetModel
|
33 | 34 | from .lib.ble.device import UNILED_TRANSPORT_BLE, UniledBleDevice, UniledBleModel
|
|
43 | 44 | )
|
44 | 45 | from .const import (
|
45 | 46 | DOMAIN,
|
| 47 | + ATTR_UL_CHIP_TYPE, |
| 48 | + ATTR_UL_LIGHT_TYPE, |
46 | 49 | CONF_UL_RETRY_COUNT as CONF_RETRY_COUNT,
|
47 | 50 | CONF_UL_TRANSPORT as CONF_TRANSPORT,
|
48 | 51 | CONF_UL_UPDATE_INTERVAL as CONF_UPDATE_INTERVAL,
|
|
52 | 55 | UNILED_MIN_UPDATE_INTERVAL as MIN_UPDATE_INTERVAL,
|
53 | 56 | UNILED_DEF_UPDATE_INTERVAL as DEFAULT_UPDATE_INTERVAL,
|
54 | 57 | UNILED_MAX_UPDATE_INTERVAL as MAX_UPDATE_INTERVAL,
|
| 58 | + UNILED_OPTIONS_ATTRIBUTES, |
55 | 59 | )
|
| 60 | +from .coordinator import UniledUpdateCoordinator |
| 61 | + |
| 62 | +import homeassistant.helpers.config_validation as cv |
56 | 63 | import voluptuous as vol
|
57 | 64 | import functools
|
58 | 65 | import operator
|
@@ -103,6 +110,21 @@ def _mesh_get_context(self) -> dict:
|
103 | 110 | "options": options,
|
104 | 111 | }
|
105 | 112 |
|
| 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 | + |
106 | 128 | async def async_step_mesh_cloud(
|
107 | 129 | self, user_input: dict[str, Any] | None = None
|
108 | 130 | ) -> FlowResult:
|
@@ -149,13 +171,13 @@ async def async_step_mesh_cloud(
|
149 | 171 | if has_changed:
|
150 | 172 | # _LOGGER.info(f"Updating configuration: {info['data']}")
|
151 | 173 | self.hass.config_entries.async_update_entry(
|
152 |
| - self.config_entry, data=info['data'] |
| 174 | + self.config_entry, data=info["data"] |
153 | 175 | )
|
154 |
| - return self.async_create_entry(title="", data=info['options']) |
| 176 | + return self.async_create_entry(title="", data=info["options"]) |
155 | 177 | return self.async_create_entry(
|
156 | 178 | title=self.context["title_placeholders"]["name"],
|
157 |
| - data=info['data'], |
158 |
| - options=info['options'] |
| 179 | + data=info["data"], |
| 180 | + options=info["options"], |
159 | 181 | )
|
160 | 182 | else:
|
161 | 183 | errors[CONF_COUNTRY] = "mesh_no_devices"
|
@@ -197,26 +219,206 @@ async def async_step_init(
|
197 | 219 | self, user_input: dict[str, Any] | None = None
|
198 | 220 | ) -> FlowResult:
|
199 | 221 | """Manage the options."""
|
200 |
| - self._mesh_set_context() |
| 222 | + self.coordinator: UniledUpdateCoordinator = self.hass.data[DOMAIN][ |
| 223 | + self.config_entry.entry_id |
| 224 | + ] |
| 225 | + |
201 | 226 | if self.config_entry.data.get(CONF_TRANSPORT) == UNILED_TRANSPORT_ZNG:
|
| 227 | + self._mesh_set_context() |
202 | 228 | 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() |
203 | 240 | return await self.async_step_tune_comms()
|
204 | 241 |
|
205 |
| - async def async_step_mesh_menu( |
| 242 | + async def async_step_conf_menu( |
206 | 243 | self, user_input: dict[str, Any] | None = None
|
207 | 244 | ) -> 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") |
211 | 248 | 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, |
214 | 337 | description_placeholders={
|
215 |
| - "mesh_title": self._mesh_title(mesh_uuid), |
216 |
| - "mesh_uuid": hex(mesh_uuid), |
| 338 | + "channel_name": channel.name, |
217 | 339 | },
|
| 340 | + errors=errors, |
218 | 341 | )
|
219 | 342 |
|
| 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 | + |
220 | 422 | async def async_step_tune_comms(
|
221 | 423 | self, user_input: dict[str, Any] | None = None
|
222 | 424 | ) -> FlowResult:
|
|
0 commit comments