ESPHome enable static type checking (#52348)
This commit is contained in:
parent
9b2107b71f
commit
4d16cda957
16 changed files with 364 additions and 304 deletions
|
@ -31,6 +31,7 @@ homeassistant.components.dnsip.*
|
|||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.dunehd.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.esphome.*
|
||||
homeassistant.components.fastdotcom.*
|
||||
homeassistant.components.fitbit.*
|
||||
homeassistant.components.forecast_solar.*
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from dataclasses import dataclass, field
|
||||
import functools
|
||||
import logging
|
||||
import math
|
||||
from typing import Generic, TypeVar
|
||||
from typing import Any, Callable, Generic, TypeVar, cast, overload
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIConnectionError,
|
||||
APIIntEnum,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EntityInfo,
|
||||
|
@ -32,13 +34,14 @@ from homeassistant.const import (
|
|||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.service import async_set_service_schema
|
||||
|
@ -97,7 +100,7 @@ class DomainData:
|
|||
"""Get the global DomainData instance stored in hass.data."""
|
||||
# Don't use setdefault - this is a hot code path
|
||||
if DOMAIN in hass.data:
|
||||
return hass.data[DOMAIN]
|
||||
return cast(_T, hass.data[DOMAIN])
|
||||
ret = hass.data[DOMAIN] = cls()
|
||||
return ret
|
||||
|
||||
|
@ -153,7 +156,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
if service.data_template:
|
||||
try:
|
||||
data_template = {
|
||||
key: Template(value) for key, value in service.data_template.items()
|
||||
key: Template(value) # type: ignore[no-untyped-call]
|
||||
for key, value in service.data_template.items()
|
||||
}
|
||||
template.attach(hass, data_template)
|
||||
service_data.update(
|
||||
|
@ -197,10 +201,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
send_state = state.state
|
||||
if attribute:
|
||||
send_state = state.attributes[attribute]
|
||||
attr_val = state.attributes[attribute]
|
||||
# ESPHome only handles "on"/"off" for boolean values
|
||||
if isinstance(send_state, bool):
|
||||
send_state = "on" if send_state else "off"
|
||||
if isinstance(attr_val, bool):
|
||||
send_state = "on" if attr_val else "off"
|
||||
else:
|
||||
send_state = attr_val
|
||||
|
||||
await cli.send_home_assistant_state(entity_id, attribute, str(send_state))
|
||||
|
||||
|
@ -253,6 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
nonlocal device_id
|
||||
try:
|
||||
entry_data.device_info = await cli.device_info()
|
||||
assert cli.api_version is not None
|
||||
entry_data.api_version = cli.api_version
|
||||
entry_data.available = True
|
||||
device_id = await _async_setup_device_registry(
|
||||
|
@ -304,9 +311,9 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
cli: APIClient,
|
||||
entry: ConfigEntry,
|
||||
host: str,
|
||||
on_login,
|
||||
on_login: Callable[[], Awaitable[None]],
|
||||
zc: Zeroconf,
|
||||
):
|
||||
) -> None:
|
||||
"""Initialize ReconnectingLogic."""
|
||||
self._hass = hass
|
||||
self._cli = cli
|
||||
|
@ -322,12 +329,12 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
# Event the different strategies use for issuing a reconnect attempt.
|
||||
self._reconnect_event = asyncio.Event()
|
||||
# The task containing the infinite reconnect loop while running
|
||||
self._loop_task: asyncio.Task | None = None
|
||||
self._loop_task: asyncio.Task[None] | None = None
|
||||
# How many reconnect attempts have there been already, used for exponential wait time
|
||||
self._tries = 0
|
||||
self._tries_lock = asyncio.Lock()
|
||||
# Track the wait task to cancel it on HA shutdown
|
||||
self._wait_task: asyncio.Task | None = None
|
||||
self._wait_task: asyncio.Task[None] | None = None
|
||||
self._wait_task_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
|
@ -338,7 +345,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
except KeyError:
|
||||
return None
|
||||
|
||||
async def _on_disconnect(self):
|
||||
async def _on_disconnect(self) -> None:
|
||||
"""Log and issue callbacks when disconnecting."""
|
||||
if self._entry_data is None:
|
||||
return
|
||||
|
@ -364,7 +371,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
self._connected = False
|
||||
self._reconnect_event.set()
|
||||
|
||||
async def _wait_and_start_reconnect(self):
|
||||
async def _wait_and_start_reconnect(self) -> None:
|
||||
"""Wait for exponentially increasing time to issue next reconnect event."""
|
||||
async with self._tries_lock:
|
||||
tries = self._tries
|
||||
|
@ -383,7 +390,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
self._wait_task = None
|
||||
self._reconnect_event.set()
|
||||
|
||||
async def _try_connect(self):
|
||||
async def _try_connect(self) -> None:
|
||||
"""Try connecting to the API client."""
|
||||
async with self._tries_lock:
|
||||
tries = self._tries
|
||||
|
@ -421,7 +428,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
await self._stop_zc_listen()
|
||||
self._hass.async_create_task(self._on_login())
|
||||
|
||||
async def _reconnect_once(self):
|
||||
async def _reconnect_once(self) -> None:
|
||||
# Wait and clear reconnection event
|
||||
await self._reconnect_event.wait()
|
||||
self._reconnect_event.clear()
|
||||
|
@ -429,7 +436,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
# If in connected state, do not try to connect again.
|
||||
async with self._connected_lock:
|
||||
if self._connected:
|
||||
return False
|
||||
return
|
||||
|
||||
# Check if the entry got removed or disabled, in which case we shouldn't reconnect
|
||||
if not DomainData.get(self._hass).is_entry_loaded(self._entry):
|
||||
|
@ -448,7 +455,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
|
||||
await self._try_connect()
|
||||
|
||||
async def _reconnect_loop(self):
|
||||
async def _reconnect_loop(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
await self._reconnect_once()
|
||||
|
@ -457,7 +464,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.error("Caught exception while reconnecting", exc_info=True)
|
||||
|
||||
async def start(self):
|
||||
async def start(self) -> None:
|
||||
"""Start the reconnecting logic background task."""
|
||||
# Create reconnection loop outside of HA's tracked tasks in order
|
||||
# not to delay startup.
|
||||
|
@ -467,7 +474,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
self._connected = False
|
||||
self._reconnect_event.set()
|
||||
|
||||
async def stop(self):
|
||||
async def stop(self) -> None:
|
||||
"""Stop the reconnecting logic background task. Does not disconnect the client."""
|
||||
if self._loop_task is not None:
|
||||
self._loop_task.cancel()
|
||||
|
@ -478,7 +485,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
self._wait_task = None
|
||||
await self._stop_zc_listen()
|
||||
|
||||
async def _start_zc_listen(self):
|
||||
async def _start_zc_listen(self) -> None:
|
||||
"""Listen for mDNS records.
|
||||
|
||||
This listener allows us to schedule a reconnect as soon as a
|
||||
|
@ -491,7 +498,7 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
)
|
||||
self._zc_listening = True
|
||||
|
||||
async def _stop_zc_listen(self):
|
||||
async def _stop_zc_listen(self) -> None:
|
||||
"""Stop listening for zeroconf updates."""
|
||||
async with self._zc_lock:
|
||||
if self._zc_listening:
|
||||
|
@ -499,12 +506,12 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
self._zc_listening = False
|
||||
|
||||
@callback
|
||||
def stop_callback(self):
|
||||
def stop_callback(self) -> None:
|
||||
"""Stop as an async callback function."""
|
||||
self._hass.async_create_task(self.stop())
|
||||
|
||||
@callback
|
||||
def _set_reconnect(self):
|
||||
def _set_reconnect(self) -> None:
|
||||
self._reconnect_event.set()
|
||||
|
||||
def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None:
|
||||
|
@ -535,13 +542,13 @@ class ReconnectLogic(RecordUpdateListener):
|
|||
|
||||
async def _async_setup_device_registry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo
|
||||
):
|
||||
) -> str:
|
||||
"""Set up device registry feature for a particular config entry."""
|
||||
sw_version = device_info.esphome_version
|
||||
if device_info.compilation_time:
|
||||
sw_version += f" ({device_info.compilation_time})"
|
||||
device_registry = await dr.async_get_registry(hass)
|
||||
entry = device_registry.async_get_or_create(
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
|
||||
name=device_info.name,
|
||||
|
@ -549,18 +556,10 @@ async def _async_setup_device_registry(
|
|||
model=device_info.model,
|
||||
sw_version=sw_version,
|
||||
)
|
||||
return entry.id
|
||||
return device_entry.id
|
||||
|
||||
|
||||
async def _register_service(
|
||||
hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService
|
||||
):
|
||||
service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}"
|
||||
schema = {}
|
||||
fields = {}
|
||||
|
||||
for arg in service.args:
|
||||
metadata = {
|
||||
ARG_TYPE_METADATA = {
|
||||
UserServiceArgType.BOOL: {
|
||||
"validator": cv.boolean,
|
||||
"example": "False",
|
||||
|
@ -605,7 +604,28 @@ async def _register_service(
|
|||
"example": "['Example text', 'Another example']",
|
||||
"selector": {"object": {}},
|
||||
},
|
||||
}[arg.type]
|
||||
}
|
||||
|
||||
|
||||
async def _register_service(
|
||||
hass: HomeAssistant, entry_data: RuntimeEntryData, service: UserService
|
||||
) -> None:
|
||||
if entry_data.device_info is None:
|
||||
raise ValueError("Device Info needs to be fetched first")
|
||||
service_name = f"{entry_data.device_info.name.replace('-', '_')}_{service.name}"
|
||||
schema = {}
|
||||
fields = {}
|
||||
|
||||
for arg in service.args:
|
||||
if arg.type not in ARG_TYPE_METADATA:
|
||||
_LOGGER.error(
|
||||
"Can't register service %s because %s is of unknown type %s",
|
||||
service_name,
|
||||
arg.name,
|
||||
arg.type,
|
||||
)
|
||||
return
|
||||
metadata = ARG_TYPE_METADATA[arg.type]
|
||||
schema[vol.Required(arg.name)] = metadata["validator"]
|
||||
fields[arg.name] = {
|
||||
"name": arg.name,
|
||||
|
@ -615,8 +635,8 @@ async def _register_service(
|
|||
"selector": metadata["selector"],
|
||||
}
|
||||
|
||||
async def execute_service(call):
|
||||
await entry_data.client.execute_service(service, call.data)
|
||||
async def execute_service(call: ServiceCall) -> None:
|
||||
await entry_data.client.execute_service(service, call.data) # type: ignore[arg-type]
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, execute_service, vol.Schema(schema)
|
||||
|
@ -632,7 +652,7 @@ async def _register_service(
|
|||
|
||||
async def _setup_services(
|
||||
hass: HomeAssistant, entry_data: RuntimeEntryData, services: list[UserService]
|
||||
):
|
||||
) -> None:
|
||||
old_services = entry_data.services.copy()
|
||||
to_unregister = []
|
||||
to_register = []
|
||||
|
@ -653,6 +673,7 @@ async def _setup_services(
|
|||
|
||||
entry_data.services = {serv.key: serv for serv in services}
|
||||
|
||||
assert entry_data.device_info is not None
|
||||
for service in to_unregister:
|
||||
service_name = f"{entry_data.device_info.name}_{service.name}"
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
@ -688,15 +709,20 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||
await DomainData.get(hass).get_or_create_store(hass, entry).async_remove()
|
||||
|
||||
|
||||
_InfoT = TypeVar("_InfoT", bound=EntityInfo)
|
||||
_EntityT = TypeVar("_EntityT", bound="EsphomeBaseEntity[Any,Any]")
|
||||
_StateT = TypeVar("_StateT", bound=EntityState)
|
||||
|
||||
|
||||
async def platform_async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
*,
|
||||
component_key: str,
|
||||
info_type,
|
||||
entity_type,
|
||||
state_type,
|
||||
info_type: type[_InfoT],
|
||||
entity_type: type[_EntityT],
|
||||
state_type: type[_StateT],
|
||||
) -> None:
|
||||
"""Set up an esphome platform.
|
||||
|
||||
|
@ -709,15 +735,17 @@ async def platform_async_setup_entry(
|
|||
entry_data.state[component_key] = {}
|
||||
|
||||
@callback
|
||||
def async_list_entities(infos: list[EntityInfo]):
|
||||
def async_list_entities(infos: list[EntityInfo]) -> None:
|
||||
"""Update entities of this platform when entities are listed."""
|
||||
old_infos = entry_data.info[component_key]
|
||||
new_infos = {}
|
||||
new_infos: dict[int, EntityInfo] = {}
|
||||
add_entities = []
|
||||
for info in infos:
|
||||
if not isinstance(info, info_type):
|
||||
# Filter out infos that don't belong to this platform.
|
||||
continue
|
||||
# cast back to upper type, otherwise mypy gets confused
|
||||
info = cast(EntityInfo, info)
|
||||
|
||||
if info.key in old_infos:
|
||||
# Update existing entity
|
||||
|
@ -746,10 +774,13 @@ async def platform_async_setup_entry(
|
|||
)
|
||||
|
||||
@callback
|
||||
def async_entity_state(state: EntityState):
|
||||
def async_entity_state(state: EntityState) -> None:
|
||||
"""Notify the appropriate entity of an updated state."""
|
||||
if not isinstance(state, state_type):
|
||||
return
|
||||
# cast back to upper type, otherwise mypy gets confused
|
||||
state = cast(EntityState, state)
|
||||
|
||||
entry_data.state[component_key][state.key] = state
|
||||
entry_data.async_update_entity(hass, component_key, state.key)
|
||||
|
||||
|
@ -759,16 +790,20 @@ async def platform_async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
def esphome_state_property(func):
|
||||
_PropT = TypeVar("_PropT", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def esphome_state_property(func: _PropT) -> _PropT:
|
||||
"""Wrap a state property of an esphome entity.
|
||||
|
||||
This checks if the state object in the entity is set, and
|
||||
prevents writing NAN values to the Home Assistant state machine.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _wrapper(self):
|
||||
if self._state is None:
|
||||
@property # type: ignore[misc]
|
||||
@functools.wraps(func)
|
||||
def _wrapper(self): # type: ignore[no-untyped-def]
|
||||
if not self._has_state:
|
||||
return None
|
||||
val = func(self)
|
||||
if isinstance(val, float) and math.isnan(val):
|
||||
|
@ -777,29 +812,43 @@ def esphome_state_property(func):
|
|||
return None
|
||||
return val
|
||||
|
||||
return _wrapper
|
||||
return cast(_PropT, _wrapper)
|
||||
|
||||
|
||||
class EsphomeEnumMapper(Generic[_T]):
|
||||
_EnumT = TypeVar("_EnumT", bound=APIIntEnum)
|
||||
_ValT = TypeVar("_ValT")
|
||||
|
||||
|
||||
class EsphomeEnumMapper(Generic[_EnumT, _ValT]):
|
||||
"""Helper class to convert between hass and esphome enum values."""
|
||||
|
||||
def __init__(self, mapping: dict[_T, str]) -> None:
|
||||
def __init__(self, mapping: dict[_EnumT, _ValT]) -> None:
|
||||
"""Construct a EsphomeEnumMapper."""
|
||||
# Add none mapping
|
||||
mapping = {None: None, **mapping}
|
||||
self._mapping = mapping
|
||||
self._inverse: dict[str, _T] = {v: k for k, v in mapping.items()}
|
||||
augmented_mapping: dict[_EnumT | None, _ValT | None] = mapping # type: ignore[assignment]
|
||||
augmented_mapping[None] = None
|
||||
|
||||
def from_esphome(self, value: _T | None) -> str | None:
|
||||
self._mapping = augmented_mapping
|
||||
self._inverse: dict[_ValT, _EnumT] = {v: k for k, v in mapping.items()}
|
||||
|
||||
@overload
|
||||
def from_esphome(self, value: _EnumT) -> _ValT:
|
||||
...
|
||||
|
||||
@overload
|
||||
def from_esphome(self, value: _EnumT | None) -> _ValT | None:
|
||||
...
|
||||
|
||||
def from_esphome(self, value: _EnumT | None) -> _ValT | None:
|
||||
"""Convert from an esphome int representation to a hass string."""
|
||||
return self._mapping[value]
|
||||
|
||||
def from_hass(self, value: str) -> _T:
|
||||
def from_hass(self, value: _ValT) -> _EnumT:
|
||||
"""Convert from a hass string to a esphome int representation."""
|
||||
return self._inverse[value]
|
||||
|
||||
|
||||
class EsphomeBaseEntity(Entity):
|
||||
class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
"""Define a base esphome entity."""
|
||||
|
||||
def __init__(
|
||||
|
@ -850,17 +899,18 @@ class EsphomeBaseEntity(Entity):
|
|||
return self._entry_data.api_version
|
||||
|
||||
@property
|
||||
def _static_info(self) -> EntityInfo:
|
||||
def _static_info(self) -> _InfoT:
|
||||
# Check if value is in info database. Use a single lookup.
|
||||
info = self._entry_data.info[self._component_key].get(self._key)
|
||||
if info is not None:
|
||||
return info
|
||||
return cast(_InfoT, info)
|
||||
# This entity is in the removal project and has been removed from .info
|
||||
# already, look in old_info
|
||||
return self._entry_data.old_info[self._component_key].get(self._key)
|
||||
return cast(_InfoT, self._entry_data.old_info[self._component_key][self._key])
|
||||
|
||||
@property
|
||||
def _device_info(self) -> EsphomeDeviceInfo:
|
||||
assert self._entry_data.device_info is not None
|
||||
return self._entry_data.device_info
|
||||
|
||||
@property
|
||||
|
@ -868,11 +918,12 @@ class EsphomeBaseEntity(Entity):
|
|||
return self._entry_data.client
|
||||
|
||||
@property
|
||||
def _state(self) -> EntityState | None:
|
||||
try:
|
||||
return self._entry_data.state[self._component_key][self._key]
|
||||
except KeyError:
|
||||
return None
|
||||
def _state(self) -> _StateT:
|
||||
return cast(_StateT, self._entry_data.state[self._component_key][self._key])
|
||||
|
||||
@property
|
||||
def _has_state(self) -> bool:
|
||||
return self._key in self._entry_data.state[self._component_key]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@ -911,7 +962,7 @@ class EsphomeBaseEntity(Entity):
|
|||
return False
|
||||
|
||||
|
||||
class EsphomeEntity(EsphomeBaseEntity):
|
||||
class EsphomeEntity(EsphomeBaseEntity[_InfoT, _StateT]):
|
||||
"""Define a generic esphome entity."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
|
|
@ -4,11 +4,16 @@ from __future__ import annotations
|
|||
from aioesphomeapi import BinarySensorInfo, BinarySensorState
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import EsphomeEntity, platform_async_setup_entry
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome binary sensors based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
hass,
|
||||
|
@ -21,17 +26,15 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
)
|
||||
|
||||
|
||||
class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity):
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=no-member
|
||||
|
||||
|
||||
class EsphomeBinarySensor(
|
||||
EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity
|
||||
):
|
||||
"""A binary sensor implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> BinarySensorInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> BinarySensorState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
@ -39,7 +42,7 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorEntity):
|
|||
# Status binary sensors indicated connected state.
|
||||
# So in their case what's usually _availability_ is now state
|
||||
return self._entry_data.available
|
||||
if self._state is None:
|
||||
if not self._has_state:
|
||||
return None
|
||||
if self._state.missing_state:
|
||||
return None
|
||||
|
|
|
@ -2,20 +2,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import CameraInfo, CameraState
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import EsphomeBaseEntity, platform_async_setup_entry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up esphome cameras based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -29,23 +32,19 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeCamera(Camera, EsphomeBaseEntity):
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=no-member
|
||||
|
||||
|
||||
class EsphomeCamera(Camera, EsphomeBaseEntity[CameraInfo, CameraState]):
|
||||
"""A camera implementation for ESPHome."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize."""
|
||||
Camera.__init__(self)
|
||||
EsphomeBaseEntity.__init__(self, *args, **kwargs)
|
||||
self._image_cond = asyncio.Condition()
|
||||
|
||||
@property
|
||||
def _static_info(self) -> CameraInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> CameraState | None:
|
||||
return super()._state
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
|
||||
|
@ -90,7 +89,9 @@ class EsphomeCamera(Camera, EsphomeBaseEntity):
|
|||
return None
|
||||
return self._state.data[:]
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(
|
||||
self, request: web.Request
|
||||
) -> web.StreamResponse:
|
||||
"""Serve an HTTP MJPEG stream from the camera."""
|
||||
return await camera.async_get_still_stream(
|
||||
request, self._async_camera_stream_image, camera.DEFAULT_CONTENT_TYPE, 0.0
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Support for ESPHome climate devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
ClimateAction,
|
||||
ClimateFanMode,
|
||||
|
@ -56,6 +58,7 @@ from homeassistant.components.climate.const import (
|
|||
SWING_OFF,
|
||||
SWING_VERTICAL,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_HALVES,
|
||||
|
@ -63,6 +66,8 @@ from homeassistant.const import (
|
|||
PRECISION_WHOLE,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import (
|
||||
EsphomeEntity,
|
||||
|
@ -72,7 +77,9 @@ from . import (
|
|||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome climate devices based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
hass,
|
||||
|
@ -85,7 +92,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
)
|
||||
|
||||
|
||||
_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper(
|
||||
_CLIMATE_MODES: EsphomeEnumMapper[ClimateMode, str] = EsphomeEnumMapper(
|
||||
{
|
||||
ClimateMode.OFF: HVAC_MODE_OFF,
|
||||
ClimateMode.HEAT_COOL: HVAC_MODE_HEAT_COOL,
|
||||
|
@ -96,7 +103,7 @@ _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode] = EsphomeEnumMapper(
|
|||
ClimateMode.AUTO: HVAC_MODE_AUTO,
|
||||
}
|
||||
)
|
||||
_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper(
|
||||
_CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction, str] = EsphomeEnumMapper(
|
||||
{
|
||||
ClimateAction.OFF: CURRENT_HVAC_OFF,
|
||||
ClimateAction.COOLING: CURRENT_HVAC_COOL,
|
||||
|
@ -106,7 +113,7 @@ _CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction] = EsphomeEnumMapper(
|
|||
ClimateAction.FAN: CURRENT_HVAC_FAN,
|
||||
}
|
||||
)
|
||||
_FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper(
|
||||
_FAN_MODES: EsphomeEnumMapper[ClimateFanMode, str] = EsphomeEnumMapper(
|
||||
{
|
||||
ClimateFanMode.ON: FAN_ON,
|
||||
ClimateFanMode.OFF: FAN_OFF,
|
||||
|
@ -119,7 +126,7 @@ _FAN_MODES: EsphomeEnumMapper[ClimateFanMode] = EsphomeEnumMapper(
|
|||
ClimateFanMode.DIFFUSE: FAN_DIFFUSE,
|
||||
}
|
||||
)
|
||||
_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper(
|
||||
_SWING_MODES: EsphomeEnumMapper[ClimateSwingMode, str] = EsphomeEnumMapper(
|
||||
{
|
||||
ClimateSwingMode.OFF: SWING_OFF,
|
||||
ClimateSwingMode.BOTH: SWING_BOTH,
|
||||
|
@ -127,7 +134,7 @@ _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode] = EsphomeEnumMapper(
|
|||
ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL,
|
||||
}
|
||||
)
|
||||
_PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper(
|
||||
_PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper(
|
||||
{
|
||||
ClimatePreset.NONE: PRESET_NONE,
|
||||
ClimatePreset.HOME: PRESET_HOME,
|
||||
|
@ -141,17 +148,14 @@ _PRESETS: EsphomeEnumMapper[ClimatePreset] = EsphomeEnumMapper(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
|
||||
class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity):
|
||||
"""A climate implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> ClimateInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> ClimateState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the climate device."""
|
||||
|
@ -192,7 +196,7 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
|||
] + self._static_info.supported_custom_presets
|
||||
|
||||
@property
|
||||
def swing_modes(self):
|
||||
def swing_modes(self) -> list[str]:
|
||||
"""Return the list of available swing modes."""
|
||||
return [
|
||||
_SWING_MODES.from_esphome(mode)
|
||||
|
@ -231,11 +235,8 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
|||
features |= SUPPORT_SWING_MODE
|
||||
return features
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
|
||||
@esphome_state_property
|
||||
def hvac_mode(self) -> str | None:
|
||||
def hvac_mode(self) -> str | None: # type: ignore[override]
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
return _CLIMATE_MODES.from_esphome(self._state.mode)
|
||||
|
||||
|
@ -286,11 +287,11 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
|||
"""Return the highbound target temperature we try to reach."""
|
||||
return self._state.target_temperature_high
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
async def async_set_temperature(self, **kwargs: float | str) -> None:
|
||||
"""Set new target temperature (and operation mode if set)."""
|
||||
data = {"key": self._static_info.key}
|
||||
data: dict[str, Any] = {"key": self._static_info.key}
|
||||
if ATTR_HVAC_MODE in kwargs:
|
||||
data["mode"] = _CLIMATE_MODES.from_hass(kwargs[ATTR_HVAC_MODE])
|
||||
data["mode"] = _CLIMATE_MODES.from_hass(cast(str, kwargs[ATTR_HVAC_MODE]))
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
data["target_temperature"] = kwargs[ATTR_TEMPERATURE]
|
||||
if ATTR_TARGET_TEMP_LOW in kwargs:
|
||||
|
@ -307,21 +308,21 @@ class EsphomeClimateEntity(EsphomeEntity, ClimateEntity):
|
|||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
kwargs = {}
|
||||
kwargs: dict[str, Any] = {"key": self._static_info.key}
|
||||
if preset_mode in self._static_info.supported_custom_presets:
|
||||
kwargs["custom_preset"] = preset_mode
|
||||
else:
|
||||
kwargs["preset"] = _PRESETS.from_hass(preset_mode)
|
||||
await self._client.climate_command(key=self._static_info.key, **kwargs)
|
||||
await self._client.climate_command(**kwargs)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new fan mode."""
|
||||
kwargs = {}
|
||||
kwargs: dict[str, Any] = {"key": self._static_info.key}
|
||||
if fan_mode in self._static_info.supported_custom_fan_modes:
|
||||
kwargs["custom_fan_mode"] = fan_mode
|
||||
else:
|
||||
kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode)
|
||||
await self._client.climate_command(key=self._static_info.key, **kwargs)
|
||||
await self._client.climate_command(**kwargs)
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set new swing mode."""
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import APIClient, APIConnectionError
|
||||
from aioesphomeapi import APIClient, APIConnectionError, DeviceInfo
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, DomainData
|
||||
|
@ -20,20 +22,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self._host: str | None = None
|
||||
self._port: int | None = None
|
||||
self._password: str | None = None
|
||||
|
||||
async def async_step_user(
|
||||
async def _async_step_user_base(
|
||||
self, user_input: ConfigType | None = None, error: str | None = None
|
||||
): # pylint: disable=arguments-differ
|
||||
"""Handle a flow initialized by the user."""
|
||||
) -> FlowResult:
|
||||
if user_input is not None:
|
||||
return await self._async_authenticate_or_add(user_input)
|
||||
|
||||
fields = OrderedDict()
|
||||
fields: dict[Any, type] = OrderedDict()
|
||||
fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str
|
||||
fields[vol.Optional(CONF_PORT, default=self._port or 6053)] = int
|
||||
|
||||
|
@ -45,26 +46,33 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
step_id="user", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
return await self._async_step_user_base(user_input=user_input)
|
||||
|
||||
@property
|
||||
def _name(self):
|
||||
def _name(self) -> str | None:
|
||||
return self.context.get(CONF_NAME)
|
||||
|
||||
@_name.setter
|
||||
def _name(self, value):
|
||||
def _name(self, value: str) -> None:
|
||||
self.context[CONF_NAME] = value
|
||||
self.context["title_placeholders"] = {"name": self._name}
|
||||
|
||||
def _set_user_input(self, user_input):
|
||||
def _set_user_input(self, user_input: ConfigType | None) -> None:
|
||||
if user_input is None:
|
||||
return
|
||||
self._host = user_input[CONF_HOST]
|
||||
self._port = user_input[CONF_PORT]
|
||||
|
||||
async def _async_authenticate_or_add(self, user_input):
|
||||
async def _async_authenticate_or_add(
|
||||
self, user_input: ConfigType | None
|
||||
) -> FlowResult:
|
||||
self._set_user_input(user_input)
|
||||
error, device_info = await self.fetch_device_info()
|
||||
if error is not None:
|
||||
return await self.async_step_user(error=error)
|
||||
return await self._async_step_user_base(error=error)
|
||||
assert device_info is not None
|
||||
self._name = device_info.name
|
||||
|
||||
# Only show authentication step if device uses password
|
||||
|
@ -73,7 +81,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
return self._async_get_entry()
|
||||
|
||||
async def async_step_discovery_confirm(self, user_input=None):
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: ConfigType | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
if user_input is not None:
|
||||
return await self._async_authenticate_or_add(None)
|
||||
|
@ -81,7 +91,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
step_id="discovery_confirm", description_placeholders={"name": self._name}
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> FlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
# Hostname is format: livingroom.local.
|
||||
local_name = discovery_info["hostname"][:-1]
|
||||
|
@ -129,7 +141,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
return await self.async_step_discovery_confirm()
|
||||
|
||||
@callback
|
||||
def _async_get_entry(self):
|
||||
def _async_get_entry(self) -> FlowResult:
|
||||
assert self._name is not None
|
||||
return self.async_create_entry(
|
||||
title=self._name,
|
||||
data={
|
||||
|
@ -140,7 +153,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
},
|
||||
)
|
||||
|
||||
async def async_step_authenticate(self, user_input=None, error=None):
|
||||
async def async_step_authenticate(
|
||||
self, user_input: ConfigType | None = None, error: str | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle getting password for authentication."""
|
||||
if user_input is not None:
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
|
@ -160,9 +175,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
errors=errors,
|
||||
)
|
||||
|
||||
async def fetch_device_info(self):
|
||||
async def fetch_device_info(self) -> tuple[str | None, DeviceInfo | None]:
|
||||
"""Fetch device info from API and return any errors."""
|
||||
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
|
||||
assert self._host is not None
|
||||
assert self._port is not None
|
||||
cli = APIClient(
|
||||
self.hass.loop,
|
||||
self._host,
|
||||
|
@ -183,9 +200,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
return None, device_info
|
||||
|
||||
async def try_login(self):
|
||||
async def try_login(self) -> str | None:
|
||||
"""Try logging in to device and return any errors."""
|
||||
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
|
||||
assert self._host is not None
|
||||
assert self._port is not None
|
||||
cli = APIClient(
|
||||
self.hass.loop,
|
||||
self._host,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Support for ESPHome covers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import CoverInfo, CoverOperation, CoverState
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
|
@ -17,12 +19,13 @@ from homeassistant.components.cover import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome covers based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -36,12 +39,13 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeCover(EsphomeEntity, CoverEntity):
|
||||
"""A cover implementation for ESPHome."""
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
@property
|
||||
def _static_info(self) -> CoverInfo:
|
||||
return super()._static_info
|
||||
|
||||
class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity):
|
||||
"""A cover implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
|
@ -63,13 +67,6 @@ class EsphomeCover(EsphomeEntity, CoverEntity):
|
|||
"""Return true if we do optimistic updates."""
|
||||
return self._static_info.assumed_state
|
||||
|
||||
@property
|
||||
def _state(self) -> CoverState | None:
|
||||
return super()._state
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
|
||||
@esphome_state_property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Return if the cover is closed or not."""
|
||||
|
@ -94,39 +91,39 @@ class EsphomeCover(EsphomeEntity, CoverEntity):
|
|||
return round(self._state.position * 100.0)
|
||||
|
||||
@esphome_state_property
|
||||
def current_cover_tilt_position(self) -> float | None:
|
||||
def current_cover_tilt_position(self) -> int | None:
|
||||
"""Return current position of cover tilt. 0 is closed, 100 is open."""
|
||||
if not self._static_info.supports_tilt:
|
||||
return None
|
||||
return self._state.tilt * 100.0
|
||||
return round(self._state.tilt * 100.0)
|
||||
|
||||
async def async_open_cover(self, **kwargs) -> None:
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self._client.cover_command(key=self._static_info.key, position=1.0)
|
||||
|
||||
async def async_close_cover(self, **kwargs) -> None:
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self._client.cover_command(key=self._static_info.key, position=0.0)
|
||||
|
||||
async def async_stop_cover(self, **kwargs) -> None:
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self._client.cover_command(key=self._static_info.key, stop=True)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs) -> None:
|
||||
async def async_set_cover_position(self, **kwargs: int) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
await self._client.cover_command(
|
||||
key=self._static_info.key, position=kwargs[ATTR_POSITION] / 100
|
||||
)
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs) -> None:
|
||||
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover tilt."""
|
||||
await self._client.cover_command(key=self._static_info.key, tilt=1.0)
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs) -> None:
|
||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
await self._client.cover_command(key=self._static_info.key, tilt=0.0)
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs) -> None:
|
||||
async def async_set_cover_tilt_position(self, **kwargs: int) -> None:
|
||||
"""Move the cover tilt to a specific position."""
|
||||
await self._client.cover_command(
|
||||
key=self._static_info.key, tilt=kwargs[ATTR_TILT_POSITION] / 100
|
||||
|
|
|
@ -3,10 +3,11 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from typing import Any, Callable, cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
COMPONENT_TYPE_TO_INFO,
|
||||
APIClient,
|
||||
APIVersion,
|
||||
BinarySensorInfo,
|
||||
CameraInfo,
|
||||
|
@ -29,13 +30,10 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import APIClient
|
||||
|
||||
SAVE_DELAY = 120
|
||||
|
||||
# Mapping from ESPHome info type to HA platform
|
||||
INFO_TYPE_TO_PLATFORM = {
|
||||
INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = {
|
||||
BinarySensorInfo: "binary_sensor",
|
||||
CameraInfo: "camera",
|
||||
ClimateInfo: "climate",
|
||||
|
@ -56,14 +54,14 @@ class RuntimeEntryData:
|
|||
entry_id: str
|
||||
client: APIClient
|
||||
store: Store
|
||||
state: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
info: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
state: dict[str, dict[int, EntityState]] = field(default_factory=dict)
|
||||
info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict)
|
||||
|
||||
# A second list of EntityInfo objects
|
||||
# This is necessary for when an entity is being removed. HA requires
|
||||
# some static info to be accessible during removal (unique_id, maybe others)
|
||||
# If an entity can't find anything in the info array, it will look for info here.
|
||||
old_info: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
old_info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict)
|
||||
|
||||
services: dict[int, UserService] = field(default_factory=dict)
|
||||
available: bool = False
|
||||
|
@ -73,7 +71,7 @@ class RuntimeEntryData:
|
|||
disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list)
|
||||
loaded_platforms: set[str] = field(default_factory=set)
|
||||
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
_storage_contents: dict | None = None
|
||||
_storage_contents: dict[str, Any] | None = None
|
||||
|
||||
@callback
|
||||
def async_update_entity(
|
||||
|
@ -93,7 +91,7 @@ class RuntimeEntryData:
|
|||
|
||||
async def _ensure_platforms_loaded(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str]
|
||||
):
|
||||
) -> None:
|
||||
async with self.platform_load_lock:
|
||||
needed = platforms - self.loaded_platforms
|
||||
tasks = []
|
||||
|
@ -139,6 +137,7 @@ class RuntimeEntryData:
|
|||
restored = await self.store.async_load()
|
||||
if restored is None:
|
||||
return [], []
|
||||
restored = cast("dict[str, Any]", restored)
|
||||
self._storage_contents = restored.copy()
|
||||
|
||||
self.device_info = DeviceInfo.from_dict(restored.pop("device_info"))
|
||||
|
@ -157,7 +156,9 @@ class RuntimeEntryData:
|
|||
|
||||
async def async_save_to_store(self) -> None:
|
||||
"""Generate dynamic data to store and save it to the filesystem."""
|
||||
store_data = {
|
||||
if self.device_info is None:
|
||||
raise ValueError("device_info is not set yet")
|
||||
store_data: dict[str, Any] = {
|
||||
"device_info": self.device_info.to_dict(),
|
||||
"services": [],
|
||||
"api_version": self.api_version.to_dict(),
|
||||
|
@ -171,7 +172,7 @@ class RuntimeEntryData:
|
|||
if store_data == self._storage_contents:
|
||||
return
|
||||
|
||||
def _memorized_storage():
|
||||
def _memorized_storage() -> dict[str, Any]:
|
||||
self._storage_contents = store_data
|
||||
return store_data
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState
|
||||
|
||||
|
@ -15,6 +16,7 @@ from homeassistant.components.fan import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
|
@ -33,7 +35,7 @@ ORDERED_NAMED_FAN_SPEEDS = [FanSpeed.LOW, FanSpeed.MEDIUM, FanSpeed.HIGH]
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome fans based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -47,7 +49,7 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper(
|
||||
_FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection, str] = EsphomeEnumMapper(
|
||||
{
|
||||
FanDirection.FORWARD: DIRECTION_FORWARD,
|
||||
FanDirection.REVERSE: DIRECTION_REVERSE,
|
||||
|
@ -55,29 +57,26 @@ _FAN_DIRECTIONS: EsphomeEnumMapper[FanDirection] = EsphomeEnumMapper(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeFan(EsphomeEntity, FanEntity):
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
|
||||
class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
"""A fan implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> FanInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> FanState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def _supports_speed_levels(self) -> bool:
|
||||
api_version = self._api_version
|
||||
return api_version.major == 1 and api_version.minor > 3
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
async def async_set_percentage(self, percentage: int | None) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
if percentage == 0:
|
||||
await self.async_turn_off()
|
||||
return
|
||||
|
||||
data = {"key": self._static_info.key, "state": True}
|
||||
data: dict[str, Any] = {"key": self._static_info.key, "state": True}
|
||||
if percentage is not None:
|
||||
if self._supports_speed_levels:
|
||||
data["speed_level"] = math.ceil(
|
||||
|
@ -97,12 +96,12 @@ class EsphomeFan(EsphomeEntity, FanEntity):
|
|||
speed: str | None = None,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the fan."""
|
||||
await self.async_set_percentage(percentage)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the fan."""
|
||||
await self._client.fan_command(key=self._static_info.key, state=False)
|
||||
|
||||
|
@ -112,17 +111,14 @@ class EsphomeFan(EsphomeEntity, FanEntity):
|
|||
key=self._static_info.key, oscillating=oscillating
|
||||
)
|
||||
|
||||
async def async_set_direction(self, direction: str):
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set direction of the fan."""
|
||||
await self._client.fan_command(
|
||||
key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction)
|
||||
)
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool | None: # type: ignore[override]
|
||||
"""Return true if the entity is on."""
|
||||
return self._state.state
|
||||
|
||||
|
@ -134,7 +130,7 @@ class EsphomeFan(EsphomeEntity, FanEntity):
|
|||
|
||||
if not self._supports_speed_levels:
|
||||
return ordered_list_item_to_percentage(
|
||||
ORDERED_NAMED_FAN_SPEEDS, self._state.speed
|
||||
ORDERED_NAMED_FAN_SPEEDS, self._state.speed # type: ignore[misc]
|
||||
)
|
||||
|
||||
return ranged_value_to_percentage(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Support for ESPHome lights."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import LightInfo, LightState
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
@ -24,6 +26,7 @@ from homeassistant.components.light import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
|
||||
|
@ -32,7 +35,7 @@ FLASH_LENGTHS = {FLASH_SHORT: 2, FLASH_LONG: 10}
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome lights based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -46,28 +49,22 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeLight(EsphomeEntity, LightEntity):
|
||||
"""A switch implementation for ESPHome."""
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
@property
|
||||
def _static_info(self) -> LightInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> LightState | None:
|
||||
return super()._state
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
"""A light implementation for ESPHome."""
|
||||
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the switch is on."""
|
||||
def is_on(self) -> bool | None: # type: ignore[override]
|
||||
"""Return true if the light is on."""
|
||||
return self._state.state
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
data = {"key": self._static_info.key, "state": True}
|
||||
data: dict[str, Any] = {"key": self._static_info.key, "state": True}
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
hue, sat = kwargs[ATTR_HS_COLOR]
|
||||
red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100)
|
||||
|
@ -86,9 +83,9 @@ class EsphomeLight(EsphomeEntity, LightEntity):
|
|||
data["white"] = kwargs[ATTR_WHITE_VALUE] / 255
|
||||
await self._client.light_command(**data)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
data = {"key": self._static_info.key, "state": False}
|
||||
data: dict[str, Any] = {"key": self._static_info.key, "state": False}
|
||||
if ATTR_FLASH in kwargs:
|
||||
data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]]
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
|
@ -108,7 +105,7 @@ class EsphomeLight(EsphomeEntity, LightEntity):
|
|||
)
|
||||
|
||||
@esphome_state_property
|
||||
def color_temp(self) -> float | None:
|
||||
def color_temp(self) -> float | None: # type: ignore[override]
|
||||
"""Return the CT color value in mireds."""
|
||||
return self._state.color_temperature
|
||||
|
||||
|
@ -145,11 +142,11 @@ class EsphomeLight(EsphomeEntity, LightEntity):
|
|||
return self._static_info.effects
|
||||
|
||||
@property
|
||||
def min_mireds(self) -> float:
|
||||
def min_mireds(self) -> float: # type: ignore[override]
|
||||
"""Return the coldest color_temp that this light supports."""
|
||||
return self._static_info.min_mireds
|
||||
|
||||
@property
|
||||
def max_mireds(self) -> float:
|
||||
def max_mireds(self) -> float: # type: ignore[override]
|
||||
"""Return the warmest color_temp that this light supports."""
|
||||
return self._static_info.max_mireds
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
from aioesphomeapi import NumberInfo, NumberState
|
||||
import voluptuous as vol
|
||||
|
@ -35,26 +36,19 @@ async def async_setup_entry(
|
|||
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
|
||||
class EsphomeNumber(EsphomeEntity, NumberEntity):
|
||||
class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
|
||||
"""A number implementation for esphome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> NumberInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> NumberState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
if not self._static_info.icon:
|
||||
return None
|
||||
return ICON_SCHEMA(self._static_info.icon)
|
||||
return cast(str, ICON_SCHEMA(self._static_info.icon))
|
||||
|
||||
@property
|
||||
def min_value(self) -> float:
|
||||
|
@ -72,7 +66,7 @@ class EsphomeNumber(EsphomeEntity, NumberEntity):
|
|||
return super()._static_info.step
|
||||
|
||||
@esphome_state_property
|
||||
def value(self) -> float:
|
||||
def value(self) -> float | None:
|
||||
"""Return the state of the entity."""
|
||||
if math.isnan(self._state.state):
|
||||
return None
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
SensorInfo,
|
||||
|
@ -21,6 +22,7 @@ from homeassistant.components.sensor import (
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt
|
||||
|
||||
from . import (
|
||||
|
@ -34,7 +36,7 @@ ICON_SCHEMA = vol.Schema(cv.icon)
|
|||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up esphome sensors based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -58,10 +60,11 @@ async def async_setup_entry(
|
|||
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
|
||||
_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper(
|
||||
_STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMapper(
|
||||
{
|
||||
SensorStateClass.NONE: None,
|
||||
SensorStateClass.MEASUREMENT: STATE_CLASS_MEASUREMENT,
|
||||
|
@ -69,23 +72,15 @@ _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass] = EsphomeEnumMapper(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeSensor(EsphomeEntity, SensorEntity):
|
||||
class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
"""A sensor implementation for esphome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> SensorInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> SensorState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
if not self._static_info.icon or self._static_info.device_class:
|
||||
return None
|
||||
return ICON_SCHEMA(self._static_info.icon)
|
||||
return cast(str, ICON_SCHEMA(self._static_info.icon))
|
||||
|
||||
@property
|
||||
def force_update(self) -> bool:
|
||||
|
@ -104,14 +99,14 @@ class EsphomeSensor(EsphomeEntity, SensorEntity):
|
|||
return f"{self._state.state:.{self._static_info.accuracy_decimals}f}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
def unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
if not self._static_info.unit_of_measurement:
|
||||
return None
|
||||
return self._static_info.unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
if self._static_info.device_class not in DEVICE_CLASSES:
|
||||
return None
|
||||
|
@ -125,17 +120,9 @@ class EsphomeSensor(EsphomeEntity, SensorEntity):
|
|||
return _STATE_CLASSES.from_esphome(self._static_info.state_class)
|
||||
|
||||
|
||||
class EsphomeTextSensor(EsphomeEntity, SensorEntity):
|
||||
class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
|
||||
"""A text sensor implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> TextSensorInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> TextSensorState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
"""Support for ESPHome switches."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import SwitchInfo, SwitchState
|
||||
|
||||
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 EsphomeEntity, esphome_state_property, platform_async_setup_entry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up ESPHome switches based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
|
@ -25,17 +28,14 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
class EsphomeSwitch(EsphomeEntity, SwitchEntity):
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property
|
||||
# Pylint gets confused with the EsphomeEntity generics -> let mypy handle member checking
|
||||
# pylint: disable=invalid-overridden-method,no-member
|
||||
|
||||
|
||||
class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
|
||||
"""A switch implementation for ESPHome."""
|
||||
|
||||
@property
|
||||
def _static_info(self) -> SwitchInfo:
|
||||
return super()._static_info
|
||||
|
||||
@property
|
||||
def _state(self) -> SwitchState | None:
|
||||
return super()._state
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the icon."""
|
||||
|
@ -46,17 +46,15 @@ class EsphomeSwitch(EsphomeEntity, SwitchEntity):
|
|||
"""Return true if we do optimistic updates."""
|
||||
return self._static_info.assumed_state
|
||||
|
||||
# https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property
|
||||
# pylint: disable=invalid-overridden-method
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool | None: # type: ignore[override]
|
||||
"""Return true if the switch is on."""
|
||||
return self._state.state
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._client.switch_command(self._static_info.key, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self._client.switch_command(self._static_info.key, False)
|
||||
|
|
14
mypy.ini
14
mypy.ini
|
@ -352,6 +352,17 @@ no_implicit_optional = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.esphome.*]
|
||||
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.fastdotcom.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -1153,9 +1164,6 @@ ignore_errors = true
|
|||
[mypy-homeassistant.components.entur_public_transport.*]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.esphome.*]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.evohome.*]
|
||||
ignore_errors = true
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ IGNORED_MODULES: Final[list[str]] = [
|
|||
"homeassistant.components.emonitor.*",
|
||||
"homeassistant.components.enphase_envoy.*",
|
||||
"homeassistant.components.entur_public_transport.*",
|
||||
"homeassistant.components.esphome.*",
|
||||
"homeassistant.components.evohome.*",
|
||||
"homeassistant.components.filter.*",
|
||||
"homeassistant.components.fints.*",
|
||||
|
|
|
@ -48,6 +48,13 @@ def mock_api_connection_error():
|
|||
yield mock_error
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_setup_entry():
|
||||
"""Mock setting up a config entry."""
|
||||
with patch("homeassistant.components.esphome.async_setup_entry", return_value=True):
|
||||
yield
|
||||
|
||||
|
||||
async def test_user_connection_works(hass, mock_client):
|
||||
"""Test we can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue