295 lines
10 KiB
Python
295 lines
10 KiB
Python
from homeassistant.components.select import SelectEntity
|
|
from homeassistant.core import callback
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
|
|
|
from . import DOMAIN
|
|
from .core import utils
|
|
from .core.converters import Converter
|
|
from .core.device import XDevice, RE_DID
|
|
from .core.entity import XEntity
|
|
from .core.gateway import XGateway
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
def setup(gateway: XGateway, device: XDevice, conv: Converter):
|
|
if conv.attr in device.entities:
|
|
entity: XEntity = device.entities[conv.attr]
|
|
entity.gw = gateway
|
|
elif conv.attr == "command":
|
|
entity = CommandSelect(gateway, device, conv)
|
|
elif conv.attr == "data":
|
|
entity = DataSelect(gateway, device, conv)
|
|
else:
|
|
entity = XiaomiSelect(gateway, device, conv)
|
|
async_add_entities([entity])
|
|
|
|
gw: XGateway = hass.data[DOMAIN][config_entry.entry_id]
|
|
gw.add_setup(__name__, setup)
|
|
|
|
|
|
# noinspection PyAbstractClass
|
|
class XiaomiSelectBase(XEntity, SelectEntity):
|
|
_attr_current_option: str = None
|
|
|
|
def __init__(self, gateway: 'XGateway', device: XDevice, conv: Converter):
|
|
super().__init__(gateway, device, conv)
|
|
|
|
if hasattr(conv, "map"):
|
|
self._attr_options = list(conv.map.values())
|
|
|
|
@callback
|
|
def async_set_state(self, data: dict):
|
|
if self.attr in data:
|
|
self._attr_current_option = data[self.attr]
|
|
|
|
|
|
# noinspection PyAbstractClass
|
|
class XiaomiSelect(XiaomiSelectBase, RestoreEntity):
|
|
@callback
|
|
def async_restore_last_state(self, state: str, attrs: dict):
|
|
self._attr_current_option = state
|
|
|
|
async def async_select_option(self, option: str):
|
|
await self.device_send({self.attr: option})
|
|
|
|
async def async_update(self):
|
|
await self.device_read(self.subscribed_attrs)
|
|
|
|
|
|
# noinspection PyAbstractClass
|
|
class CommandSelect(XEntity, SelectEntity):
|
|
_attr_current_option = None
|
|
_attr_device_class = "command"
|
|
|
|
def __init__(self, gateway: 'XGateway', device: XDevice, conv: Converter):
|
|
super().__init__(gateway, device, conv)
|
|
if device.model == "lumi.gateway.mgl03":
|
|
self._attr_options = [
|
|
"pair", "bind", "ota", "config", "parentscan", "firmwarelock",
|
|
"reboot", "ftp"
|
|
]
|
|
elif device.model.startswith("lumi.gateway.aqcn0"):
|
|
self._attr_options = [
|
|
"pair", "bind", "ota", "config", "parentscan", "reboot", "ftp"
|
|
]
|
|
elif device.model.startswith("lumi.gateway.mcn001"):
|
|
self._attr_options = [
|
|
"pair", "bind", "ota", "config", "parentscan", "reboot", "ftp"
|
|
]
|
|
|
|
@callback
|
|
def async_set_state(self, data: dict):
|
|
if self.attr in data:
|
|
self._attr_current_option = data[self.attr]
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
# clear select.data
|
|
self.device.update({"data": None})
|
|
|
|
if option == "pair":
|
|
await self.device_send({"pair": True})
|
|
elif option in ("bind", "config", "ota"):
|
|
self.device.update({"command": option})
|
|
elif option == "firmwarelock":
|
|
lock = await self.gw.gw3_read_lock()
|
|
self.device.update({"command": option, "lock": lock})
|
|
elif option in ("ftp", "reboot"):
|
|
ok = await self.gw.telnet_send(option)
|
|
self.device.update({"command": option, "ok": ok})
|
|
elif option == "parentscan":
|
|
await self.gw.z3_run_parent_scan()
|
|
self.device.update({"command": option, "ok": True})
|
|
|
|
|
|
OPT_ENABLED = "enabled"
|
|
OPT_DISABLED = "disabled"
|
|
OPT_UNKNOWN = "unknown"
|
|
OPT_OK = "ok"
|
|
OPT_ERROR = "error"
|
|
OPT_JOIN = "permit_join"
|
|
OPT_STOP_JOIN = "stop_join"
|
|
OPT_CANCEL = "cancel"
|
|
OPT_KEY_SECURE = "key_secure"
|
|
OPT_KEY_LEGACY = "key_legacy"
|
|
OPT_NO_FIRMWARE = "no_firmware"
|
|
OPT_BIND = "bind"
|
|
OPT_UNBIND = "unbind"
|
|
OPT_NO_DEVICES = "no_devices"
|
|
|
|
|
|
# noinspection PyAbstractClass,PyUnusedLocal
|
|
class DataSelect(XEntity, SelectEntity):
|
|
_attr_current_option = None
|
|
_attr_device_class = "data"
|
|
_attr_options = None
|
|
step_id = None
|
|
kwargs = None
|
|
|
|
def set_options(self, option: str = None, options: list = None):
|
|
"""Change current option with options list dynamically."""
|
|
# always change to None first time
|
|
self._attr_current_option = None
|
|
|
|
if option:
|
|
self._attr_options = options or [option]
|
|
# important to change current option with delay after options
|
|
self.hass.async_create_task(self.async_set_current(option))
|
|
else:
|
|
# better not leave options list empty
|
|
self._attr_options = options or []
|
|
|
|
async def async_set_current(self, option: str):
|
|
"""Delayed update current option."""
|
|
self._attr_current_option = option
|
|
self.async_write_ha_state()
|
|
|
|
def set_devices(self, feature: str):
|
|
"""Set options list with devices list."""
|
|
devices = [
|
|
f"{d.mac}: {d.name}" for d in self.gw.filter_devices(feature)
|
|
]
|
|
if devices:
|
|
self.set_options(None, devices)
|
|
else:
|
|
self.set_options(OPT_NO_DEVICES)
|
|
|
|
def set_end(self, option: str):
|
|
self.set_options(option)
|
|
self.device.update({"command": None})
|
|
|
|
###########################################################################
|
|
|
|
@callback
|
|
def async_set_state(self, data: dict):
|
|
"""Can process:
|
|
- command step, done by user from another select
|
|
- event step, done by receiving some data from gateway
|
|
"""
|
|
if "command" in data:
|
|
self.step_id = data["command"]
|
|
func = getattr(self, f"step_command_{self.step_id}", None)
|
|
else:
|
|
self.step_id = next(k for k in self.subscribed_attrs if k in data)
|
|
data = data[self.step_id]
|
|
func = getattr(self, f"step_event_{self.step_id}", None)
|
|
|
|
if func:
|
|
func(data)
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
"""Can process user step, done by user from this select"""
|
|
coro = getattr(self, f"step_user_{self.step_id}", None)
|
|
if coro:
|
|
await coro(option)
|
|
|
|
###########################################################################
|
|
|
|
def step_event_data(self, value: str):
|
|
# update select.data value from outside
|
|
self.set_options(value)
|
|
|
|
def step_event_pair(self, value: bool):
|
|
if value:
|
|
self.set_options(OPT_JOIN, [OPT_JOIN, OPT_CANCEL])
|
|
# change select.command
|
|
self.device.update({"command": "pair"})
|
|
else:
|
|
# clear select.command
|
|
self.device.update({"command": None})
|
|
|
|
async def step_user_pair(self, option: str):
|
|
assert option == OPT_CANCEL
|
|
await self.device_send({"pair": False})
|
|
|
|
def step_event_discovered_mac(self, value: str):
|
|
self.set_options(f"Discovered: 0x{value:>016s}")
|
|
|
|
def step_event_pair_command(self, value: dict):
|
|
secure = value["install_code"]
|
|
option = OPT_KEY_SECURE if secure else OPT_KEY_LEGACY
|
|
self.set_options(option, self.options + [option])
|
|
|
|
def step_event_added_device(self, value: dict):
|
|
option = f"Paired: {value['model']}"
|
|
self.set_options(option, self.options + [option])
|
|
|
|
def step_event_remove_did(self, value: str):
|
|
assert RE_DID.search(value)
|
|
device = self.gw.devices.get(value)
|
|
utils.remove_device(self.hass, device)
|
|
self.set_end(f"Removed: {value[5:]}")
|
|
|
|
def step_event_ota_progress(self, value: int):
|
|
self.set_options(f"Update progress: {value}%")
|
|
|
|
def step_command_config(self, value: dict):
|
|
self.set_devices("zigbee")
|
|
|
|
async def step_user_config(self, option: str):
|
|
did = "lumi." + option[:18].lstrip("0x")
|
|
device = self.gw.devices.get(did)
|
|
await self.gw.silabs_rejoin(device)
|
|
|
|
def step_command_ota(self, value: dict):
|
|
self.set_devices("zigbee")
|
|
|
|
async def step_user_ota(self, option: str):
|
|
did = "lumi." + option[:18].lstrip("0x")
|
|
device = self.gw.devices.get(did)
|
|
resp = await utils.run_zigbee_ota(self.hass, self.gw, device)
|
|
if resp is True:
|
|
resp = OPT_OK
|
|
elif resp is False:
|
|
resp = OPT_NO_FIRMWARE
|
|
else:
|
|
resp = OPT_ERROR
|
|
self.set_end(resp)
|
|
|
|
def step_command_bind(self, value: dict):
|
|
self.set_devices(self.step_id)
|
|
self.step_id = "bind_from"
|
|
|
|
async def step_user_bind_from(self, option: str):
|
|
did = "lumi." + option[:18].lstrip("0x")
|
|
self.kwargs = {"bind_from": self.gw.devices.get(did)}
|
|
self.set_devices("bind_to")
|
|
self.step_id = "bind_to"
|
|
|
|
async def step_user_bind_to(self, option: str):
|
|
did = "lumi." + option[:18].lstrip("0x")
|
|
self.kwargs["bind_to"] = self.gw.devices.get(did)
|
|
self.set_options(None, [OPT_BIND, OPT_UNBIND])
|
|
self.step_id = "bind_type"
|
|
|
|
async def step_user_bind_type(self, option: str):
|
|
if option == OPT_BIND:
|
|
await self.gw.silabs_bind(**self.kwargs)
|
|
else:
|
|
await self.gw.silabs_unbind(**self.kwargs)
|
|
self.set_end(OPT_OK)
|
|
|
|
def step_command_firmwarelock(self, value: dict):
|
|
lock = value["lock"]
|
|
if lock is None:
|
|
self.set_options(
|
|
OPT_UNKNOWN, [OPT_UNKNOWN, OPT_ENABLED, OPT_DISABLED]
|
|
)
|
|
else:
|
|
self.set_options(
|
|
OPT_ENABLED if lock else OPT_DISABLED,
|
|
[OPT_ENABLED, OPT_DISABLED]
|
|
)
|
|
|
|
async def step_user_firmwarelock(self, option: str):
|
|
ok = await self.gw.gw3_send_lock(option == OPT_ENABLED)
|
|
self.set_end(OPT_OK if ok else OPT_ERROR)
|
|
|
|
def step_command_reboot(self, value: dict):
|
|
self.set_end(OPT_OK if value["ok"] else OPT_ERROR)
|
|
|
|
def step_command_ftp(self, value: dict):
|
|
self.step_command_reboot(value)
|
|
|
|
def step_command_parentscan(self, value: dict):
|
|
self.step_command_reboot(value)
|