Files
2024-01-11 15:10:51 +03:00

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)