Add type annotations for Netatmo (#52811)
This commit is contained in:
parent
84c482441d
commit
583deada83
21 changed files with 288 additions and 177 deletions
|
@ -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.*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 []
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)},
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
14
mypy.ini
14
mypy.ini
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.*",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue