Improve Elk-M1 Control typing (#69924)

* Add types to __init__.py

* Fixup typing.

* Fix type error.

* Bump lib to fix login error.

Co-authored-by: Shay Levy <levyshay1@gmail.com>
pull/70127/head
Glenn Waters 2022-04-15 17:14:45 -04:00 committed by GitHub
parent f8367d3c01
commit c80853496d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 78 additions and 46 deletions

View File

@ -81,6 +81,7 @@ homeassistant.components.dsmr.*
homeassistant.components.dunehd.*
homeassistant.components.efergy.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.__init__
homeassistant.components.esphome.*
homeassistant.components.energy.*
homeassistant.components.evil_genius_labs.*

View File

@ -5,11 +5,12 @@ import asyncio
import logging
import re
from types import MappingProxyType
from typing import Any
from typing import Any, cast
from urllib.parse import urlparse
import async_timeout
import elkm1_lib as elkm1
from elkm1_lib.elements import Element
from elkm1_lib.elk import Elk
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -22,6 +23,7 @@ from homeassistant.const import (
CONF_PREFIX,
CONF_TEMPERATURE_UNIT,
CONF_USERNAME,
CONF_ZONE,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
Platform,
@ -50,7 +52,6 @@ from .const import (
CONF_SETTING,
CONF_TASK,
CONF_THERMOSTAT,
CONF_ZONE,
DISCOVER_SCAN_TIMEOUT,
DISCOVERY_INTERVAL,
DOMAIN,
@ -92,7 +93,7 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema(
)
def _host_validator(config):
def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"):
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
@ -104,14 +105,14 @@ def _host_validator(config):
return config
def _elk_range_validator(rng):
def _housecode_to_int(val):
def _elk_range_validator(rng: str) -> tuple[int, int]:
def _housecode_to_int(val: str) -> int:
match = re.search(r"^([a-p])(0[1-9]|1[0-6]|[1-9])$", val.lower())
if match:
return (ord(match.group(1)) - ord("a")) * 16 + int(match.group(2))
raise vol.Invalid("Invalid range")
def _elk_value(val):
def _elk_value(val: str) -> int:
return int(val) if val.isdigit() else _housecode_to_int(val)
vals = [s.strip() for s in str(rng).split("-")]
@ -120,7 +121,7 @@ def _elk_range_validator(rng):
return (start, end)
def _has_all_unique_prefixes(value):
def _has_all_unique_prefixes(value: list[dict[str, str]]) -> list[dict[str, str]]:
"""Validate that each m1 configured has a unique prefix.
Uniqueness is determined case-independently.
@ -214,10 +215,13 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
@callback
def _async_find_matching_config_entry(hass, prefix):
def _async_find_matching_config_entry(
hass: HomeAssistant, prefix: str
) -> ConfigEntry | None:
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.unique_id == prefix:
return entry
return None
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -253,7 +257,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Config item: %s; %s", item, err)
return False
elk = elkm1.Elk(
elk = Elk(
{
"url": conf[CONF_HOST],
"userid": conf[CONF_USERNAME],
@ -262,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
elk.connect()
def _element_changed(element, changeset):
def _element_changed(element: Element, changeset: dict[str, Any]) -> None:
if (keypress := changeset.get("last_keypress")) is None:
return
@ -275,7 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
},
)
for keypad in elk.keypads: # pylint: disable=no-member
for keypad in elk.keypads:
keypad.add_callback(_element_changed)
try:
@ -284,7 +288,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except asyncio.TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc
elk_temp_unit = elk.panel.temperature_units # pylint: disable=no-member
elk_temp_unit = elk.panel.temperature_units
temperature_unit = TEMP_CELSIUS if elk_temp_unit == "C" else TEMP_FAHRENHEIT
config["temperature_unit"] = temperature_unit
hass.data[DOMAIN][entry.entry_id] = {
@ -301,18 +305,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
def _included(ranges, set_to, values):
def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -> None:
for rng in ranges:
if not rng[0] <= rng[1] <= len(values):
raise vol.Invalid(f"Invalid range {rng}")
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
def _find_elk_by_prefix(hass, prefix):
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
"""Search all config entries for a given prefix."""
for entry_id in hass.data[DOMAIN]:
if hass.data[DOMAIN][entry_id]["prefix"] == prefix:
return hass.data[DOMAIN][entry_id]["elk"]
return cast(Elk, hass.data[DOMAIN][entry_id]["elk"])
return None
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -329,7 +334,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_wait_for_elk_to_sync(
elk: elkm1.Elk,
elk: Elk,
login_timeout: int,
sync_timeout: int,
) -> bool:
@ -338,7 +343,9 @@ async def async_wait_for_elk_to_sync(
sync_event = asyncio.Event()
login_event = asyncio.Event()
def login_status(succeeded):
success = True
def login_status(succeeded: bool) -> None:
nonlocal success
success = succeeded
@ -351,10 +358,9 @@ async def async_wait_for_elk_to_sync(
login_event.set()
sync_event.set()
def sync_complete():
def sync_complete() -> None:
sync_event.set()
success = True
elk.add_handler("login", login_status)
elk.add_handler("sync_complete", sync_complete)
for name, event, timeout in (
@ -374,8 +380,8 @@ async def async_wait_for_elk_to_sync(
return success
def _create_elk_services(hass):
def _getelk(service):
def _create_elk_services(hass: HomeAssistant) -> None:
def _getelk(service: ServiceCall) -> Elk:
prefix = service.data["prefix"]
elk = _find_elk_by_prefix(hass, prefix)
if elk is None:
@ -402,12 +408,18 @@ def _create_elk_services(hass):
)
def create_elk_entities(elk_data, elk_elements, element_type, class_, entities):
def create_elk_entities(
elk_data: dict[str, Any],
elk_elements: list[Element],
element_type: str,
class_: Any,
entities: list[ElkEntity],
) -> list[ElkEntity] | None:
"""Create the ElkM1 devices of a particular class."""
auto_configure = elk_data["auto_configure"]
if not auto_configure and not elk_data["config"][element_type]["enabled"]:
return
return None
elk = elk_data["elk"]
_LOGGER.debug("Creating elk entities for %s", elk)
@ -427,7 +439,7 @@ def create_elk_entities(elk_data, elk_elements, element_type, class_, entities):
class ElkEntity(Entity):
"""Base class for all Elk entities."""
def __init__(self, element, elk, elk_data):
def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None:
"""Initialize the base of all Elk devices."""
self._elk = elk
self._element = element
@ -450,12 +462,12 @@ class ElkEntity(Entity):
self._unique_id = f"{uid_start}_{self._element.default_name('_')}".lower()
@property
def name(self):
def name(self) -> str:
"""Name of the element."""
return f"{self._name_prefix}{self._element.name}"
@property
def unique_id(self):
def unique_id(self) -> str:
"""Return unique id of the element."""
return self._unique_id
@ -465,31 +477,31 @@ class ElkEntity(Entity):
return False
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the default attributes of the element."""
return {**self._element.as_dict(), **self.initial_attrs()}
@property
def available(self):
def available(self) -> bool:
"""Is the entity available to be updated."""
return self._elk.is_connected()
def initial_attrs(self):
def initial_attrs(self) -> dict[str, int]:
"""Return the underlying element's attributes as a dict."""
attrs = {}
attrs["index"] = self._element.index + 1
return attrs
def _element_changed(self, element, changeset):
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
pass
@callback
def _element_callback(self, element, changeset):
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
"""Handle callback from an Elk element that has changed."""
self._element_changed(element, changeset)
self.async_write_ha_state()
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register callback for ElkM1 changes and update entity state."""
self._element.add_callback(self._element_callback)
self._element_callback(self._element, {})

View File

@ -26,7 +26,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import ElkAttachedEntity, create_elk_entities
from . import ElkAttachedEntity, ElkEntity, create_elk_entities
from .const import (
ATTR_CHANGED_BY_ID,
ATTR_CHANGED_BY_KEYPAD,
@ -61,7 +61,7 @@ async def async_setup_entry(
"""Set up the ElkM1 alarm platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
elk = elk_data["elk"]
entities: list[ElkArea] = []
entities: list[ElkEntity] = []
create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities)
async_add_entities(entities, True)

View File

@ -37,7 +37,7 @@ async def async_setup_entry(
) -> None:
"""Create the Elk-M1 thermostat platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ElkThermostat] = []
entities: list[ElkEntity] = []
elk = elk_data["elk"]
create_elk_entities(
elk_data, elk.thermostats, "thermostat", ElkThermostat, entities

View File

@ -21,7 +21,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Elk light platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ElkLight] = []
entities: list[ElkEntity] = []
elk = elk_data["elk"]
create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities)
async_add_entities(entities, True)

View File

@ -2,7 +2,7 @@
"domain": "elkm1",
"name": "Elk-M1 Control",
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"requirements": ["elkm1-lib==1.3.0"],
"requirements": ["elkm1-lib==1.3.3"],
"dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }],
"codeowners": ["@gwww", "@bdraco"],
"dependencies": ["network"],

View File

@ -3,12 +3,14 @@ from __future__ import annotations
from typing import Any
from elkm1_lib.tasks import Task
from homeassistant.components.scene import Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ElkAttachedEntity, create_elk_entities
from . import ElkAttachedEntity, ElkEntity, create_elk_entities
from .const import DOMAIN
@ -19,7 +21,7 @@ async def async_setup_entry(
) -> None:
"""Create the Elk-M1 scene platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ElkTask] = []
entities: list[ElkEntity] = []
elk = elk_data["elk"]
create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities)
async_add_entities(entities, True)
@ -28,6 +30,8 @@ async def async_setup_entry(
class ElkTask(ElkAttachedEntity, Scene):
"""Elk-M1 task as scene."""
_element: Task
async def async_activate(self, **kwargs: Any) -> None:
"""Activate the task."""
self._element.activate()

View File

@ -19,7 +19,7 @@ from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ElkAttachedEntity, create_elk_entities
from . import ElkAttachedEntity, ElkEntity, create_elk_entities
from .const import ATTR_VALUE, DOMAIN, ELK_USER_CODE_SERVICE_SCHEMA
SERVICE_SENSOR_COUNTER_REFRESH = "sensor_counter_refresh"
@ -40,7 +40,7 @@ async def async_setup_entry(
) -> None:
"""Create the Elk-M1 sensor platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ElkSensor] = []
entities: list[ElkEntity] = []
elk = elk_data["elk"]
create_elk_entities(elk_data, elk.counters, "counter", ElkCounter, entities)
create_elk_entities(elk_data, elk.keypads, "keypad", ElkKeypad, entities)

View File

@ -1,12 +1,14 @@
"""Support for control of ElkM1 outputs (relays)."""
from __future__ import annotations
from elkm1_lib.outputs import Output
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ElkAttachedEntity, create_elk_entities
from . import ElkAttachedEntity, ElkEntity, create_elk_entities
from .const import DOMAIN
@ -17,7 +19,7 @@ async def async_setup_entry(
) -> None:
"""Create the Elk-M1 switch platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
entities: list[ElkOutput] = []
entities: list[ElkEntity] = []
elk = elk_data["elk"]
create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities)
async_add_entities(entities, True)
@ -26,6 +28,8 @@ async def async_setup_entry(
class ElkOutput(ElkAttachedEntity, SwitchEntity):
"""Elk output as switch."""
_element: Output
@property
def is_on(self) -> bool:
"""Get the current output status."""

View File

@ -693,6 +693,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.elkm1.__init__]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.esphome.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -574,7 +574,7 @@ elgato==3.0.0
eliqonline==1.2.2
# homeassistant.components.elkm1
elkm1-lib==1.3.0
elkm1-lib==1.3.3
# homeassistant.components.elmax
elmax_api==0.0.2

View File

@ -408,7 +408,7 @@ dynalite_devices==0.1.46
elgato==3.0.0
# homeassistant.components.elkm1
elkm1-lib==1.3.0
elkm1-lib==1.3.3
# homeassistant.components.elmax
elmax_api==0.0.2