Add type annotations for Netatmo (#52811)

This commit is contained in:
Tobias Sauerwein 2021-07-21 23:36:57 +02:00 committed by GitHub
parent 84c482441d
commit 583deada83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 288 additions and 177 deletions

View file

@ -60,6 +60,7 @@ homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.mysensors.* homeassistant.components.mysensors.*
homeassistant.components.nam.* homeassistant.components.nam.*
homeassistant.components.netatmo.*
homeassistant.components.network.* homeassistant.components.network.*
homeassistant.components.no_ip.* homeassistant.components.no_ip.*
homeassistant.components.notify.* homeassistant.components.notify.*

View file

@ -1,4 +1,6 @@
"""The Netatmo integration.""" """The Netatmo integration."""
from __future__ import annotations
import logging import logging
import secrets import secrets
@ -67,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema(
) )
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the Netatmo component.""" """Set up the Netatmo component."""
hass.data[DOMAIN] = { hass.data[DOMAIN] = {
DATA_PERSONS: {}, DATA_PERSONS: {},
@ -121,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
async def unregister_webhook(_): async def unregister_webhook(_: None) -> None:
if CONF_WEBHOOK_ID not in entry.data: if CONF_WEBHOOK_ID not in entry.data:
return return
_LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID]) _LOGGER.debug("Unregister Netatmo webhook (%s)", entry.data[CONF_WEBHOOK_ID])
@ -138,7 +140,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID] "No webhook to be dropped for %s", entry.data[CONF_WEBHOOK_ID]
) )
async def register_webhook(event): async def register_webhook(_: None) -> None:
if CONF_WEBHOOK_ID not in entry.data: if CONF_WEBHOOK_ID not in entry.data:
data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()} data = {**entry.data, CONF_WEBHOOK_ID: secrets.token_hex()}
hass.config_entries.async_update_entry(entry, data=data) hass.config_entries.async_update_entry(entry, data=data)
@ -175,7 +177,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async_handle_webhook, async_handle_webhook,
) )
async def handle_event(event): async def handle_event(event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION:
if activation_listener is not None: if activation_listener is not None:
@ -219,7 +221,7 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) ->
async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}")
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if CONF_WEBHOOK_ID in entry.data: if CONF_WEBHOOK_ID in entry.data:
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
@ -236,7 +238,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok return unload_ok
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Cleanup when entry is removed.""" """Cleanup when entry is removed."""
if ( if (
CONF_WEBHOOK_ID in entry.data CONF_WEBHOOK_ID in entry.data

View file

@ -1,4 +1,6 @@
"""API for Netatmo bound to HASS OAuth.""" """API for Netatmo bound to HASS OAuth."""
from typing import cast
from aiohttp import ClientSession from aiohttp import ClientSession
import pyatmo import pyatmo
@ -17,8 +19,8 @@ class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth):
super().__init__(websession) super().__init__(websession)
self._oauth_session = oauth_session self._oauth_session = oauth_session
async def async_get_access_token(self): async def async_get_access_token(self) -> str:
"""Return a valid access token for Netatmo API.""" """Return a valid access token for Netatmo API."""
if not self._oauth_session.valid_token: if not self._oauth_session.valid_token:
await self._oauth_session.async_ensure_token_valid() await self._oauth_session.async_ensure_token_valid()
return self._oauth_session.token["access_token"] return cast(str, self._oauth_session.token["access_token"])

View file

@ -1,15 +1,20 @@
"""Support for the Netatmo cameras.""" """Support for the Netatmo cameras."""
from __future__ import annotations
import logging import logging
from typing import Any, cast
import aiohttp import aiohttp
import pyatmo import pyatmo
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
ATTR_CAMERA_LIGHT_MODE, ATTR_CAMERA_LIGHT_MODE,
@ -31,11 +36,12 @@ from .const import (
SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSON_AWAY,
SERVICE_SET_PERSONS_HOME, SERVICE_SET_PERSONS_HOME,
SIGNAL_NAME, SIGNAL_NAME,
UNKNOWN,
WEBHOOK_LIGHT_MODE, WEBHOOK_LIGHT_MODE,
WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_NACAMERA_CONNECTION,
WEBHOOK_PUSH_TYPE, WEBHOOK_PUSH_TYPE,
) )
from .data_handler import CAMERA_DATA_CLASS_NAME from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler
from .netatmo_entity_base import NetatmoBase from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,7 +49,9 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_QUALITY = "high" DEFAULT_QUALITY = "high"
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 the Netatmo camera platform.""" """Set up the Netatmo camera platform."""
if "access_camera" not in entry.data["token"]["scope"]: if "access_camera" not in entry.data["token"]["scope"]:
_LOGGER.info( _LOGGER.info(
@ -108,12 +116,12 @@ class NetatmoCamera(NetatmoBase, Camera):
def __init__( def __init__(
self, self,
data_handler, data_handler: NetatmoDataHandler,
camera_id, camera_id: str,
camera_type, camera_type: str,
home_id, home_id: str,
quality, quality: str,
): ) -> None:
"""Set up for access to the Netatmo camera images.""" """Set up for access to the Netatmo camera images."""
Camera.__init__(self) Camera.__init__(self)
super().__init__(data_handler) super().__init__(data_handler)
@ -124,17 +132,19 @@ class NetatmoCamera(NetatmoBase, Camera):
self._id = camera_id self._id = camera_id
self._home_id = home_id self._home_id = home_id
self._device_name = self._data.get_camera(camera_id=camera_id).get("name") self._device_name = self._data.get_camera(camera_id=camera_id).get(
"name", UNKNOWN
)
self._attr_name = f"{MANUFACTURER} {self._device_name}" self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._model = camera_type self._model = camera_type
self._attr_unique_id = f"{self._id}-{self._model}" self._attr_unique_id = f"{self._id}-{self._model}"
self._quality = quality self._quality = quality
self._vpnurl = None self._vpnurl: str | None = None
self._localurl = None self._localurl: str | None = None
self._status = None self._status: str | None = None
self._sd_status = None self._sd_status: str | None = None
self._alim_status = None self._alim_status: str | None = None
self._is_local = None self._is_local: str | None = None
self._light_state = None self._light_state = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -153,7 +163,7 @@ class NetatmoCamera(NetatmoBase, Camera):
self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name
@callback @callback
def handle_event(self, event): def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
data = event["data"] data = event["data"]
@ -179,7 +189,15 @@ class NetatmoCamera(NetatmoBase, Camera):
self.async_write_ha_state() self.async_write_ha_state()
return return
async def async_camera_image(self): @property
def _data(self) -> pyatmo.AsyncCameraData:
"""Return data for this entity."""
return cast(
pyatmo.AsyncCameraData,
self.data_handler.data[self._data_classes[0]["name"]],
)
async def async_camera_image(self) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:
return await self._data.async_get_live_snapshot(camera_id=self._id) return await self._data.async_get_live_snapshot(camera_id=self._id)
@ -194,43 +212,43 @@ class NetatmoCamera(NetatmoBase, Camera):
return None return None
@property @property
def available(self): def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return bool(self._alim_status == "on" or self._status == "disconnected") return bool(self._alim_status == "on" or self._status == "disconnected")
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Return supported features.""" """Return supported features."""
return SUPPORT_STREAM return SUPPORT_STREAM
@property @property
def brand(self): def brand(self) -> str:
"""Return the camera brand.""" """Return the camera brand."""
return MANUFACTURER return MANUFACTURER
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return bool(self._status == "on") return bool(self._status == "on")
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if on.""" """Return true if on."""
return self.is_streaming return self.is_streaming
async def async_turn_off(self): async def async_turn_off(self) -> None:
"""Turn off camera.""" """Turn off camera."""
await self._data.async_set_state( await self._data.async_set_state(
home_id=self._home_id, camera_id=self._id, monitoring="off" home_id=self._home_id, camera_id=self._id, monitoring="off"
) )
async def async_turn_on(self): async def async_turn_on(self) -> None:
"""Turn on camera.""" """Turn on camera."""
await self._data.async_set_state( await self._data.async_set_state(
home_id=self._home_id, camera_id=self._id, monitoring="on" home_id=self._home_id, camera_id=self._id, monitoring="on"
) )
async def stream_source(self): async def stream_source(self) -> str:
"""Return the stream source.""" """Return the stream source."""
url = "{0}/live/files/{1}/index.m3u8" url = "{0}/live/files/{1}/index.m3u8"
if self._localurl: if self._localurl:
@ -238,12 +256,12 @@ class NetatmoCamera(NetatmoBase, Camera):
return url.format(self._vpnurl, self._quality) return url.format(self._vpnurl, self._quality)
@property @property
def model(self): def model(self) -> str:
"""Return the camera model.""" """Return the camera model."""
return MODELS[self._model] return MODELS[self._model]
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
camera = self._data.get_camera(self._id) camera = self._data.get_camera(self._id)
self._vpnurl, self._localurl = self._data.camera_urls(self._id) self._vpnurl, self._localurl = self._data.camera_urls(self._id)
@ -275,7 +293,7 @@ class NetatmoCamera(NetatmoBase, Camera):
} }
) )
def process_events(self, events): def process_events(self, events: dict) -> dict:
"""Add meta data to events.""" """Add meta data to events."""
for event in events.values(): for event in events.values():
if "video_id" not in event: if "video_id" not in event:
@ -290,9 +308,9 @@ class NetatmoCamera(NetatmoBase, Camera):
] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
return events return events
async def _service_set_persons_home(self, **kwargs): async def _service_set_persons_home(self, **kwargs: Any) -> None:
"""Service to change current home schedule.""" """Service to change current home schedule."""
persons = kwargs.get(ATTR_PERSONS) persons = kwargs.get(ATTR_PERSONS, {})
person_ids = [] person_ids = []
for person in persons: for person in persons:
for pid, data in self._data.persons.items(): for pid, data in self._data.persons.items():
@ -304,7 +322,7 @@ class NetatmoCamera(NetatmoBase, Camera):
) )
_LOGGER.debug("Set %s as at home", persons) _LOGGER.debug("Set %s as at home", persons)
async def _service_set_person_away(self, **kwargs): async def _service_set_person_away(self, **kwargs: Any) -> None:
"""Service to mark a person as away or set the home as empty.""" """Service to mark a person as away or set the home as empty."""
person = kwargs.get(ATTR_PERSON) person = kwargs.get(ATTR_PERSON)
person_id = None person_id = None
@ -327,10 +345,10 @@ class NetatmoCamera(NetatmoBase, Camera):
) )
_LOGGER.debug("Set home as empty") _LOGGER.debug("Set home as empty")
async def _service_set_camera_light(self, **kwargs): async def _service_set_camera_light(self, **kwargs: Any) -> None:
"""Service to set light mode.""" """Service to set light mode."""
mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE))
_LOGGER.debug("Turn %s camera light for '%s'", mode, self.name) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name)
await self._data.async_set_state( await self._data.async_set_state(
home_id=self._home_id, home_id=self._home_id,
camera_id=self._id, camera_id=self._id,

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import cast
import pyatmo import pyatmo
import voluptuous as vol import voluptuous as vol
@ -19,6 +20,7 @@ from homeassistant.components.climate.const import (
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
@ -26,11 +28,13 @@ from homeassistant.const import (
STATE_OFF, STATE_OFF,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
ATTR_HEATING_POWER_REQUEST, ATTR_HEATING_POWER_REQUEST,
@ -49,7 +53,11 @@ from .const import (
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
SIGNAL_NAME, SIGNAL_NAME,
) )
from .data_handler import HOMEDATA_DATA_CLASS_NAME, HOMESTATUS_DATA_CLASS_NAME from .data_handler import (
HOMEDATA_DATA_CLASS_NAME,
HOMESTATUS_DATA_CLASS_NAME,
NetatmoDataHandler,
)
from .netatmo_entity_base import NetatmoBase from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -106,8 +114,12 @@ DEFAULT_MAX_TEMP = 30
NA_THERM = "NATherm1" NA_THERM = "NATherm1"
NA_VALVE = "NRV" NA_VALVE = "NRV"
SUGGESTED_AREA = "suggested_area"
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 the Netatmo energy platform.""" """Set up the Netatmo energy platform."""
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
@ -163,7 +175,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
class NetatmoThermostat(NetatmoBase, ClimateEntity): class NetatmoThermostat(NetatmoBase, ClimateEntity):
"""Representation a Netatmo thermostat.""" """Representation a Netatmo thermostat."""
def __init__(self, data_handler, home_id, room_id): def __init__(
self, data_handler: NetatmoDataHandler, home_id: str, room_id: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
ClimateEntity.__init__(self) ClimateEntity.__init__(self)
super().__init__(data_handler) super().__init__(data_handler)
@ -189,29 +203,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._home_status = self.data_handler.data[self._home_status_class] self._home_status = self.data_handler.data[self._home_status_class]
self._room_status = self._home_status.rooms[room_id] self._room_status = self._home_status.rooms[room_id]
self._room_data = self._data.rooms[home_id][room_id] self._room_data: dict = self._data.rooms[home_id][room_id]
self._model = NA_VALVE self._model: str = NA_VALVE
for module in self._room_data.get("module_ids"): for module in self._room_data.get("module_ids", []):
if self._home_status.thermostats.get(module): if self._home_status.thermostats.get(module):
self._model = NA_THERM self._model = NA_THERM
break break
self._device_name = self._data.rooms[home_id][room_id]["name"] self._device_name = self._data.rooms[home_id][room_id]["name"]
self._attr_name = f"{MANUFACTURER} {self._device_name}" self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._current_temperature = None self._current_temperature: float | None = None
self._target_temperature = None self._target_temperature: float | None = None
self._preset = None self._preset: str | None = None
self._away = None self._away: bool | None = None
self._operation_list = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] self._operation_list = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
self._support_flags = SUPPORT_FLAGS self._support_flags = SUPPORT_FLAGS
self._hvac_mode = None self._hvac_mode: str = HVAC_MODE_AUTO
self._battery_level = None self._battery_level = None
self._connected = None self._connected: bool | None = None
self._away_temperature = None self._away_temperature: float | None = None
self._hg_temperature = None self._hg_temperature: float | None = None
self._boilerstatus = None self._boilerstatus: bool | None = None
self._setpoint_duration = None self._setpoint_duration = None
self._selected_schedule = None self._selected_schedule = None
@ -240,9 +254,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
registry = await async_get_registry(self.hass) registry = await async_get_registry(self.hass)
device = registry.async_get_device({(DOMAIN, self._id)}, set()) device = registry.async_get_device({(DOMAIN, self._id)}, set())
assert device
self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id
async def handle_event(self, event): async def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
data = event["data"] data = event["data"]
@ -307,22 +322,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return return
@property @property
def supported_features(self): def _data(self) -> pyatmo.AsyncHomeData:
"""Return data for this entity."""
return cast(
pyatmo.AsyncHomeData, self.data_handler.data[self._data_classes[0]["name"]]
)
@property
def supported_features(self) -> int:
"""Return the list of supported features.""" """Return the list of supported features."""
return self._support_flags return self._support_flags
@property @property
def temperature_unit(self): def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS return TEMP_CELSIUS
@property @property
def current_temperature(self): def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
return self._current_temperature return self._current_temperature
@property @property
def target_temperature(self): def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temperature return self._target_temperature
@ -332,12 +354,12 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return PRECISION_HALVES return PRECISION_HALVES
@property @property
def hvac_mode(self): def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.""" """Return hvac operation ie. heat, cool mode."""
return self._hvac_mode return self._hvac_mode
@property @property
def hvac_modes(self): def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes.""" """Return the list of available hvac operation modes."""
return self._operation_list return self._operation_list
@ -418,7 +440,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
"""Return a list of available preset modes.""" """Return a list of available preset modes."""
return SUPPORT_PRESET return SUPPORT_PRESET
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs: dict) -> None:
"""Set new target temperature for 2 hours.""" """Set new target temperature for 2 hours."""
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None: if temp is None:
@ -429,7 +451,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self): async def async_turn_off(self) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if self._model == NA_VALVE: if self._model == NA_VALVE:
await self._home_status.async_set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
@ -443,7 +465,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
) )
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_on(self): async def async_turn_on(self) -> None:
"""Turn the entity on.""" """Turn the entity on."""
await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME)
self.async_write_ha_state() self.async_write_ha_state()
@ -454,7 +476,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return bool(self._connected) return bool(self._connected)
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
self._home_status = self.data_handler.data[self._home_status_class] self._home_status = self.data_handler.data[self._home_status_class]
if self._home_status is None: if self._home_status is None:
@ -487,8 +509,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
if "current_temperature" not in roomstatus: if "current_temperature" not in roomstatus:
return return
if self._model is None:
self._model = roomstatus["module_type"]
self._current_temperature = roomstatus["current_temperature"] self._current_temperature = roomstatus["current_temperature"]
self._target_temperature = roomstatus["target_temperature"] self._target_temperature = roomstatus["target_temperature"]
self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]]
@ -511,7 +531,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
ATTR_SELECTED_SCHEDULE ATTR_SELECTED_SCHEDULE
] = self._selected_schedule ] = self._selected_schedule
def _build_room_status(self): def _build_room_status(self) -> dict:
"""Construct room status.""" """Construct room status."""
try: try:
roomstatus = { roomstatus = {
@ -570,7 +590,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return {} return {}
async def _async_service_set_schedule(self, **kwargs): async def _async_service_set_schedule(self, **kwargs: dict) -> None:
schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_name = kwargs.get(ATTR_SCHEDULE_NAME)
schedule_id = None schedule_id = None
for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items():
@ -592,12 +612,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
) )
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info for the thermostat.""" """Return the device info for the thermostat."""
return {**super().device_info, "suggested_area": self._room_data["name"]} device_info: DeviceInfo = super().device_info
device_info["suggested_area"] = self._room_data["name"]
return device_info
def get_all_home_ids(home_data: pyatmo.HomeData) -> list[str]: def get_all_home_ids(home_data: pyatmo.HomeData | None) -> list[str]:
"""Get all the home ids returned by NetAtmo API.""" """Get all the home ids returned by NetAtmo API."""
if home_data is None: if home_data is None:
return [] return []

View file

@ -1,4 +1,6 @@
"""Config flow for Netatmo.""" """Config flow for Netatmo."""
from __future__ import annotations
import logging import logging
import uuid import uuid
@ -7,6 +9,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.const import CONF_SHOW_ON_MAP
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from .const import ( from .const import (
@ -32,7 +35,9 @@ class NetatmoFlowHandler(
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return NetatmoOptionsFlowHandler(config_entry) return NetatmoOptionsFlowHandler(config_entry)
@ -62,7 +67,7 @@ class NetatmoFlowHandler(
return {"scope": " ".join(scopes)} return {"scope": " ".join(scopes)}
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
"""Handle a flow start.""" """Handle a flow start."""
await self.async_set_unique_id(DOMAIN) await self.async_set_unique_id(DOMAIN)
@ -81,17 +86,19 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow):
self.options = dict(config_entry.options) self.options = dict(config_entry.options)
self.options.setdefault(CONF_WEATHER_AREAS, {}) self.options.setdefault(CONF_WEATHER_AREAS, {})
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input: dict | None = None) -> FlowResult:
"""Manage the Netatmo options.""" """Manage the Netatmo options."""
return await self.async_step_public_weather_areas() return await self.async_step_public_weather_areas()
async def async_step_public_weather_areas(self, user_input=None): async def async_step_public_weather_areas(
self, user_input: dict | None = None
) -> FlowResult:
"""Manage configuration of Netatmo public weather areas.""" """Manage configuration of Netatmo public weather areas."""
errors = {} errors: dict = {}
if user_input is not None: if user_input is not None:
new_client = user_input.pop(CONF_NEW_AREA, None) new_client = user_input.pop(CONF_NEW_AREA, None)
areas = user_input.pop(CONF_WEATHER_AREAS, None) areas = user_input.pop(CONF_WEATHER_AREAS, [])
user_input[CONF_WEATHER_AREAS] = { user_input[CONF_WEATHER_AREAS] = {
area: self.options[CONF_WEATHER_AREAS][area] for area in areas area: self.options[CONF_WEATHER_AREAS][area] for area in areas
} }
@ -110,7 +117,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow):
vol.Optional( vol.Optional(
CONF_WEATHER_AREAS, CONF_WEATHER_AREAS,
default=weather_areas, default=weather_areas,
): cv.multi_select(weather_areas), ): cv.multi_select({wa: None for wa in weather_areas}),
vol.Optional(CONF_NEW_AREA): str, vol.Optional(CONF_NEW_AREA): str,
} }
) )
@ -120,7 +127,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow):
errors=errors, errors=errors,
) )
async def async_step_public_weather(self, user_input=None): async def async_step_public_weather(self, user_input: dict) -> FlowResult:
"""Manage configuration of Netatmo public weather sensors.""" """Manage configuration of Netatmo public weather sensors."""
if user_input is not None and CONF_NEW_AREA not in user_input: if user_input is not None and CONF_NEW_AREA not in user_input:
self.options[CONF_WEATHER_AREAS][ self.options[CONF_WEATHER_AREAS][
@ -181,14 +188,14 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow):
return self.async_show_form(step_id="public_weather", data_schema=data_schema) return self.async_show_form(step_id="public_weather", data_schema=data_schema)
def _create_options_entry(self): def _create_options_entry(self) -> FlowResult:
"""Update config entry options.""" """Update config entry options."""
return self.async_create_entry( return self.async_create_entry(
title="Netatmo Public Weather", data=self.options title="Netatmo Public Weather", data=self.options
) )
def fix_coordinates(user_input): def fix_coordinates(user_input: dict) -> dict:
"""Fix coordinates if they don't comply with the Netatmo API.""" """Fix coordinates if they don't comply with the Netatmo API."""
# Ensure coordinates have acceptable length for the Netatmo API # Ensure coordinates have acceptable length for the Netatmo API
for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW): for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW):

View file

@ -6,6 +6,7 @@ from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
API = "api" API = "api"
UNKNOWN = "unknown"
DOMAIN = "netatmo" DOMAIN = "netatmo"
MANUFACTURER = "Netatmo" MANUFACTURER = "Netatmo"
@ -76,7 +77,7 @@ DATA_SCHEDULES = "netatmo_schedules"
NETATMO_WEBHOOK_URL = None NETATMO_WEBHOOK_URL = None
NETATMO_EVENT = "netatmo_event" NETATMO_EVENT = "netatmo_event"
DEFAULT_PERSON = "Unknown" DEFAULT_PERSON = UNKNOWN
DEFAULT_DISCOVERY = True DEFAULT_DISCOVERY = True
DEFAULT_WEBHOOKS = False DEFAULT_WEBHOOKS = False

View file

@ -8,6 +8,7 @@ from datetime import timedelta
from itertools import islice from itertools import islice
import logging import logging
from time import time from time import time
from typing import Any
import pyatmo import pyatmo
@ -75,11 +76,11 @@ class NetatmoDataHandler:
self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] self._auth = hass.data[DOMAIN][entry.entry_id][AUTH]
self.listeners: list[CALLBACK_TYPE] = [] self.listeners: list[CALLBACK_TYPE] = []
self.data_classes: dict = {} self.data_classes: dict = {}
self.data = {} self.data: dict = {}
self._queue = deque() self._queue: deque = deque()
self._webhook: bool = False self._webhook: bool = False
async def async_setup(self): async def async_setup(self) -> None:
"""Set up the Netatmo data handler.""" """Set up the Netatmo data handler."""
async_track_time_interval( async_track_time_interval(
@ -94,7 +95,7 @@ class NetatmoDataHandler:
) )
) )
async def async_update(self, event_time): async def async_update(self, event_time: timedelta) -> None:
""" """
Update device. Update device.
@ -115,17 +116,17 @@ class NetatmoDataHandler:
self._queue.rotate(BATCH_SIZE) self._queue.rotate(BATCH_SIZE)
@callback @callback
def async_force_update(self, data_class_entry): def async_force_update(self, data_class_entry: str) -> None:
"""Prioritize data retrieval for given data class entry.""" """Prioritize data retrieval for given data class entry."""
self.data_classes[data_class_entry].next_scan = time() self.data_classes[data_class_entry].next_scan = time()
self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry])))
async def async_cleanup(self): async def async_cleanup(self) -> None:
"""Clean up the Netatmo data handler.""" """Clean up the Netatmo data handler."""
for listener in self.listeners: for listener in self.listeners:
listener() listener()
async def handle_event(self, event): async def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION:
_LOGGER.info("%s webhook successfully registered", MANUFACTURER) _LOGGER.info("%s webhook successfully registered", MANUFACTURER)
@ -139,7 +140,7 @@ class NetatmoDataHandler:
_LOGGER.debug("%s camera reconnected", MANUFACTURER) _LOGGER.debug("%s camera reconnected", MANUFACTURER)
self.async_force_update(CAMERA_DATA_CLASS_NAME) self.async_force_update(CAMERA_DATA_CLASS_NAME)
async def async_fetch_data(self, data_class_entry): async def async_fetch_data(self, data_class_entry: str) -> None:
"""Fetch data and notify.""" """Fetch data and notify."""
if self.data[data_class_entry] is None: if self.data[data_class_entry] is None:
return return
@ -163,8 +164,12 @@ class NetatmoDataHandler:
update_callback() update_callback()
async def register_data_class( async def register_data_class(
self, data_class_name, data_class_entry, update_callback, **kwargs self,
): data_class_name: str,
data_class_entry: str,
update_callback: CALLBACK_TYPE,
**kwargs: Any,
) -> None:
"""Register data class.""" """Register data class."""
if data_class_entry in self.data_classes: if data_class_entry in self.data_classes:
if update_callback not in self.data_classes[data_class_entry].subscriptions: if update_callback not in self.data_classes[data_class_entry].subscriptions:
@ -189,7 +194,9 @@ class NetatmoDataHandler:
self._queue.append(self.data_classes[data_class_entry]) self._queue.append(self.data_classes[data_class_entry])
_LOGGER.debug("Data class %s added", data_class_entry) _LOGGER.debug("Data class %s added", data_class_entry)
async def unregister_data_class(self, data_class_entry, update_callback): async def unregister_data_class(
self, data_class_entry: str, update_callback: CALLBACK_TYPE | None
) -> None:
"""Unregister data class.""" """Unregister data class."""
self.data_classes[data_class_entry].subscriptions.remove(update_callback) self.data_classes[data_class_entry].subscriptions.remove(update_callback)

View file

@ -63,7 +63,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
) )
async def async_validate_trigger_config(hass, config): async def async_validate_trigger_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config.""" """Validate config."""
config = TRIGGER_SCHEMA(config) config = TRIGGER_SCHEMA(config)
@ -129,10 +131,10 @@ async def async_attach_trigger(
device = device_registry.async_get(config[CONF_DEVICE_ID]) device = device_registry.async_get(config[CONF_DEVICE_ID])
if not device: if not device:
return return lambda: None
if device.model not in DEVICES: if device.model not in DEVICES:
return return lambda: None
event_config = { event_config = {
event_trigger.CONF_PLATFORM: "event", event_trigger.CONF_PLATFORM: "event",
@ -142,10 +144,14 @@ async def async_attach_trigger(
ATTR_DEVICE_ID: config[ATTR_DEVICE_ID], ATTR_DEVICE_ID: config[ATTR_DEVICE_ID],
}, },
} }
# if config[CONF_TYPE] in SUBTYPES:
# event_config[event_trigger.CONF_EVENT_DATA]["data"] = {
# "mode": config[CONF_SUBTYPE]
# }
if config[CONF_TYPE] in SUBTYPES: if config[CONF_TYPE] in SUBTYPES:
event_config[event_trigger.CONF_EVENT_DATA]["data"] = { event_config.update(
"mode": config[CONF_SUBTYPE] {event_trigger.CONF_EVENT_DATA: {"data": {"mode": config[CONF_SUBTYPE]}}}
} )
event_config = event_trigger.TRIGGER_SCHEMA(event_config) event_config = event_trigger.TRIGGER_SCHEMA(event_config)
return await event_trigger.async_attach_trigger( return await event_trigger.async_attach_trigger(

View file

@ -1,6 +1,6 @@
"""Helper for Netatmo integration.""" """Helper for Netatmo integration."""
from dataclasses import dataclass from dataclasses import dataclass
from uuid import uuid4 from uuid import UUID, uuid4
@dataclass @dataclass
@ -14,4 +14,4 @@ class NetatmoArea:
lon_sw: float lon_sw: float
mode: str mode: str
show_on_map: bool show_on_map: bool
uuid: str = uuid4() uuid: UUID = uuid4()

View file

@ -1,10 +1,17 @@
"""Support for the Netatmo camera lights.""" """Support for the Netatmo camera lights."""
from __future__ import annotations
import logging import logging
from typing import cast
import pyatmo
from homeassistant.components.light import LightEntity from homeassistant.components.light import LightEntity
from homeassistant.core import callback from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DATA_HANDLER, DATA_HANDLER,
@ -12,6 +19,7 @@ from .const import (
EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_LIGHT_MODE,
MANUFACTURER, MANUFACTURER,
SIGNAL_NAME, SIGNAL_NAME,
UNKNOWN,
WEBHOOK_LIGHT_MODE, WEBHOOK_LIGHT_MODE,
WEBHOOK_PUSH_TYPE, WEBHOOK_PUSH_TYPE,
) )
@ -21,7 +29,9 @@ from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
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 the Netatmo camera light platform.""" """Set up the Netatmo camera light platform."""
if "access_camera" not in entry.data["token"]["scope"]: if "access_camera" not in entry.data["token"]["scope"]:
_LOGGER.info( _LOGGER.info(
@ -79,7 +89,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
self._id = camera_id self._id = camera_id
self._home_id = home_id self._home_id = home_id
self._model = camera_type self._model = camera_type
self._device_name = self._data.get_camera(camera_id).get("name") self._device_name: str = self._data.get_camera(camera_id).get("name", UNKNOWN)
self._attr_name = f"{MANUFACTURER} {self._device_name}" self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._is_on = False self._is_on = False
self._attr_unique_id = f"{self._id}-light" self._attr_unique_id = f"{self._id}-light"
@ -97,7 +107,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
) )
@callback @callback
def handle_event(self, event): def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
data = event["data"] data = event["data"]
@ -114,17 +124,25 @@ class NetatmoLight(NetatmoBase, LightEntity):
self.async_write_ha_state() self.async_write_ha_state()
return return
@property
def _data(self) -> pyatmo.AsyncCameraData:
"""Return data for this entity."""
return cast(
pyatmo.AsyncCameraData,
self.data_handler.data[self._data_classes[0]["name"]],
)
@property @property
def available(self) -> bool: def available(self) -> bool:
"""If the webhook is not established, mark as unavailable.""" """If the webhook is not established, mark as unavailable."""
return bool(self.data_handler.webhook) return bool(self.data_handler.webhook)
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if light is on.""" """Return true if light is on."""
return self._is_on return self._is_on
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: dict) -> None:
"""Turn camera floodlight on.""" """Turn camera floodlight on."""
_LOGGER.debug("Turn camera '%s' on", self.name) _LOGGER.debug("Turn camera '%s' on", self.name)
await self._data.async_set_state( await self._data.async_set_state(
@ -133,7 +151,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
floodlight="on", floodlight="on",
) )
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: dict) -> None:
"""Turn camera floodlight into auto mode.""" """Turn camera floodlight into auto mode."""
_LOGGER.debug("Turn camera '%s' to auto mode", self.name) _LOGGER.debug("Turn camera '%s' to auto mode", self.name)
await self._data.async_set_state( await self._data.async_set_state(
@ -143,6 +161,6 @@ class NetatmoLight(NetatmoBase, LightEntity):
) )
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
self._is_on = bool(self._data.get_light_state(self._id) == "on") self._is_on = bool(self._data.get_light_state(self._id) == "on")

View file

@ -3,7 +3,7 @@
"name": "Netatmo", "name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [ "requirements": [
"pyatmo==5.2.0" "pyatmo==5.2.1"
], ],
"after_dependencies": [ "after_dependencies": [
"cloud", "cloud",

View file

@ -11,7 +11,6 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO,
) )
from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES
from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.error import MediaSourceError, Unresolvable
from homeassistant.components.media_source.models import ( from homeassistant.components.media_source.models import (
BrowseMediaSource, BrowseMediaSource,
@ -31,7 +30,7 @@ class IncompatibleMediaSource(MediaSourceError):
"""Incompatible media source attributes.""" """Incompatible media source attributes."""
async def async_get_media_source(hass: HomeAssistant): async def async_get_media_source(hass: HomeAssistant) -> NetatmoSource:
"""Set up Netatmo media source.""" """Set up Netatmo media source."""
return NetatmoSource(hass) return NetatmoSource(hass)
@ -54,7 +53,9 @@ class NetatmoSource(MediaSource):
return PlayMedia(url, MIME_TYPE) return PlayMedia(url, MIME_TYPE)
async def async_browse_media( async def async_browse_media(
self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES self,
item: MediaSourceItem,
media_types: tuple[str] = ("video",),
) -> BrowseMediaSource: ) -> BrowseMediaSource:
"""Return media.""" """Return media."""
try: try:
@ -65,7 +66,7 @@ class NetatmoSource(MediaSource):
return self._browse_media(source, camera_id, event_id) return self._browse_media(source, camera_id, event_id)
def _browse_media( def _browse_media(
self, source: str, camera_id: str, event_id: int self, source: str, camera_id: str, event_id: int | None
) -> BrowseMediaSource: ) -> BrowseMediaSource:
"""Browse media.""" """Browse media."""
if camera_id and camera_id not in self.events: if camera_id and camera_id not in self.events:
@ -77,7 +78,7 @@ class NetatmoSource(MediaSource):
return self._build_item_response(source, camera_id, event_id) return self._build_item_response(source, camera_id, event_id)
def _build_item_response( def _build_item_response(
self, source: str, camera_id: str, event_id: int = None self, source: str, camera_id: str, event_id: int | None = None
) -> BrowseMediaSource: ) -> BrowseMediaSource:
if event_id and event_id in self.events[camera_id]: if event_id and event_id in self.events[camera_id]:
created = dt.datetime.fromtimestamp(event_id) created = dt.datetime.fromtimestamp(event_id)
@ -148,7 +149,7 @@ class NetatmoSource(MediaSource):
return media return media
def remove_html_tags(text): def remove_html_tags(text: str) -> str:
"""Remove html tags from string.""" """Remove html tags from string."""
clean = re.compile("<.*?>") clean = re.compile("<.*?>")
return re.sub(clean, "", text) return re.sub(clean, "", text)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import ( from .const import (
DATA_DEVICE_IDS, DATA_DEVICE_IDS,
@ -25,12 +25,14 @@ class NetatmoBase(Entity):
self._data_classes: list[dict] = [] self._data_classes: list[dict] = []
self._listeners: list[CALLBACK_TYPE] = [] self._listeners: list[CALLBACK_TYPE] = []
self._device_name = None self._device_name: str = ""
self._id = None self._id: str = ""
self._model = None self._model: str = ""
self._attr_name = None self._attr_name = None
self._attr_unique_id = None self._attr_unique_id = None
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attr_extra_state_attributes: dict = {
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION
}
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Entity created.""" """Entity created."""
@ -71,7 +73,7 @@ class NetatmoBase(Entity):
self.async_update_callback() self.async_update_callback()
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
@ -84,17 +86,12 @@ class NetatmoBase(Entity):
) )
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
raise NotImplementedError raise NotImplementedError
@property @property
def _data(self): def device_info(self) -> DeviceInfo:
"""Return data for this entity."""
return self.data_handler.data[self._data_classes[0]["name"]]
@property
def device_info(self):
"""Return the device info for the sensor.""" """Return the device info for the sensor."""
return { return {
"identifiers": {(DOMAIN, self._id)}, "identifiers": {(DOMAIN, self._id)},

View file

@ -2,9 +2,12 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import NamedTuple from typing import NamedTuple, cast
import pyatmo
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
@ -24,19 +27,21 @@ from homeassistant.const import (
SPEED_KILOMETERS_PER_HOUR, SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.device_registry import async_entries_for_config_entry
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
) )
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME from .const import CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME
from .data_handler import ( from .data_handler import (
HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME,
PUBLICDATA_DATA_CLASS_NAME, PUBLICDATA_DATA_CLASS_NAME,
WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME,
NetatmoDataHandler,
) )
from .helper import NetatmoArea from .helper import NetatmoArea
from .netatmo_entity_base import NetatmoBase from .netatmo_entity_base import NetatmoBase
@ -267,12 +272,14 @@ BATTERY_VALUES = {
PUBLIC = "public" PUBLIC = "public"
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 the Netatmo weather and homecoach platform.""" """Set up the Netatmo weather and homecoach platform."""
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
platform_not_ready = True platform_not_ready = True
async def find_entities(data_class_name): async def find_entities(data_class_name: str) -> list:
"""Find all entities.""" """Find all entities."""
all_module_infos = {} all_module_infos = {}
data = data_handler.data data = data_handler.data
@ -330,7 +337,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
device_registry = await hass.helpers.device_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry()
async def add_public_entities(update=True): async def add_public_entities(update: bool = True) -> None:
"""Retrieve Netatmo public weather entities.""" """Retrieve Netatmo public weather entities."""
entities = { entities = {
device.name: device.id device.name: device.id
@ -396,7 +403,13 @@ async def async_setup_entry(hass, entry, async_add_entities):
class NetatmoSensor(NetatmoBase, SensorEntity): class NetatmoSensor(NetatmoBase, SensorEntity):
"""Implementation of a Netatmo sensor.""" """Implementation of a Netatmo sensor."""
def __init__(self, data_handler, data_class_name, module_info, sensor_type): def __init__(
self,
data_handler: NetatmoDataHandler,
data_class_name: str,
module_info: dict,
sensor_type: str,
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data_handler) super().__init__(data_handler)
@ -434,20 +447,21 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
self._attr_entity_registry_enabled_default = metadata.enable_default self._attr_entity_registry_enabled_default = metadata.enable_default
@property @property
def available(self): def _data(self) -> pyatmo.AsyncWeatherStationData:
"""Return data for this entity."""
return cast(
pyatmo.AsyncWeatherStationData,
self.data_handler.data[self._data_classes[0]["name"]],
)
@property
def available(self) -> bool:
"""Return entity availability.""" """Return entity availability."""
return self.state is not None return self.state is not None
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
if self._data is None:
if self.state is None:
return
_LOGGER.warning("No data from update")
self._attr_state = None
return
data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get( data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get(
self._id self._id
) )
@ -531,7 +545,7 @@ def process_battery(data: int, model: str) -> str:
return "Very Low" return "Very Low"
def process_health(health): def process_health(health: int) -> str:
"""Process health index and return string for display.""" """Process health index and return string for display."""
if health == 0: if health == 0:
return "Healthy" return "Healthy"
@ -541,11 +555,10 @@ def process_health(health):
return "Fair" return "Fair"
if health == 3: if health == 3:
return "Poor" return "Poor"
if health == 4:
return "Unhealthy" return "Unhealthy"
def process_rf(strength): def process_rf(strength: int) -> str:
"""Process wifi signal strength and return string for display.""" """Process wifi signal strength and return string for display."""
if strength >= 90: if strength >= 90:
return "Low" return "Low"
@ -556,7 +569,7 @@ def process_rf(strength):
return "Full" return "Full"
def process_wifi(strength): def process_wifi(strength: int) -> str:
"""Process wifi signal strength and return string for display.""" """Process wifi signal strength and return string for display."""
if strength >= 86: if strength >= 86:
return "Low" return "Low"
@ -570,7 +583,9 @@ def process_wifi(strength):
class NetatmoPublicSensor(NetatmoBase, SensorEntity): class NetatmoPublicSensor(NetatmoBase, SensorEntity):
"""Represent a single sensor in a Netatmo.""" """Represent a single sensor in a Netatmo."""
def __init__(self, data_handler, area, sensor_type): def __init__(
self, data_handler: NetatmoDataHandler, area: NetatmoArea, sensor_type: str
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data_handler) super().__init__(data_handler)
@ -611,13 +626,15 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
) )
@property @property
def _data(self): def _data(self) -> pyatmo.AsyncPublicData:
return self.data_handler.data[self._signal_name] """Return data for this entity."""
return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name])
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Entity created.""" """Entity created."""
await super().async_added_to_hass() await super().async_added_to_hass()
assert self.device_info and "name" in self.device_info
self.data_handler.listeners.append( self.data_handler.listeners.append(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, self.hass,
@ -626,7 +643,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
) )
) )
async def async_config_update_callback(self, area): async def async_config_update_callback(self, area: NetatmoArea) -> None:
"""Update the entity's config.""" """Update the entity's config."""
if self.area == area: if self.area == area:
return return
@ -661,7 +678,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
) )
@callback @callback
def async_update_callback(self): def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
data = None data = None

View file

@ -1,7 +1,10 @@
"""The Netatmo integration.""" """The Netatmo integration."""
import logging import logging
from aiohttp.web import Request
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME from homeassistant.const import ATTR_DEVICE_ID, ATTR_ID, ATTR_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import ( from .const import (
@ -25,7 +28,9 @@ SUBEVENT_TYPE_MAP = {
} }
async def async_handle_webhook(hass, webhook_id, request): async def async_handle_webhook(
hass: HomeAssistant, webhook_id: str, request: Request
) -> None:
"""Handle webhook callback.""" """Handle webhook callback."""
try: try:
data = await request.json() data = await request.json()
@ -47,12 +52,12 @@ async def async_handle_webhook(hass, webhook_id, request):
async_evaluate_event(hass, data) async_evaluate_event(hass, data)
def async_evaluate_event(hass, event_data): def async_evaluate_event(hass: HomeAssistant, event_data: dict) -> None:
"""Evaluate events from webhook.""" """Evaluate events from webhook."""
event_type = event_data.get(ATTR_EVENT_TYPE) event_type = event_data.get(ATTR_EVENT_TYPE, "None")
if event_type == "person": if event_type == "person":
for person in event_data.get(ATTR_PERSONS): for person in event_data.get(ATTR_PERSONS, {}):
person_event_data = dict(event_data) person_event_data = dict(event_data)
person_event_data[ATTR_ID] = person.get(ATTR_ID) person_event_data[ATTR_ID] = person.get(ATTR_ID)
person_event_data[ATTR_NAME] = hass.data[DOMAIN][DATA_PERSONS].get( person_event_data[ATTR_NAME] = hass.data[DOMAIN][DATA_PERSONS].get(
@ -67,7 +72,7 @@ def async_evaluate_event(hass, event_data):
async_send_event(hass, event_type, event_data) async_send_event(hass, event_type, event_data)
def async_send_event(hass, event_type, data): def async_send_event(hass: HomeAssistant, event_type: str, data: dict) -> None:
"""Send events.""" """Send events."""
_LOGGER.debug("%s: %s", event_type, data) _LOGGER.debug("%s: %s", event_type, data)
async_dispatcher_send( async_dispatcher_send(

View file

@ -671,6 +671,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.netatmo.*]
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.network.*] [mypy-homeassistant.components.network.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -1388,9 +1399,6 @@ ignore_errors = true
[mypy-homeassistant.components.nest.legacy.*] [mypy-homeassistant.components.nest.legacy.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.netatmo.*]
ignore_errors = true
[mypy-homeassistant.components.netio.*] [mypy-homeassistant.components.netio.*]
ignore_errors = true ignore_errors = true

View file

@ -1316,7 +1316,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==5.2.0 pyatmo==5.2.1
# homeassistant.components.atome # homeassistant.components.atome
pyatome==0.1.1 pyatome==0.1.1

View file

@ -747,7 +747,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==5.2.0 pyatmo==5.2.1
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.8.1 pyatv==0.8.1

View file

@ -110,7 +110,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.neato.*", "homeassistant.components.neato.*",
"homeassistant.components.ness_alarm.*", "homeassistant.components.ness_alarm.*",
"homeassistant.components.nest.legacy.*", "homeassistant.components.nest.legacy.*",
"homeassistant.components.netatmo.*",
"homeassistant.components.netio.*", "homeassistant.components.netio.*",
"homeassistant.components.nightscout.*", "homeassistant.components.nightscout.*",
"homeassistant.components.nilu.*", "homeassistant.components.nilu.*",

View file

@ -16,10 +16,10 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
select_entity_livingroom = "select.netatmo_myhome" select_entity = "select.netatmo_myhome"
assert hass.states.get(select_entity_livingroom).state == "Default" assert hass.states.get(select_entity).state == "Default"
assert hass.states.get(select_entity_livingroom).attributes[ATTR_OPTIONS] == [ assert hass.states.get(select_entity).attributes[ATTR_OPTIONS] == [
"Default", "Default",
"Winter", "Winter",
] ]
@ -33,7 +33,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
} }
await simulate_webhook(hass, webhook_id, response) await simulate_webhook(hass, webhook_id, response)
assert hass.states.get(select_entity_livingroom).state == "Winter" assert hass.states.get(select_entity).state == "Winter"
# Test setting a different schedule # Test setting a different schedule
with patch( with patch(
@ -43,7 +43,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
SELECT_DOMAIN, SELECT_DOMAIN,
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
{ {
ATTR_ENTITY_ID: select_entity_livingroom, ATTR_ENTITY_ID: select_entity,
ATTR_OPTION: "Default", ATTR_OPTION: "Default",
}, },
blocking=True, blocking=True,
@ -62,4 +62,4 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
} }
await simulate_webhook(hass, webhook_id, response) await simulate_webhook(hass, webhook_id, response)
assert hass.states.get(select_entity_livingroom).state == "Default" assert hass.states.get(select_entity).state == "Default"