Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
parent
1b17c83095
commit
81abeac83e
37 changed files with 2915 additions and 1279 deletions
|
@ -8,6 +8,7 @@ import secrets
|
|||
|
||||
import aiohttp
|
||||
import pyatmo
|
||||
from pyatmo.const import ALL_SCOPES as NETATMO_SCOPES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import cloud
|
||||
|
@ -51,7 +52,6 @@ from .const import (
|
|||
DATA_PERSONS,
|
||||
DATA_SCHEDULES,
|
||||
DOMAIN,
|
||||
NETATMO_SCOPES,
|
||||
PLATFORMS,
|
||||
WEBHOOK_DEACTIVATION,
|
||||
WEBHOOK_PUSH_TYPE,
|
||||
|
@ -150,10 +150,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
}
|
||||
|
||||
data_handler = NetatmoDataHandler(hass, entry)
|
||||
await data_handler.async_setup()
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await data_handler.async_setup()
|
||||
|
||||
async def unregister_webhook(
|
||||
call_or_event_or_dt: ServiceCall | Event | datetime | None,
|
||||
|
@ -208,7 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
_LOGGER.info("Register Netatmo webhook: %s", webhook_url)
|
||||
except pyatmo.ApiError as err:
|
||||
_LOGGER.error("Error during webhook registration - %s", err)
|
||||
|
||||
else:
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import pyatmo
|
|||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
|
||||
class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth):
|
||||
class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth):
|
||||
"""Provide Netatmo authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
|
|
|
@ -5,13 +5,14 @@ import logging
|
|||
from typing import Any, cast
|
||||
|
||||
import aiohttp
|
||||
import pyatmo
|
||||
from pyatmo import ApiError as NetatmoApiError, modules as NaModules
|
||||
from pyatmo.event import Event as NaEvent
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
@ -20,28 +21,24 @@ from .const import (
|
|||
ATTR_CAMERA_LIGHT_MODE,
|
||||
ATTR_PERSON,
|
||||
ATTR_PERSONS,
|
||||
ATTR_PSEUDO,
|
||||
CAMERA_LIGHT_MODES,
|
||||
CONF_URL_SECURITY,
|
||||
DATA_CAMERAS,
|
||||
DATA_EVENTS,
|
||||
DATA_HANDLER,
|
||||
DATA_PERSONS,
|
||||
DOMAIN,
|
||||
EVENT_TYPE_LIGHT_MODE,
|
||||
EVENT_TYPE_OFF,
|
||||
EVENT_TYPE_ON,
|
||||
MANUFACTURER,
|
||||
MODELS,
|
||||
NETATMO_CREATE_CAMERA,
|
||||
SERVICE_SET_CAMERA_LIGHT,
|
||||
SERVICE_SET_PERSON_AWAY,
|
||||
SERVICE_SET_PERSONS_HOME,
|
||||
SIGNAL_NAME,
|
||||
TYPE_SECURITY,
|
||||
WEBHOOK_LIGHT_MODE,
|
||||
WEBHOOK_NACAMERA_CONNECTION,
|
||||
WEBHOOK_PUSH_TYPE,
|
||||
)
|
||||
from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler
|
||||
from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -53,42 +50,15 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Netatmo camera platform."""
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
|
||||
data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
entity = NetatmoCamera(netatmo_device)
|
||||
async_add_entities([entity])
|
||||
|
||||
if not data_class or not data_class.raw_data:
|
||||
raise PlatformNotReady
|
||||
|
||||
all_cameras = []
|
||||
for home in data_class.cameras.values():
|
||||
for camera in home.values():
|
||||
all_cameras.append(camera)
|
||||
|
||||
entities = [
|
||||
NetatmoCamera(
|
||||
data_handler,
|
||||
camera["id"],
|
||||
camera["type"],
|
||||
camera["home_id"],
|
||||
DEFAULT_QUALITY,
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_CAMERA, _create_entity)
|
||||
)
|
||||
for camera in all_cameras
|
||||
]
|
||||
|
||||
for home in data_class.homes.values():
|
||||
if home.get("id") is None:
|
||||
continue
|
||||
|
||||
hass.data[DOMAIN][DATA_PERSONS][home["id"]] = {
|
||||
person_id: person_data.get(ATTR_PSEUDO)
|
||||
for person_id, person_data in data_handler.data[CAMERA_DATA_CLASS_NAME]
|
||||
.persons[home["id"]]
|
||||
.items()
|
||||
}
|
||||
|
||||
_LOGGER.debug("Adding cameras %s", entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
|
@ -118,41 +88,44 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
data_handler: NetatmoDataHandler,
|
||||
camera_id: str,
|
||||
camera_type: str,
|
||||
home_id: str,
|
||||
quality: str,
|
||||
netatmo_device: NetatmoDevice,
|
||||
) -> None:
|
||||
"""Set up for access to the Netatmo camera images."""
|
||||
Camera.__init__(self)
|
||||
super().__init__(data_handler)
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._publishers.append(
|
||||
{"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
|
||||
)
|
||||
|
||||
self._id = camera_id
|
||||
self._home_id = home_id
|
||||
self._device_name = self._data.get_camera(camera_id=camera_id)["name"]
|
||||
self._model = camera_type
|
||||
self._netatmo_type = TYPE_SECURITY
|
||||
self._camera = cast(NaModules.Camera, netatmo_device.device)
|
||||
self._id = self._camera.entity_id
|
||||
self._home_id = self._camera.home.entity_id
|
||||
self._device_name = self._camera.name
|
||||
self._model = self._camera.device_type
|
||||
self._config_url = CONF_URL_SECURITY
|
||||
self._attr_unique_id = f"{self._id}-{self._model}"
|
||||
self._quality = quality
|
||||
self._vpnurl: str | None = None
|
||||
self._localurl: str | None = None
|
||||
self._status: str | None = None
|
||||
self._sd_status: str | None = None
|
||||
self._alim_status: str | None = None
|
||||
self._is_local: str | None = None
|
||||
self._quality = DEFAULT_QUALITY
|
||||
self._monitoring: bool | None = None
|
||||
self._light_state = None
|
||||
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: f"{HOME}-{self._home_id}",
|
||||
},
|
||||
{
|
||||
"name": EVENT,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: f"{EVENT}-{self._home_id}",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON):
|
||||
self.data_handler.config_entry.async_on_unload(
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"signal-{DOMAIN}-webhook-{event_type}",
|
||||
|
@ -173,13 +146,13 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
if data["home_id"] == self._home_id and data["camera_id"] == self._id:
|
||||
if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"):
|
||||
self._attr_is_streaming = False
|
||||
self._status = "off"
|
||||
self._monitoring = False
|
||||
elif data[WEBHOOK_PUSH_TYPE] in (
|
||||
"NACamera-on",
|
||||
WEBHOOK_NACAMERA_CONNECTION,
|
||||
):
|
||||
self._attr_is_streaming = True
|
||||
self._status = "on"
|
||||
self._monitoring = True
|
||||
elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE:
|
||||
self._light_state = data["sub_type"]
|
||||
self._attr_extra_state_attributes.update(
|
||||
|
@ -189,128 +162,107 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def _data(self) -> pyatmo.AsyncCameraData:
|
||||
"""Return data for this entity."""
|
||||
return cast(
|
||||
pyatmo.AsyncCameraData,
|
||||
self.data_handler.data[self._publishers[0]["name"]],
|
||||
)
|
||||
|
||||
async def async_camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
return cast(
|
||||
bytes, await self._data.async_get_live_snapshot(camera_id=self._id)
|
||||
)
|
||||
return cast(bytes, await self._camera.async_get_live_snapshot())
|
||||
except (
|
||||
aiohttp.ClientPayloadError,
|
||||
aiohttp.ContentTypeError,
|
||||
aiohttp.ServerDisconnectedError,
|
||||
aiohttp.ClientConnectorError,
|
||||
pyatmo.exceptions.ApiError,
|
||||
NetatmoApiError,
|
||||
) as err:
|
||||
_LOGGER.debug("Could not fetch live camera image (%s)", err)
|
||||
return None
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._alim_status == "on" or self._status == "disconnected")
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self) -> bool:
|
||||
"""Return the camera motion detection status."""
|
||||
return bool(self._status == "on")
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if on."""
|
||||
return self.is_streaming
|
||||
def supported_features(self) -> int:
|
||||
"""Return supported features."""
|
||||
supported_features: int = CameraEntityFeature.ON_OFF
|
||||
if self._model != "NDB":
|
||||
supported_features |= CameraEntityFeature.STREAM
|
||||
return supported_features
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off camera."""
|
||||
await self._data.async_set_state(
|
||||
home_id=self._home_id, camera_id=self._id, monitoring="off"
|
||||
)
|
||||
await self._camera.async_monitoring_off()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on camera."""
|
||||
await self._data.async_set_state(
|
||||
home_id=self._home_id, camera_id=self._id, monitoring="on"
|
||||
)
|
||||
await self._camera.async_monitoring_on()
|
||||
|
||||
async def stream_source(self) -> str:
|
||||
"""Return the stream source."""
|
||||
url = "{0}/live/files/{1}/index.m3u8"
|
||||
if self._localurl:
|
||||
return url.format(self._localurl, self._quality)
|
||||
return url.format(self._vpnurl, self._quality)
|
||||
if self._camera.is_local:
|
||||
await self._camera.async_update_camera_urls()
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
"""Return the camera model."""
|
||||
return MODELS[self._model]
|
||||
if self._camera.local_url:
|
||||
return "{}/live/files/{}/index.m3u8".format(
|
||||
self._camera.local_url, self._quality
|
||||
)
|
||||
return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8"
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
camera = self._data.get_camera(self._id)
|
||||
self._vpnurl, self._localurl = self._data.camera_urls(self._id)
|
||||
self._status = camera.get("status")
|
||||
self._sd_status = camera.get("sd_status")
|
||||
self._alim_status = camera.get("alim_status")
|
||||
self._is_local = camera.get("is_local")
|
||||
self._attr_is_streaming = bool(self._status == "on")
|
||||
self._attr_is_on = self._camera.alim_status is not None
|
||||
self._attr_available = self._camera.alim_status is not None
|
||||
|
||||
if self._camera.monitoring is not None:
|
||||
self._attr_is_streaming = self._camera.monitoring
|
||||
self._attr_motion_detection_enabled = self._camera.monitoring
|
||||
|
||||
if self._model == "NACamera": # Smart Indoor Camera
|
||||
self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
|
||||
self._data.events.get(self._id, {})
|
||||
)
|
||||
elif self._model == "NOC": # Smart Outdoor Camera
|
||||
self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
|
||||
self._data.outdoor_events.get(self._id, {})
|
||||
self._camera.events
|
||||
)
|
||||
|
||||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
"id": self._id,
|
||||
"status": self._status,
|
||||
"sd_status": self._sd_status,
|
||||
"alim_status": self._alim_status,
|
||||
"is_local": self._is_local,
|
||||
"vpn_url": self._vpnurl,
|
||||
"local_url": self._localurl,
|
||||
"monitoring": self._monitoring,
|
||||
"sd_status": self._camera.sd_status,
|
||||
"alim_status": self._camera.alim_status,
|
||||
"is_local": self._camera.is_local,
|
||||
"vpn_url": self._camera.vpn_url,
|
||||
"local_url": self._camera.local_url,
|
||||
"light_state": self._light_state,
|
||||
}
|
||||
)
|
||||
|
||||
def process_events(self, events: dict) -> dict:
|
||||
def process_events(self, event_list: list[NaEvent]) -> dict:
|
||||
"""Add meta data to events."""
|
||||
for event in events.values():
|
||||
if "video_id" not in event:
|
||||
events = {}
|
||||
for event in event_list:
|
||||
if not (video_id := event.video_id):
|
||||
continue
|
||||
if self._is_local:
|
||||
event[
|
||||
"media_url"
|
||||
] = f"{self._localurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
|
||||
else:
|
||||
event[
|
||||
"media_url"
|
||||
] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
|
||||
event_data = event.__dict__
|
||||
event_data["subevents"] = [
|
||||
event.__dict__
|
||||
for event in event_data.get("subevents", [])
|
||||
if not isinstance(event, dict)
|
||||
]
|
||||
event_data["media_url"] = self.get_video_url(video_id)
|
||||
events[event.event_time] = event_data
|
||||
return events
|
||||
|
||||
def get_video_url(self, video_id: str) -> str:
|
||||
"""Get video url."""
|
||||
if self._camera.is_local:
|
||||
return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
|
||||
return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
|
||||
|
||||
def fetch_person_ids(self, persons: list[str | None]) -> list[str]:
|
||||
"""Fetch matching person ids for give list of persons."""
|
||||
"""Fetch matching person ids for given list of persons."""
|
||||
person_ids = []
|
||||
person_id_errors = []
|
||||
|
||||
for person in persons:
|
||||
person_id = None
|
||||
for pid, data in self._data.persons[self._home_id].items():
|
||||
if data.get("pseudo") == person:
|
||||
for pid, data in self._camera.home.persons.items():
|
||||
if data.pseudo == person:
|
||||
person_ids.append(pid)
|
||||
person_id = pid
|
||||
break
|
||||
|
@ -328,9 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
persons = kwargs.get(ATTR_PERSONS, [])
|
||||
person_ids = self.fetch_person_ids(persons)
|
||||
|
||||
await self._data.async_set_persons_home(
|
||||
person_ids=person_ids, home_id=self._home_id
|
||||
)
|
||||
await self._camera.home.async_set_persons_home(person_ids=person_ids)
|
||||
_LOGGER.debug("Set %s as at home", persons)
|
||||
|
||||
async def _service_set_person_away(self, **kwargs: Any) -> None:
|
||||
|
@ -339,9 +289,8 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
person_ids = self.fetch_person_ids([person] if person else [])
|
||||
person_id = next(iter(person_ids), None)
|
||||
|
||||
await self._data.async_set_persons_away(
|
||||
await self._camera.home.async_set_persons_away(
|
||||
person_id=person_id,
|
||||
home_id=self._home_id,
|
||||
)
|
||||
|
||||
if person_id:
|
||||
|
@ -351,10 +300,11 @@ class NetatmoCamera(NetatmoBase, Camera):
|
|||
|
||||
async def _service_set_camera_light(self, **kwargs: Any) -> None:
|
||||
"""Service to set light mode."""
|
||||
if not isinstance(self._camera, NaModules.netatmo.NOC):
|
||||
raise HomeAssistantError(
|
||||
f"{self._model} <{self._device_name}> does not have a floodlight"
|
||||
)
|
||||
|
||||
mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE))
|
||||
_LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name)
|
||||
await self._data.async_set_state(
|
||||
home_id=self._home_id,
|
||||
camera_id=self._id,
|
||||
floodlight=mode,
|
||||
)
|
||||
await self._camera.async_set_floodlight_state(mode)
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pyatmo
|
||||
from pyatmo.modules import NATherm1
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DEFAULT_MIN_TEMP,
|
||||
PRESET_AWAY,
|
||||
PRESET_BOOST,
|
||||
PRESET_HOME,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
|
@ -25,12 +26,8 @@ from homeassistant.const import (
|
|||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
|
@ -38,25 +35,17 @@ from .const import (
|
|||
ATTR_HEATING_POWER_REQUEST,
|
||||
ATTR_SCHEDULE_NAME,
|
||||
ATTR_SELECTED_SCHEDULE,
|
||||
DATA_HANDLER,
|
||||
DATA_HOMES,
|
||||
CONF_URL_ENERGY,
|
||||
DATA_SCHEDULES,
|
||||
DOMAIN,
|
||||
EVENT_TYPE_CANCEL_SET_POINT,
|
||||
EVENT_TYPE_SCHEDULE,
|
||||
EVENT_TYPE_SET_POINT,
|
||||
EVENT_TYPE_THERM_MODE,
|
||||
NETATMO_CREATE_BATTERY,
|
||||
NETATMO_CREATE_CLIMATE,
|
||||
SERVICE_SET_SCHEDULE,
|
||||
SIGNAL_NAME,
|
||||
TYPE_ENERGY,
|
||||
)
|
||||
from .data_handler import (
|
||||
CLIMATE_STATE_CLASS_NAME,
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
NetatmoDataHandler,
|
||||
NetatmoDevice,
|
||||
)
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -65,6 +54,9 @@ PRESET_FROST_GUARD = "Frost Guard"
|
|||
PRESET_SCHEDULE = "Schedule"
|
||||
PRESET_MANUAL = "Manual"
|
||||
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE]
|
||||
|
||||
STATE_NETATMO_SCHEDULE = "schedule"
|
||||
|
@ -116,46 +108,17 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Netatmo energy platform."""
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
|
||||
climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME)
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoRoom) -> None:
|
||||
entity = NetatmoThermostat(netatmo_device)
|
||||
async_add_entities([entity])
|
||||
|
||||
if not climate_topology or climate_topology.raw_data == {}:
|
||||
raise PlatformNotReady
|
||||
|
||||
entities = []
|
||||
for home_id in climate_topology.home_ids:
|
||||
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
||||
|
||||
await data_handler.subscribe(
|
||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_CLIMATE, _create_entity)
|
||||
)
|
||||
|
||||
if (climate_state := data_handler.data[signal_name]) is None:
|
||||
continue
|
||||
|
||||
climate_topology.register_handler(home_id, climate_state.process_topology)
|
||||
|
||||
for room in climate_state.homes[home_id].rooms.values():
|
||||
if room.device_type is None or room.device_type.value not in [
|
||||
NA_THERM,
|
||||
NA_VALVE,
|
||||
]:
|
||||
continue
|
||||
entities.append(NetatmoThermostat(data_handler, room))
|
||||
|
||||
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[
|
||||
home_id
|
||||
].schedules
|
||||
|
||||
hass.data[DOMAIN][DATA_HOMES][home_id] = climate_state.homes[home_id].name
|
||||
|
||||
_LOGGER.debug("Adding climate devices %s", entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
if climate_topology is not None:
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_SCHEDULE,
|
||||
{vol.Required(ATTR_SCHEDULE_NAME): cv.string},
|
||||
|
@ -169,42 +132,33 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
_attr_hvac_mode = HVACMode.AUTO
|
||||
_attr_max_temp = DEFAULT_MAX_TEMP
|
||||
_attr_preset_modes = SUPPORT_PRESET
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
_attr_supported_features = SUPPORT_FLAGS
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
|
||||
def __init__(
|
||||
self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom
|
||||
) -> None:
|
||||
def __init__(self, netatmo_device: NetatmoRoom) -> None:
|
||||
"""Initialize the sensor."""
|
||||
ClimateEntity.__init__(self)
|
||||
super().__init__(data_handler)
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._room = room
|
||||
self._room = netatmo_device.room
|
||||
self._id = self._room.entity_id
|
||||
self._home_id = self._room.home.entity_id
|
||||
|
||||
self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}"
|
||||
self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name]
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
},
|
||||
{
|
||||
"name": CLIMATE_STATE_CLASS_NAME,
|
||||
"name": HOME,
|
||||
"home_id": self._room.home.entity_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._model: str = getattr(room.device_type, "value")
|
||||
self._model: str = f"{self._room.climate_type}"
|
||||
|
||||
self._netatmo_type = TYPE_ENERGY
|
||||
self._config_url = CONF_URL_ENERGY
|
||||
|
||||
self._attr_name = self._room.name
|
||||
self._away: bool | None = None
|
||||
|
@ -231,7 +185,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
EVENT_TYPE_CANCEL_SET_POINT,
|
||||
EVENT_TYPE_SCHEDULE,
|
||||
):
|
||||
self.data_handler.config_entry.async_on_unload(
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"signal-{DOMAIN}-webhook-{event_type}",
|
||||
|
@ -239,21 +193,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
)
|
||||
)
|
||||
|
||||
for module in self._room.modules.values():
|
||||
if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]:
|
||||
continue
|
||||
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_BATTERY,
|
||||
NetatmoDevice(
|
||||
self.data_handler,
|
||||
module,
|
||||
self._id,
|
||||
self._signal_name,
|
||||
),
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_event(self, event: dict) -> None:
|
||||
"""Handle webhook events."""
|
||||
|
@ -289,7 +228,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
self._attr_target_temperature = self._hg_temperature
|
||||
elif self._attr_preset_mode == PRESET_AWAY:
|
||||
self._attr_target_temperature = self._away_temperature
|
||||
elif self._attr_preset_mode == PRESET_SCHEDULE:
|
||||
elif self._attr_preset_mode in [PRESET_SCHEDULE, PRESET_HOME]:
|
||||
self.async_update_callback()
|
||||
self.data_handler.async_force_update(self._signal_name)
|
||||
self.async_write_ha_state()
|
||||
|
@ -322,6 +261,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT
|
||||
and self._room.entity_id == room["id"]
|
||||
):
|
||||
if self._attr_hvac_mode == HVACMode.OFF:
|
||||
self._attr_hvac_mode = HVACMode.AUTO
|
||||
self._attr_preset_mode = PRESET_MAP_NETATMO[PRESET_SCHEDULE]
|
||||
|
||||
self.async_update_callback()
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
@ -329,7 +272,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the current running hvac operation if supported."""
|
||||
if self._model == NA_THERM and self._boilerstatus is not None:
|
||||
if self._model != NA_VALVE and self._boilerstatus is not None:
|
||||
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
|
||||
# Maybe it is a valve
|
||||
if (
|
||||
|
@ -343,55 +286,36 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
if hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_off()
|
||||
elif hvac_mode == HVACMode.AUTO:
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_on()
|
||||
await self.async_set_preset_mode(PRESET_SCHEDULE)
|
||||
elif hvac_mode == HVACMode.HEAT:
|
||||
await self.async_set_preset_mode(PRESET_BOOST)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self.hvac_mode == HVACMode.OFF:
|
||||
await self.async_turn_on()
|
||||
|
||||
if self.target_temperature == 0:
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id,
|
||||
STATE_NETATMO_HOME,
|
||||
)
|
||||
|
||||
if (
|
||||
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
|
||||
and self._model == NA_VALVE
|
||||
and self.hvac_mode == HVACMode.HEAT
|
||||
and self._attr_hvac_mode == HVACMode.HEAT
|
||||
):
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id,
|
||||
await self._room.async_therm_set(
|
||||
STATE_NETATMO_HOME,
|
||||
)
|
||||
elif (
|
||||
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE
|
||||
):
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id,
|
||||
await self._room.async_therm_set(
|
||||
STATE_NETATMO_MANUAL,
|
||||
DEFAULT_MAX_TEMP,
|
||||
)
|
||||
elif (
|
||||
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
|
||||
and self.hvac_mode == HVACMode.HEAT
|
||||
and self._attr_hvac_mode == HVACMode.HEAT
|
||||
):
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id, STATE_NETATMO_HOME
|
||||
)
|
||||
await self._room.async_therm_set(STATE_NETATMO_HOME)
|
||||
elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id, PRESET_MAP_NETATMO[preset_mode]
|
||||
)
|
||||
await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
|
||||
elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY):
|
||||
await self._climate_state.async_set_thermmode(
|
||||
PRESET_MAP_NETATMO[preset_mode]
|
||||
)
|
||||
await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
|
||||
else:
|
||||
_LOGGER.error("Preset mode '%s' not available", preset_mode)
|
||||
|
||||
|
@ -399,33 +323,25 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature for 2 hours."""
|
||||
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
return
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP)
|
||||
await self._room.async_therm_set(
|
||||
STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP)
|
||||
)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity off."""
|
||||
if self._model == NA_VALVE:
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id,
|
||||
await self._room.async_therm_set(
|
||||
STATE_NETATMO_MANUAL,
|
||||
DEFAULT_MIN_TEMP,
|
||||
)
|
||||
elif self.hvac_mode != HVACMode.OFF:
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id, STATE_NETATMO_OFF
|
||||
)
|
||||
elif self._attr_hvac_mode != HVACMode.OFF:
|
||||
await self._room.async_therm_set(STATE_NETATMO_OFF)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._climate_state.async_set_room_thermpoint(
|
||||
self._room.entity_id, STATE_NETATMO_HOME
|
||||
)
|
||||
await self._room.async_therm_set(STATE_NETATMO_HOME)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -466,6 +382,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
] = self._room.heating_power_request
|
||||
else:
|
||||
for module in self._room.modules.values():
|
||||
if hasattr(module, "boiler_status"):
|
||||
module = cast(NATherm1, module)
|
||||
if module.boiler_status is not None:
|
||||
self._boilerstatus = module.boiler_status
|
||||
break
|
||||
|
||||
|
@ -483,7 +402,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
|
|||
_LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME))
|
||||
return
|
||||
|
||||
await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id)
|
||||
await self._room.home.async_switch_schedule(schedule_id=schedule_id)
|
||||
_LOGGER.debug(
|
||||
"Setting %s schedule to %s (%s)",
|
||||
self._room.home.entity_id,
|
||||
|
|
|
@ -6,6 +6,7 @@ import logging
|
|||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from pyatmo.const import ALL_SCOPES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -25,7 +26,6 @@ from .const import (
|
|||
CONF_UUID,
|
||||
CONF_WEATHER_AREAS,
|
||||
DOMAIN,
|
||||
NETATMO_SCOPES,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -54,7 +54,7 @@ class NetatmoFlowHandler(
|
|||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {"scope": " ".join(NETATMO_SCOPES)}
|
||||
return {"scope": " ".join(ALL_SCOPES)}
|
||||
|
||||
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
|
||||
"""Handle a flow start."""
|
||||
|
|
|
@ -10,60 +10,17 @@ DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}"
|
|||
PLATFORMS = [
|
||||
Platform.CAMERA,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
NETATMO_SCOPES = [
|
||||
"access_camera",
|
||||
"access_presence",
|
||||
"read_camera",
|
||||
"read_homecoach",
|
||||
"read_presence",
|
||||
"read_smokedetector",
|
||||
"read_station",
|
||||
"read_thermostat",
|
||||
"write_camera",
|
||||
"write_presence",
|
||||
"write_thermostat",
|
||||
]
|
||||
|
||||
MODEL_NAPLUG = "Relay"
|
||||
MODEL_NATHERM1 = "Smart Thermostat"
|
||||
MODEL_NRV = "Smart Radiator Valves"
|
||||
MODEL_NOC = "Smart Outdoor Camera"
|
||||
MODEL_NACAMERA = "Smart Indoor Camera"
|
||||
MODEL_NSD = "Smart Smoke Alarm"
|
||||
MODEL_NACAMDOORTAG = "Smart Door and Window Sensors"
|
||||
MODEL_NHC = "Smart Indoor Air Quality Monitor"
|
||||
MODEL_NAMAIN = "Smart Home Weather station – indoor module"
|
||||
MODEL_NAMODULE1 = "Smart Home Weather station – outdoor module"
|
||||
MODEL_NAMODULE4 = "Smart Additional Indoor module"
|
||||
MODEL_NAMODULE3 = "Smart Rain Gauge"
|
||||
MODEL_NAMODULE2 = "Smart Anemometer"
|
||||
MODEL_PUBLIC = "Public Weather stations"
|
||||
|
||||
MODELS = {
|
||||
"NAPlug": MODEL_NAPLUG,
|
||||
"NATherm1": MODEL_NATHERM1,
|
||||
"NRV": MODEL_NRV,
|
||||
"NACamera": MODEL_NACAMERA,
|
||||
"NOC": MODEL_NOC,
|
||||
"NSD": MODEL_NSD,
|
||||
"NACamDoorTag": MODEL_NACAMDOORTAG,
|
||||
"NHC": MODEL_NHC,
|
||||
"NAMain": MODEL_NAMAIN,
|
||||
"NAModule1": MODEL_NAMODULE1,
|
||||
"NAModule4": MODEL_NAMODULE4,
|
||||
"NAModule3": MODEL_NAMODULE3,
|
||||
"NAModule2": MODEL_NAMODULE2,
|
||||
"public": MODEL_PUBLIC,
|
||||
}
|
||||
|
||||
TYPE_SECURITY = "security"
|
||||
TYPE_ENERGY = "energy"
|
||||
TYPE_WEATHER = "weather"
|
||||
CONF_URL_SECURITY = "https://home.netatmo.com/security"
|
||||
CONF_URL_ENERGY = "https://my.netatmo.com/app/energy"
|
||||
CONF_URL_WEATHER = "https://my.netatmo.com/app/weather"
|
||||
CONF_URL_CONTROL = "https://home.netatmo.com/control"
|
||||
|
||||
AUTH = "netatmo_auth"
|
||||
CONF_PUBLIC = "public_sensor_config"
|
||||
|
@ -71,7 +28,18 @@ CAMERA_DATA = "netatmo_camera"
|
|||
HOME_DATA = "netatmo_home_data"
|
||||
DATA_HANDLER = "netatmo_data_handler"
|
||||
SIGNAL_NAME = "signal_name"
|
||||
|
||||
NETATMO_CREATE_BATTERY = "netatmo_create_battery"
|
||||
NETATMO_CREATE_CAMERA = "netatmo_create_camera"
|
||||
NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light"
|
||||
NETATMO_CREATE_CLIMATE = "netatmo_create_climate"
|
||||
NETATMO_CREATE_COVER = "netatmo_create_cover"
|
||||
NETATMO_CREATE_LIGHT = "netatmo_create_light"
|
||||
NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
|
||||
NETATMO_CREATE_SELECT = "netatmo_create_select"
|
||||
NETATMO_CREATE_SENSOR = "netatmo_create_sensor"
|
||||
NETATMO_CREATE_SWITCH = "netatmo_create_switch"
|
||||
NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor"
|
||||
|
||||
CONF_AREA_NAME = "area_name"
|
||||
CONF_CLOUDHOOK_URL = "cloudhook_url"
|
||||
|
|
110
homeassistant/components/netatmo/cover.py
Normal file
110
homeassistant/components/netatmo/cover.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
"""Support for Netatmo/Bubendorff covers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pyatmo import modules as NaModules
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Netatmo cover platform."""
|
||||
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
entity = NetatmoCover(netatmo_device)
|
||||
_LOGGER.debug("Adding cover %s", entity)
|
||||
async_add_entities([entity])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_COVER, _create_entity)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoCover(NetatmoBase, CoverEntity):
|
||||
"""Representation of a Netatmo cover device."""
|
||||
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
|
||||
def __init__(self, netatmo_device: NetatmoDevice) -> None:
|
||||
"""Initialize the Netatmo device."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._cover = cast(NaModules.Shutter, netatmo_device.device)
|
||||
|
||||
self._id = self._cover.entity_id
|
||||
self._attr_name = self._device_name = self._cover.name
|
||||
self._model = self._cover.device_type
|
||||
self._config_url = CONF_URL_CONTROL
|
||||
|
||||
self._home_id = self._cover.home.entity_id
|
||||
self._attr_is_closed = self._cover.current_position == 0
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self._attr_unique_id = f"{self._id}-{self._model}"
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self._cover.async_close()
|
||||
self._attr_is_closed = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self._cover.async_open()
|
||||
self._attr_is_closed = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
await self._cover.async_stop()
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover shutter to a specific position."""
|
||||
await self._cover.async_set_target_position(kwargs[ATTR_POSITION])
|
||||
|
||||
@property
|
||||
def device_class(self) -> str:
|
||||
"""Return the device class."""
|
||||
return CoverDeviceClass.SHUTTER
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
self._attr_is_closed = self._cover.current_position == 0
|
||||
self._attr_current_cover_position = self._cover.current_position
|
|
@ -11,16 +11,34 @@ from time import time
|
|||
from typing import Any
|
||||
|
||||
import pyatmo
|
||||
from pyatmo.modules.device_types import DeviceCategory as NetatmoDeviceCategory
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import (
|
||||
AUTH,
|
||||
DATA_PERSONS,
|
||||
DATA_SCHEDULES,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
NETATMO_CREATE_BATTERY,
|
||||
NETATMO_CREATE_CAMERA,
|
||||
NETATMO_CREATE_CAMERA_LIGHT,
|
||||
NETATMO_CREATE_CLIMATE,
|
||||
NETATMO_CREATE_COVER,
|
||||
NETATMO_CREATE_LIGHT,
|
||||
NETATMO_CREATE_ROOM_SENSOR,
|
||||
NETATMO_CREATE_SELECT,
|
||||
NETATMO_CREATE_SENSOR,
|
||||
NETATMO_CREATE_SWITCH,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
PLATFORMS,
|
||||
WEBHOOK_ACTIVATION,
|
||||
WEBHOOK_DEACTIVATION,
|
||||
WEBHOOK_NACAMERA_CONNECTION,
|
||||
|
@ -29,30 +47,31 @@ from .const import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CAMERA_DATA_CLASS_NAME = "AsyncCameraData"
|
||||
WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData"
|
||||
HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData"
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology"
|
||||
CLIMATE_STATE_CLASS_NAME = "AsyncClimate"
|
||||
PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData"
|
||||
SIGNAL_NAME = "signal_name"
|
||||
ACCOUNT = "account"
|
||||
HOME = "home"
|
||||
WEATHER = "weather"
|
||||
AIR_CARE = "air_care"
|
||||
PUBLIC = "public"
|
||||
EVENT = "event"
|
||||
|
||||
DATA_CLASSES = {
|
||||
WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData,
|
||||
HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData,
|
||||
CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData,
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology,
|
||||
CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate,
|
||||
PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData,
|
||||
PUBLISHERS = {
|
||||
ACCOUNT: "async_update_topology",
|
||||
HOME: "async_update_status",
|
||||
WEATHER: "async_update_weather_stations",
|
||||
AIR_CARE: "async_update_air_care",
|
||||
PUBLIC: "async_update_public_weather",
|
||||
EVENT: "async_update_events",
|
||||
}
|
||||
|
||||
BATCH_SIZE = 3
|
||||
DEFAULT_INTERVALS = {
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME: 3600,
|
||||
CLIMATE_STATE_CLASS_NAME: 300,
|
||||
CAMERA_DATA_CLASS_NAME: 900,
|
||||
WEATHERSTATION_DATA_CLASS_NAME: 600,
|
||||
HOMECOACH_DATA_CLASS_NAME: 300,
|
||||
PUBLICDATA_DATA_CLASS_NAME: 600,
|
||||
ACCOUNT: 10800,
|
||||
HOME: 300,
|
||||
WEATHER: 600,
|
||||
AIR_CARE: 300,
|
||||
PUBLIC: 600,
|
||||
EVENT: 600,
|
||||
}
|
||||
SCAN_INTERVAL = 60
|
||||
|
||||
|
@ -62,7 +81,27 @@ class NetatmoDevice:
|
|||
"""Netatmo device class."""
|
||||
|
||||
data_handler: NetatmoDataHandler
|
||||
device: pyatmo.climate.NetatmoModule
|
||||
device: pyatmo.modules.Module
|
||||
parent_id: str
|
||||
signal_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetatmoHome:
|
||||
"""Netatmo home class."""
|
||||
|
||||
data_handler: NetatmoDataHandler
|
||||
home: pyatmo.Home
|
||||
parent_id: str
|
||||
signal_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetatmoRoom:
|
||||
"""Netatmo room class."""
|
||||
|
||||
data_handler: NetatmoDataHandler
|
||||
room: pyatmo.Room
|
||||
parent_id: str
|
||||
signal_name: str
|
||||
|
||||
|
@ -74,25 +113,27 @@ class NetatmoPublisher:
|
|||
name: str
|
||||
interval: int
|
||||
next_scan: float
|
||||
subscriptions: list[CALLBACK_TYPE | None]
|
||||
subscriptions: set[CALLBACK_TYPE | None]
|
||||
method: str
|
||||
kwargs: dict
|
||||
|
||||
|
||||
class NetatmoDataHandler:
|
||||
"""Manages the Netatmo data handling."""
|
||||
|
||||
account: pyatmo.AsyncAccount
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize self."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH]
|
||||
self.publisher: dict[str, NetatmoPublisher] = {}
|
||||
self.data: dict = {}
|
||||
self._queue: deque = deque()
|
||||
self._webhook: bool = False
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the Netatmo data handler."""
|
||||
|
||||
async_track_time_interval(
|
||||
self.hass, self.async_update, timedelta(seconds=SCAN_INTERVAL)
|
||||
)
|
||||
|
@ -105,17 +146,14 @@ class NetatmoDataHandler:
|
|||
)
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.subscribe(data_class, data_class, None)
|
||||
for data_class in (
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
CAMERA_DATA_CLASS_NAME,
|
||||
WEATHERSTATION_DATA_CLASS_NAME,
|
||||
HOMECOACH_DATA_CLASS_NAME,
|
||||
)
|
||||
]
|
||||
self.account = pyatmo.AsyncAccount(self._auth)
|
||||
|
||||
await self.subscribe(ACCOUNT, ACCOUNT, None)
|
||||
|
||||
await self.hass.config_entries.async_forward_entry_setups(
|
||||
self.config_entry, PLATFORMS
|
||||
)
|
||||
await self.async_dispatch()
|
||||
|
||||
async def async_update(self, event_time: datetime) -> None:
|
||||
"""
|
||||
|
@ -153,19 +191,17 @@ class NetatmoDataHandler:
|
|||
|
||||
elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION:
|
||||
_LOGGER.debug("%s camera reconnected", MANUFACTURER)
|
||||
self.async_force_update(CAMERA_DATA_CLASS_NAME)
|
||||
self.async_force_update(ACCOUNT)
|
||||
|
||||
async def async_fetch_data(self, signal_name: str) -> None:
|
||||
"""Fetch data and notify."""
|
||||
if self.data[signal_name] is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.data[signal_name].async_update()
|
||||
await getattr(self.account, self.publisher[signal_name].method)(
|
||||
**self.publisher[signal_name].kwargs
|
||||
)
|
||||
|
||||
except pyatmo.NoDevice as err:
|
||||
_LOGGER.debug(err)
|
||||
self.data[signal_name] = None
|
||||
|
||||
except pyatmo.ApiError as err:
|
||||
_LOGGER.debug(err)
|
||||
|
@ -188,18 +224,21 @@ class NetatmoDataHandler:
|
|||
"""Subscribe to publisher."""
|
||||
if signal_name in self.publisher:
|
||||
if update_callback not in self.publisher[signal_name].subscriptions:
|
||||
self.publisher[signal_name].subscriptions.append(update_callback)
|
||||
self.publisher[signal_name].subscriptions.add(update_callback)
|
||||
return
|
||||
|
||||
if publisher == "public":
|
||||
kwargs = {"area_id": self.account.register_public_weather_area(**kwargs)}
|
||||
|
||||
self.publisher[signal_name] = NetatmoPublisher(
|
||||
name=signal_name,
|
||||
interval=DEFAULT_INTERVALS[publisher],
|
||||
next_scan=time() + DEFAULT_INTERVALS[publisher],
|
||||
subscriptions=[update_callback],
|
||||
subscriptions={update_callback},
|
||||
method=PUBLISHERS[publisher],
|
||||
kwargs=kwargs,
|
||||
)
|
||||
|
||||
self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs)
|
||||
|
||||
try:
|
||||
await self.async_fetch_data(signal_name)
|
||||
except KeyError:
|
||||
|
@ -213,15 +252,158 @@ class NetatmoDataHandler:
|
|||
self, signal_name: str, update_callback: CALLBACK_TYPE | None
|
||||
) -> None:
|
||||
"""Unsubscribe from publisher."""
|
||||
if update_callback in self.publisher[signal_name].subscriptions:
|
||||
return
|
||||
|
||||
self.publisher[signal_name].subscriptions.remove(update_callback)
|
||||
|
||||
if not self.publisher[signal_name].subscriptions:
|
||||
self._queue.remove(self.publisher[signal_name])
|
||||
self.publisher.pop(signal_name)
|
||||
self.data.pop(signal_name)
|
||||
_LOGGER.debug("Publisher %s removed", signal_name)
|
||||
|
||||
@property
|
||||
def webhook(self) -> bool:
|
||||
"""Return the webhook state."""
|
||||
return self._webhook
|
||||
|
||||
async def async_dispatch(self) -> None:
|
||||
"""Dispatch the creation of entities."""
|
||||
await self.subscribe(WEATHER, WEATHER, None)
|
||||
await self.subscribe(AIR_CARE, AIR_CARE, None)
|
||||
|
||||
self.setup_air_care()
|
||||
|
||||
for home in self.account.homes.values():
|
||||
signal_home = f"{HOME}-{home.entity_id}"
|
||||
|
||||
await self.subscribe(HOME, signal_home, None, home_id=home.entity_id)
|
||||
await self.subscribe(EVENT, signal_home, None, home_id=home.entity_id)
|
||||
|
||||
self.setup_climate_schedule_select(home, signal_home)
|
||||
self.setup_rooms(home, signal_home)
|
||||
self.setup_modules(home, signal_home)
|
||||
|
||||
self.hass.data[DOMAIN][DATA_PERSONS][home.entity_id] = {
|
||||
person.entity_id: person.pseudo for person in home.persons.values()
|
||||
}
|
||||
|
||||
def setup_air_care(self) -> None:
|
||||
"""Set up home coach/air care modules."""
|
||||
for module in self.account.modules.values():
|
||||
if module.device_category is NetatmoDeviceCategory.air_care:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
AIR_CARE,
|
||||
AIR_CARE,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up modules."""
|
||||
netatmo_type_signal_map = {
|
||||
NetatmoDeviceCategory.camera: [
|
||||
NETATMO_CREATE_CAMERA,
|
||||
NETATMO_CREATE_CAMERA_LIGHT,
|
||||
],
|
||||
NetatmoDeviceCategory.dimmer: [NETATMO_CREATE_LIGHT],
|
||||
NetatmoDeviceCategory.shutter: [NETATMO_CREATE_COVER],
|
||||
NetatmoDeviceCategory.switch: [
|
||||
NETATMO_CREATE_LIGHT,
|
||||
NETATMO_CREATE_SWITCH,
|
||||
NETATMO_CREATE_SENSOR,
|
||||
],
|
||||
NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR],
|
||||
}
|
||||
for module in home.modules.values():
|
||||
if not module.device_category:
|
||||
continue
|
||||
|
||||
for signal in netatmo_type_signal_map.get(module.device_category, []):
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
signal,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
signal_home,
|
||||
),
|
||||
)
|
||||
if module.device_category is NetatmoDeviceCategory.weather:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
home.entity_id,
|
||||
WEATHER,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None:
|
||||
"""Set up rooms."""
|
||||
for room in home.rooms.values():
|
||||
if NetatmoDeviceCategory.climate in room.features:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_CLIMATE,
|
||||
NetatmoRoom(
|
||||
self,
|
||||
room,
|
||||
home.entity_id,
|
||||
signal_home,
|
||||
),
|
||||
)
|
||||
|
||||
for module in room.modules.values():
|
||||
if module.device_category is NetatmoDeviceCategory.climate:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_BATTERY,
|
||||
NetatmoDevice(
|
||||
self,
|
||||
module,
|
||||
room.entity_id,
|
||||
signal_home,
|
||||
),
|
||||
)
|
||||
|
||||
if "humidity" in room.features:
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_ROOM_SENSOR,
|
||||
NetatmoRoom(
|
||||
self,
|
||||
room,
|
||||
room.entity_id,
|
||||
signal_home,
|
||||
),
|
||||
)
|
||||
|
||||
def setup_climate_schedule_select(
|
||||
self, home: pyatmo.Home, signal_home: str
|
||||
) -> None:
|
||||
"""Set up climate schedule per home."""
|
||||
if NetatmoDeviceCategory.climate in [
|
||||
next(iter(x)) for x in [room.features for room in home.rooms.values()] if x
|
||||
]:
|
||||
self.hass.data[DOMAIN][DATA_SCHEDULES][home.entity_id] = self.account.homes[
|
||||
home.entity_id
|
||||
].schedules
|
||||
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
NETATMO_CREATE_SELECT,
|
||||
NetatmoHome(
|
||||
self,
|
||||
home,
|
||||
home.entity_id,
|
||||
signal_home,
|
||||
),
|
||||
)
|
||||
|
|
|
@ -31,10 +31,6 @@ from .const import (
|
|||
DOMAIN,
|
||||
EVENT_TYPE_THERM_MODE,
|
||||
INDOOR_CAMERA_TRIGGERS,
|
||||
MODEL_NACAMERA,
|
||||
MODEL_NATHERM1,
|
||||
MODEL_NOC,
|
||||
MODEL_NRV,
|
||||
NETATMO_EVENT,
|
||||
OUTDOOR_CAMERA_TRIGGERS,
|
||||
)
|
||||
|
@ -42,10 +38,10 @@ from .const import (
|
|||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
DEVICES = {
|
||||
MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS,
|
||||
MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS,
|
||||
MODEL_NATHERM1: CLIMATE_TRIGGERS,
|
||||
MODEL_NRV: CLIMATE_TRIGGERS,
|
||||
"NACamera": INDOOR_CAMERA_TRIGGERS,
|
||||
"NOC": OUTDOOR_CAMERA_TRIGGERS,
|
||||
"NATherm1": CLIMATE_TRIGGERS,
|
||||
"NRV": CLIMATE_TRIGGERS,
|
||||
}
|
||||
|
||||
SUBTYPES = {
|
||||
|
@ -76,7 +72,7 @@ async def async_validate_trigger_config(
|
|||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(config[CONF_DEVICE_ID])
|
||||
|
||||
if not device:
|
||||
if not device or device.model is None:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Trigger invalid, device with ID {config[CONF_DEVICE_ID]} not found"
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DATA_HANDLER, DOMAIN
|
||||
from .data_handler import CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler
|
||||
from .data_handler import ACCOUNT, NetatmoDataHandler
|
||||
|
||||
TO_REDACT = {
|
||||
"access_token",
|
||||
|
@ -45,8 +45,8 @@ async def async_get_config_entry_diagnostics(
|
|||
TO_REDACT,
|
||||
),
|
||||
"data": {
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME: async_redact_data(
|
||||
getattr(data_handler.data[CLIMATE_TOPOLOGY_CLASS_NAME], "raw_data"),
|
||||
ACCOUNT: async_redact_data(
|
||||
getattr(data_handler.account, "raw_data"),
|
||||
TO_REDACT,
|
||||
)
|
||||
},
|
||||
|
|
|
@ -4,25 +4,25 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
import pyatmo
|
||||
from pyatmo import modules as NaModules
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DATA_HANDLER,
|
||||
CONF_URL_CONTROL,
|
||||
CONF_URL_SECURITY,
|
||||
DOMAIN,
|
||||
EVENT_TYPE_LIGHT_MODE,
|
||||
SIGNAL_NAME,
|
||||
TYPE_SECURITY,
|
||||
NETATMO_CREATE_CAMERA_LIGHT,
|
||||
NETATMO_CREATE_LIGHT,
|
||||
WEBHOOK_LIGHT_MODE,
|
||||
WEBHOOK_PUSH_TYPE,
|
||||
)
|
||||
from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -32,66 +32,73 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Netatmo camera light platform."""
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
|
||||
|
||||
if not data_class or data_class.raw_data == {}:
|
||||
raise PlatformNotReady
|
||||
@callback
|
||||
def _create_camera_light_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
if not hasattr(netatmo_device.device, "floodlight"):
|
||||
return
|
||||
|
||||
all_cameras = []
|
||||
for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values():
|
||||
for camera in home.values():
|
||||
all_cameras.append(camera)
|
||||
entity = NetatmoCameraLight(netatmo_device)
|
||||
async_add_entities([entity])
|
||||
|
||||
entities = [
|
||||
NetatmoLight(
|
||||
data_handler,
|
||||
camera["id"],
|
||||
camera["type"],
|
||||
camera["home_id"],
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, NETATMO_CREATE_CAMERA_LIGHT, _create_camera_light_entity
|
||||
)
|
||||
)
|
||||
for camera in all_cameras
|
||||
if camera["type"] == "NOC"
|
||||
]
|
||||
|
||||
_LOGGER.debug("Adding camera lights %s", entities)
|
||||
async_add_entities(entities, True)
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
if not hasattr(netatmo_device.device, "brightness"):
|
||||
return
|
||||
|
||||
entity = NetatmoLight(netatmo_device)
|
||||
_LOGGER.debug("Adding light %s", entity)
|
||||
async_add_entities([entity])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_LIGHT, _create_entity)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoLight(NetatmoBase, LightEntity):
|
||||
class NetatmoCameraLight(NetatmoBase, LightEntity):
|
||||
"""Representation of a Netatmo Presence camera light."""
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_has_entity_name = True
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_handler: NetatmoDataHandler,
|
||||
camera_id: str,
|
||||
camera_type: str,
|
||||
home_id: str,
|
||||
netatmo_device: NetatmoDevice,
|
||||
) -> None:
|
||||
"""Initialize a Netatmo Presence camera light."""
|
||||
LightEntity.__init__(self)
|
||||
super().__init__(data_handler)
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._publishers.append(
|
||||
{"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
|
||||
)
|
||||
self._id = camera_id
|
||||
self._home_id = home_id
|
||||
self._model = camera_type
|
||||
self._netatmo_type = TYPE_SECURITY
|
||||
self._device_name: str = self._data.get_camera(camera_id)["name"]
|
||||
self._camera = cast(NaModules.NOC, netatmo_device.device)
|
||||
self._id = self._camera.entity_id
|
||||
self._home_id = self._camera.home.entity_id
|
||||
self._device_name = self._camera.name
|
||||
self._model = self._camera.device_type
|
||||
self._config_url = CONF_URL_SECURITY
|
||||
self._is_on = False
|
||||
self._attr_unique_id = f"{self._id}-light"
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._camera.home.entity_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self.data_handler.config_entry.async_on_unload(
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}",
|
||||
|
@ -117,14 +124,6 @@ class NetatmoLight(NetatmoBase, LightEntity):
|
|||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
@property
|
||||
def _data(self) -> pyatmo.AsyncCameraData:
|
||||
"""Return data for this entity."""
|
||||
return cast(
|
||||
pyatmo.AsyncCameraData,
|
||||
self.data_handler.data[self._publishers[0]["name"]],
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""If the webhook is not established, mark as unavailable."""
|
||||
|
@ -138,22 +137,79 @@ class NetatmoLight(NetatmoBase, LightEntity):
|
|||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn camera floodlight on."""
|
||||
_LOGGER.debug("Turn camera '%s' on", self.name)
|
||||
await self._data.async_set_state(
|
||||
home_id=self._home_id,
|
||||
camera_id=self._id,
|
||||
floodlight="on",
|
||||
)
|
||||
await self._camera.async_floodlight_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn camera floodlight into auto mode."""
|
||||
_LOGGER.debug("Turn camera '%s' to auto mode", self.name)
|
||||
await self._data.async_set_state(
|
||||
home_id=self._home_id,
|
||||
camera_id=self._id,
|
||||
floodlight="auto",
|
||||
)
|
||||
await self._camera.async_floodlight_auto()
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
self._is_on = bool(self._data.get_light_state(self._id) == "on")
|
||||
self._is_on = bool(self._camera.floodlight == "on")
|
||||
|
||||
|
||||
class NetatmoLight(NetatmoBase, LightEntity):
|
||||
"""Representation of a dimmable light by Legrand/BTicino."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
netatmo_device: NetatmoDevice,
|
||||
) -> None:
|
||||
"""Initialize a Netatmo light."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._dimmer = cast(NaModules.NLFN, netatmo_device.device)
|
||||
self._id = self._dimmer.entity_id
|
||||
self._home_id = self._dimmer.home.entity_id
|
||||
self._device_name = self._dimmer.name
|
||||
self._attr_name = f"{self._device_name}"
|
||||
self._model = self._dimmer.device_type
|
||||
self._config_url = CONF_URL_CONTROL
|
||||
self._attr_brightness = 0
|
||||
self._attr_unique_id = f"{self._id}-light"
|
||||
|
||||
self._attr_supported_color_modes: set[str] = set()
|
||||
|
||||
if not self._attr_supported_color_modes and self._dimmer.brightness is not None:
|
||||
self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._dimmer.home.entity_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return self._dimmer.on is True
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn light on."""
|
||||
_LOGGER.debug("Turn light '%s' on", self.name)
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
else:
|
||||
await self._dimmer.async_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn light off."""
|
||||
_LOGGER.debug("Turn light '%s' off", self.name)
|
||||
await self._dimmer.async_off()
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
if self._dimmer.brightness is not None:
|
||||
# Netatmo uses a range of [0, 100] to control brightness
|
||||
self._attr_brightness = round((self._dimmer.brightness / 100) * 255)
|
||||
else:
|
||||
self._attr_brightness = None
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "netatmo",
|
||||
"name": "Netatmo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||
"requirements": ["pyatmo==6.2.4"],
|
||||
"requirements": ["pyatmo==7.0.1"],
|
||||
"after_dependencies": ["cloud", "media_source"],
|
||||
"dependencies": ["application_credentials", "webhook"],
|
||||
"codeowners": ["@cgtobi"],
|
||||
|
|
|
@ -72,21 +72,13 @@ class NetatmoSource(MediaSource):
|
|||
self, source: str, camera_id: str, event_id: int | None = None
|
||||
) -> BrowseMediaSource:
|
||||
if event_id and event_id in self.events[camera_id]:
|
||||
created = dt.datetime.fromtimestamp(event_id)
|
||||
if self.events[camera_id][event_id]["type"] == "outdoor":
|
||||
thumbnail = (
|
||||
self.events[camera_id][event_id]["event_list"][0]
|
||||
.get("snapshot", {})
|
||||
.get("url")
|
||||
created = dt.datetime.fromtimestamp(
|
||||
self.events[camera_id][event_id]["event_time"]
|
||||
)
|
||||
thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
|
||||
message = remove_html_tags(
|
||||
self.events[camera_id][event_id]["event_list"][0]["message"]
|
||||
self.events[camera_id][event_id].get("message", "")
|
||||
)
|
||||
else:
|
||||
thumbnail = (
|
||||
self.events[camera_id][event_id].get("snapshot", {}).get("url")
|
||||
)
|
||||
message = remove_html_tags(self.events[camera_id][event_id]["message"])
|
||||
title = f"{created} - {message}"
|
||||
else:
|
||||
title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
|
||||
|
|
|
@ -3,20 +3,18 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from pyatmo.modules.device_types import (
|
||||
DEVICE_DESCRIPTION_MAP,
|
||||
DeviceType as NetatmoDeviceType,
|
||||
)
|
||||
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import (
|
||||
DATA_DEVICE_IDS,
|
||||
DEFAULT_ATTRIBUTION,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
MODELS,
|
||||
SIGNAL_NAME,
|
||||
)
|
||||
from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler
|
||||
from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME
|
||||
from .data_handler import PUBLIC, NetatmoDataHandler
|
||||
|
||||
|
||||
class NetatmoBase(Entity):
|
||||
|
@ -30,38 +28,38 @@ class NetatmoBase(Entity):
|
|||
self._device_name: str = ""
|
||||
self._id: str = ""
|
||||
self._model: str = ""
|
||||
self._netatmo_type: str = ""
|
||||
self._config_url: str = ""
|
||||
self._attr_name = None
|
||||
self._attr_unique_id = None
|
||||
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
for data_class in self._publishers:
|
||||
signal_name = data_class[SIGNAL_NAME]
|
||||
for publisher in self._publishers:
|
||||
signal_name = publisher[SIGNAL_NAME]
|
||||
|
||||
if "home_id" in data_class:
|
||||
if "home_id" in publisher:
|
||||
await self.data_handler.subscribe(
|
||||
data_class["name"],
|
||||
publisher["name"],
|
||||
signal_name,
|
||||
self.async_update_callback,
|
||||
home_id=data_class["home_id"],
|
||||
home_id=publisher["home_id"],
|
||||
)
|
||||
|
||||
elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME:
|
||||
elif publisher["name"] == PUBLIC:
|
||||
await self.data_handler.subscribe(
|
||||
data_class["name"],
|
||||
publisher["name"],
|
||||
signal_name,
|
||||
self.async_update_callback,
|
||||
lat_ne=data_class["lat_ne"],
|
||||
lon_ne=data_class["lon_ne"],
|
||||
lat_sw=data_class["lat_sw"],
|
||||
lon_sw=data_class["lon_sw"],
|
||||
lat_ne=publisher["lat_ne"],
|
||||
lon_ne=publisher["lon_ne"],
|
||||
lat_sw=publisher["lat_sw"],
|
||||
lon_sw=publisher["lon_sw"],
|
||||
)
|
||||
|
||||
else:
|
||||
await self.data_handler.subscribe(
|
||||
data_class["name"], signal_name, self.async_update_callback
|
||||
publisher["name"], signal_name, self.async_update_callback
|
||||
)
|
||||
|
||||
for sub in self.data_handler.publisher[signal_name].subscriptions:
|
||||
|
@ -78,9 +76,9 @@ class NetatmoBase(Entity):
|
|||
"""Run when entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
for data_class in self._publishers:
|
||||
for publisher in self._publishers:
|
||||
await self.data_handler.unsubscribe(
|
||||
data_class[SIGNAL_NAME], self.async_update_callback
|
||||
publisher[SIGNAL_NAME], self.async_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@ -91,10 +89,13 @@ class NetatmoBase(Entity):
|
|||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device info for the sensor."""
|
||||
manufacturer, model = DEVICE_DESCRIPTION_MAP[
|
||||
getattr(NetatmoDeviceType, self._model)
|
||||
]
|
||||
return DeviceInfo(
|
||||
configuration_url=f"https://my.netatmo.com/app/{self._netatmo_type}",
|
||||
configuration_url=self._config_url,
|
||||
identifiers={(DOMAIN, self._id)},
|
||||
name=self._device_name,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=MODELS[self._model],
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
)
|
||||
|
|
|
@ -3,29 +3,20 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
import pyatmo
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DATA_HANDLER,
|
||||
CONF_URL_ENERGY,
|
||||
DATA_SCHEDULES,
|
||||
DOMAIN,
|
||||
EVENT_TYPE_SCHEDULE,
|
||||
MANUFACTURER,
|
||||
SIGNAL_NAME,
|
||||
TYPE_ENERGY,
|
||||
)
|
||||
from .data_handler import (
|
||||
CLIMATE_STATE_CLASS_NAME,
|
||||
CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
NetatmoDataHandler,
|
||||
NETATMO_CREATE_SELECT,
|
||||
)
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoHome
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -35,97 +26,63 @@ async def async_setup_entry(
|
|||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Netatmo energy platform schedule selector."""
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
|
||||
climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME)
|
||||
@callback
|
||||
def _create_entity(netatmo_home: NetatmoHome) -> None:
|
||||
entity = NetatmoScheduleSelect(netatmo_home)
|
||||
async_add_entities([entity])
|
||||
|
||||
if not climate_topology or climate_topology.raw_data == {}:
|
||||
raise PlatformNotReady
|
||||
|
||||
entities = []
|
||||
for home_id in climate_topology.home_ids:
|
||||
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
|
||||
|
||||
await data_handler.subscribe(
|
||||
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_SELECT, _create_entity)
|
||||
)
|
||||
|
||||
if (climate_state := data_handler.data[signal_name]) is None:
|
||||
continue
|
||||
|
||||
climate_topology.register_handler(home_id, climate_state.process_topology)
|
||||
|
||||
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[
|
||||
home_id
|
||||
].schedules
|
||||
|
||||
entities = [
|
||||
NetatmoScheduleSelect(
|
||||
data_handler,
|
||||
home_id,
|
||||
[schedule.name for schedule in schedules.values()],
|
||||
)
|
||||
for home_id, schedules in hass.data[DOMAIN][DATA_SCHEDULES].items()
|
||||
if schedules
|
||||
]
|
||||
|
||||
_LOGGER.debug("Adding climate schedule select entities %s", entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
|
||||
"""Representation a Netatmo thermostat schedule selector."""
|
||||
|
||||
def __init__(
|
||||
self, data_handler: NetatmoDataHandler, home_id: str, options: list
|
||||
self,
|
||||
netatmo_home: NetatmoHome,
|
||||
) -> None:
|
||||
"""Initialize the select entity."""
|
||||
SelectEntity.__init__(self)
|
||||
super().__init__(data_handler)
|
||||
super().__init__(netatmo_home.data_handler)
|
||||
|
||||
self._home_id = home_id
|
||||
|
||||
self._climate_state_class = f"{CLIMATE_STATE_CLASS_NAME}-{self._home_id}"
|
||||
self._climate_state: pyatmo.AsyncClimate = data_handler.data[
|
||||
self._climate_state_class
|
||||
]
|
||||
|
||||
self._home = self._climate_state.homes[self._home_id]
|
||||
self._home = netatmo_home.home
|
||||
self._home_id = self._home.entity_id
|
||||
|
||||
self._signal_name = netatmo_home.signal_name
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME,
|
||||
},
|
||||
{
|
||||
"name": CLIMATE_STATE_CLASS_NAME,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: self._climate_state_class,
|
||||
"name": HOME,
|
||||
"home_id": self._home.entity_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._device_name = self._home.name
|
||||
self._attr_name = f"{MANUFACTURER} {self._device_name}"
|
||||
self._attr_name = f"{self._device_name}"
|
||||
|
||||
self._model: str = "NATherm1"
|
||||
self._netatmo_type = TYPE_ENERGY
|
||||
self._config_url = CONF_URL_ENERGY
|
||||
|
||||
self._attr_unique_id = f"{self._home_id}-schedule-select"
|
||||
|
||||
self._attr_current_option = getattr(self._home.get_selected_schedule(), "name")
|
||||
self._attr_options = options
|
||||
self._attr_options = [
|
||||
schedule.name for schedule in self._home.schedules.values()
|
||||
]
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
for event_type in (EVENT_TYPE_SCHEDULE,):
|
||||
self.data_handler.config_entry.async_on_unload(
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"signal-{DOMAIN}-webhook-{event_type}",
|
||||
f"signal-{DOMAIN}-webhook-{EVENT_TYPE_SCHEDULE}",
|
||||
self.handle_event,
|
||||
)
|
||||
)
|
||||
|
@ -160,7 +117,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
|
|||
option,
|
||||
sid,
|
||||
)
|
||||
await self._climate_state.async_switch_home_schedule(schedule_id=sid)
|
||||
await self._home.async_switch_schedule(schedule_id=sid)
|
||||
break
|
||||
|
||||
@callback
|
||||
|
@ -169,8 +126,5 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
|
|||
self._attr_current_option = getattr(self._home.get_selected_schedule(), "name")
|
||||
self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules
|
||||
self._attr_options = [
|
||||
schedule.name
|
||||
for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][
|
||||
self._home_id
|
||||
].values()
|
||||
schedule.name for schedule in self._home.schedules.values()
|
||||
]
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""Support for the Netatmo Weather Service."""
|
||||
"""Support for the Netatmo sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import NamedTuple, cast
|
||||
from typing import cast
|
||||
|
||||
import pyatmo
|
||||
|
||||
|
@ -21,15 +21,15 @@ from homeassistant.const import (
|
|||
DEGREE,
|
||||
LENGTH_MILLIMETERS,
|
||||
PERCENTAGE,
|
||||
POWER_WATT,
|
||||
PRESSURE_MBAR,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
SOUND_PRESSURE_DB,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import async_entries_for_config_entry
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
|
@ -38,20 +38,18 @@ from homeassistant.helpers.entity import EntityCategory
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_URL_ENERGY,
|
||||
CONF_URL_WEATHER,
|
||||
CONF_WEATHER_AREAS,
|
||||
DATA_HANDLER,
|
||||
DOMAIN,
|
||||
NETATMO_CREATE_BATTERY,
|
||||
NETATMO_CREATE_ROOM_SENSOR,
|
||||
NETATMO_CREATE_SENSOR,
|
||||
NETATMO_CREATE_WEATHER_SENSOR,
|
||||
SIGNAL_NAME,
|
||||
TYPE_WEATHER,
|
||||
)
|
||||
from .data_handler import (
|
||||
HOMECOACH_DATA_CLASS_NAME,
|
||||
PUBLICDATA_DATA_CLASS_NAME,
|
||||
WEATHERSTATION_DATA_CLASS_NAME,
|
||||
NetatmoDataHandler,
|
||||
NetatmoDevice,
|
||||
)
|
||||
from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom
|
||||
from .helper import NetatmoArea
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
|
@ -62,10 +60,12 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = (
|
|||
"pressure",
|
||||
"humidity",
|
||||
"rain",
|
||||
"windstrength",
|
||||
"guststrength",
|
||||
"wind_strength",
|
||||
"gust_strength",
|
||||
"sum_rain_1",
|
||||
"sum_rain_24",
|
||||
"wind_angle",
|
||||
"gust_angle",
|
||||
)
|
||||
|
||||
|
||||
|
@ -85,7 +85,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="temperature",
|
||||
name="Temperature",
|
||||
netatmo_name="Temperature",
|
||||
netatmo_name="temperature",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@ -101,7 +101,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="co2",
|
||||
name="CO2",
|
||||
netatmo_name="CO2",
|
||||
netatmo_name="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
entity_registry_enabled_default=True,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@ -110,7 +110,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="pressure",
|
||||
name="Pressure",
|
||||
netatmo_name="Pressure",
|
||||
netatmo_name="pressure",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=PRESSURE_MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="noise",
|
||||
name="Noise",
|
||||
netatmo_name="Noise",
|
||||
netatmo_name="noise",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=SOUND_PRESSURE_DB,
|
||||
icon="mdi:volume-high",
|
||||
|
@ -135,7 +135,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="humidity",
|
||||
name="Humidity",
|
||||
netatmo_name="Humidity",
|
||||
netatmo_name="humidity",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="rain",
|
||||
name="Rain",
|
||||
netatmo_name="Rain",
|
||||
netatmo_name="rain",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=LENGTH_MILLIMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@ -156,7 +156,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
netatmo_name="sum_rain_1",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=LENGTH_MILLIMETERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:weather-rainy",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
|
@ -171,7 +171,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="battery_percent",
|
||||
name="Battery Percent",
|
||||
netatmo_name="battery_percent",
|
||||
netatmo_name="battery",
|
||||
entity_registry_enabled_default=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
@ -181,14 +181,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="windangle",
|
||||
name="Direction",
|
||||
netatmo_name="WindAngle",
|
||||
netatmo_name="wind_direction",
|
||||
entity_registry_enabled_default=True,
|
||||
icon="mdi:compass-outline",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="windangle_value",
|
||||
name="Angle",
|
||||
netatmo_name="WindAngle",
|
||||
netatmo_name="wind_angle",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
|
@ -197,7 +197,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="windstrength",
|
||||
name="Wind Strength",
|
||||
netatmo_name="WindStrength",
|
||||
netatmo_name="wind_strength",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
icon="mdi:weather-windy",
|
||||
|
@ -206,14 +206,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="gustangle",
|
||||
name="Gust Direction",
|
||||
netatmo_name="GustAngle",
|
||||
netatmo_name="gust_direction",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:compass-outline",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="gustangle_value",
|
||||
name="Gust Angle",
|
||||
netatmo_name="GustAngle",
|
||||
netatmo_name="gust_angle",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
|
@ -222,7 +222,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="guststrength",
|
||||
name="Gust Strength",
|
||||
netatmo_name="GustStrength",
|
||||
netatmo_name="gust_strength",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
|
||||
icon="mdi:weather-windy",
|
||||
|
@ -239,39 +239,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
NetatmoSensorEntityDescription(
|
||||
key="rf_status",
|
||||
name="Radio",
|
||||
netatmo_name="rf_status",
|
||||
netatmo_name="rf_strength",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:signal",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="rf_status_lvl",
|
||||
name="Radio Level",
|
||||
netatmo_name="rf_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="wifi_status",
|
||||
name="Wifi",
|
||||
netatmo_name="wifi_status",
|
||||
netatmo_name="wifi_strength",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:wifi",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="wifi_status_lvl",
|
||||
name="Wifi Level",
|
||||
netatmo_name="wifi_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="health_idx",
|
||||
name="Health",
|
||||
|
@ -279,136 +259,110 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
|
|||
entity_registry_enabled_default=True,
|
||||
icon="mdi:cloud",
|
||||
),
|
||||
NetatmoSensorEntityDescription(
|
||||
key="power",
|
||||
name="Power",
|
||||
netatmo_name="power",
|
||||
entity_registry_enabled_default=True,
|
||||
native_unit_of_measurement=POWER_WATT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
)
|
||||
SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
MODULE_TYPE_OUTDOOR = "NAModule1"
|
||||
MODULE_TYPE_WIND = "NAModule2"
|
||||
MODULE_TYPE_RAIN = "NAModule3"
|
||||
MODULE_TYPE_INDOOR = "NAModule4"
|
||||
|
||||
|
||||
class BatteryData(NamedTuple):
|
||||
"""Metadata for a batter."""
|
||||
|
||||
full: int
|
||||
high: int
|
||||
medium: int
|
||||
low: int
|
||||
|
||||
|
||||
BATTERY_VALUES = {
|
||||
MODULE_TYPE_WIND: BatteryData(
|
||||
full=5590,
|
||||
high=5180,
|
||||
medium=4770,
|
||||
low=4360,
|
||||
),
|
||||
MODULE_TYPE_RAIN: BatteryData(
|
||||
full=5500,
|
||||
high=5000,
|
||||
medium=4500,
|
||||
low=4000,
|
||||
),
|
||||
MODULE_TYPE_INDOOR: BatteryData(
|
||||
full=5500,
|
||||
high=5280,
|
||||
medium=4920,
|
||||
low=4560,
|
||||
),
|
||||
MODULE_TYPE_OUTDOOR: BatteryData(
|
||||
full=5500,
|
||||
high=5000,
|
||||
medium=4500,
|
||||
low=4000,
|
||||
),
|
||||
}
|
||||
|
||||
PUBLIC = "public"
|
||||
BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription(
|
||||
key="battery",
|
||||
name="Battery Percent",
|
||||
netatmo_name="battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Netatmo weather and homecoach platform."""
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
platform_not_ready = True
|
||||
"""Set up the Netatmo sensor platform."""
|
||||
|
||||
async def find_entities(data_class_name: str) -> list:
|
||||
"""Find all entities."""
|
||||
all_module_infos = {}
|
||||
data = data_handler.data
|
||||
@callback
|
||||
def _create_battery_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
if not hasattr(netatmo_device.device, "battery"):
|
||||
return
|
||||
entity = NetatmoClimateBatterySensor(netatmo_device)
|
||||
async_add_entities([entity])
|
||||
|
||||
if data_class_name not in data:
|
||||
return []
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_battery_entity)
|
||||
)
|
||||
|
||||
if data[data_class_name] is None:
|
||||
return []
|
||||
|
||||
data_class = data[data_class_name]
|
||||
|
||||
for station_id in data_class.stations:
|
||||
for module_id in data_class.get_modules(station_id):
|
||||
all_module_infos[module_id] = data_class.get_module(module_id)
|
||||
|
||||
all_module_infos[station_id] = data_class.get_station(station_id)
|
||||
|
||||
entities = []
|
||||
for module in all_module_infos.values():
|
||||
if "_id" not in module:
|
||||
_LOGGER.debug("Skipping module %s", module.get("module_name"))
|
||||
continue
|
||||
|
||||
conditions = [
|
||||
c.lower()
|
||||
for c in data_class.get_monitored_conditions(module_id=module["_id"])
|
||||
if c.lower() in SENSOR_TYPES_KEYS
|
||||
]
|
||||
for condition in conditions:
|
||||
if f"{condition}_value" in SENSOR_TYPES_KEYS:
|
||||
conditions.append(f"{condition}_value")
|
||||
elif f"{condition}_lvl" in SENSOR_TYPES_KEYS:
|
||||
conditions.append(f"{condition}_lvl")
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
NetatmoSensor(data_handler, data_class_name, module, description)
|
||||
@callback
|
||||
def _create_weather_sensor_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
async_add_entities(
|
||||
NetatmoWeatherSensor(netatmo_device, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in conditions
|
||||
if description.netatmo_name in netatmo_device.device.features
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_sensor_entity
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _create_sensor_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
_LOGGER.debug(
|
||||
"Adding %s sensor %s",
|
||||
netatmo_device.device.device_category,
|
||||
netatmo_device.device.name,
|
||||
)
|
||||
async_add_entities(
|
||||
[
|
||||
NetatmoSensor(netatmo_device, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in netatmo_device.device.features
|
||||
]
|
||||
)
|
||||
|
||||
_LOGGER.debug("Adding weather sensors %s", entities)
|
||||
return entities
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_SENSOR, _create_sensor_entity)
|
||||
)
|
||||
|
||||
for data_class_name in (
|
||||
WEATHERSTATION_DATA_CLASS_NAME,
|
||||
HOMECOACH_DATA_CLASS_NAME,
|
||||
):
|
||||
data_class = data_handler.data.get(data_class_name)
|
||||
@callback
|
||||
def _create_room_sensor_entity(netatmo_device: NetatmoRoom) -> None:
|
||||
async_add_entities(
|
||||
NetatmoRoomSensor(netatmo_device, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in netatmo_device.room.features
|
||||
)
|
||||
|
||||
if data_class and data_class.raw_data:
|
||||
platform_not_ready = False
|
||||
|
||||
async_add_entities(await find_entities(data_class_name), True)
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, NETATMO_CREATE_ROOM_SENSOR, _create_room_sensor_entity
|
||||
)
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
|
||||
|
||||
async def add_public_entities(update: bool = True) -> None:
|
||||
"""Retrieve Netatmo public weather entities."""
|
||||
entities = {
|
||||
device.name: device.id
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
for device in async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
)
|
||||
if device.model == "Public Weather stations"
|
||||
if device.model == "Public Weather station"
|
||||
}
|
||||
|
||||
new_entities = []
|
||||
for area in [
|
||||
NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values()
|
||||
]:
|
||||
signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
|
||||
signal_name = f"{PUBLIC}-{area.uuid}"
|
||||
|
||||
if area.area_name in entities:
|
||||
entities.pop(area.area_name)
|
||||
|
@ -422,25 +376,21 @@ async def async_setup_entry(
|
|||
continue
|
||||
|
||||
await data_handler.subscribe(
|
||||
PUBLICDATA_DATA_CLASS_NAME,
|
||||
PUBLIC,
|
||||
signal_name,
|
||||
None,
|
||||
lat_ne=area.lat_ne,
|
||||
lon_ne=area.lon_ne,
|
||||
lat_sw=area.lat_sw,
|
||||
lon_sw=area.lon_sw,
|
||||
area_id=str(area.uuid),
|
||||
)
|
||||
data_class = data_handler.data.get(signal_name)
|
||||
|
||||
if data_class and data_class.raw_data:
|
||||
nonlocal platform_not_ready
|
||||
platform_not_ready = False
|
||||
|
||||
new_entities.extend(
|
||||
[
|
||||
NetatmoPublicSensor(data_handler, area, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in SUPPORTED_PUBLIC_SENSOR_TYPES
|
||||
if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -454,67 +404,55 @@ async def async_setup_entry(
|
|||
hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities
|
||||
)
|
||||
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
entity = NetatmoClimateBatterySensor(netatmo_device)
|
||||
_LOGGER.debug("Adding climate battery sensor %s", entity)
|
||||
async_add_entities([entity])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity)
|
||||
)
|
||||
|
||||
await add_public_entities(False)
|
||||
|
||||
if platform_not_ready:
|
||||
raise PlatformNotReady
|
||||
|
||||
class NetatmoWeatherSensor(NetatmoBase, SensorEntity):
|
||||
"""Implementation of a Netatmo weather/home coach sensor."""
|
||||
|
||||
class NetatmoSensor(NetatmoBase, SensorEntity):
|
||||
"""Implementation of a Netatmo sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NetatmoSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_handler: NetatmoDataHandler,
|
||||
data_class_name: str,
|
||||
module_info: dict,
|
||||
netatmo_device: NetatmoDevice,
|
||||
description: NetatmoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data_handler)
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
self.entity_description = description
|
||||
|
||||
self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name})
|
||||
|
||||
self._id = module_info["_id"]
|
||||
self._station_id = module_info.get("main_device", self._id)
|
||||
|
||||
station = self._data.get_station(self._station_id)
|
||||
if not (device := self._data.get_module(self._id)):
|
||||
# Assume it's a station if module can't be found
|
||||
device = station
|
||||
|
||||
if device["type"] in ("NHC", "NAMain"):
|
||||
self._device_name = module_info["station_name"]
|
||||
else:
|
||||
self._device_name = (
|
||||
f"{station['station_name']} "
|
||||
f"{module_info.get('module_name', device['type'])}"
|
||||
self._module = netatmo_device.device
|
||||
self._id = self._module.entity_id
|
||||
self._station_id = (
|
||||
self._module.bridge if self._module.bridge is not None else self._id
|
||||
)
|
||||
self._device_name = self._module.name
|
||||
category = getattr(self._module.device_category, "name")
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": category,
|
||||
SIGNAL_NAME: category,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._attr_name = f"{self._device_name} {description.name}"
|
||||
self._model = device["type"]
|
||||
self._netatmo_type = TYPE_WEATHER
|
||||
self._attr_name = f"{description.name}"
|
||||
self._model = self._module.device_type
|
||||
self._config_url = CONF_URL_WEATHER
|
||||
self._attr_unique_id = f"{self._id}-{description.key}"
|
||||
|
||||
@property
|
||||
def _data(self) -> pyatmo.AsyncWeatherStationData:
|
||||
"""Return data for this entity."""
|
||||
return cast(
|
||||
pyatmo.AsyncWeatherStationData,
|
||||
self.data_handler.data[self._publishers[0]["name"]],
|
||||
if hasattr(self._module, "place"):
|
||||
place = cast(
|
||||
pyatmo.modules.base_class.Place, getattr(self._module, "place")
|
||||
)
|
||||
if hasattr(place, "location") and place.location is not None:
|
||||
self._attr_extra_state_attributes.update(
|
||||
{
|
||||
ATTR_LATITUDE: place.location.latitude,
|
||||
ATTR_LONGITUDE: place.location.longitude,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -525,46 +463,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
|
|||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get(
|
||||
self._id
|
||||
)
|
||||
|
||||
if data is None:
|
||||
if self.state:
|
||||
_LOGGER.debug(
|
||||
"No data found for %s - %s (%s)",
|
||||
self.name,
|
||||
self._device_name,
|
||||
self._id,
|
||||
)
|
||||
self._attr_native_value = None
|
||||
if (
|
||||
state := getattr(self._module, self.entity_description.netatmo_name)
|
||||
) is None:
|
||||
return
|
||||
|
||||
try:
|
||||
state = data[self.entity_description.netatmo_name]
|
||||
if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}:
|
||||
if self.entity_description.netatmo_name in {
|
||||
"temperature",
|
||||
"pressure",
|
||||
"sum_rain_1",
|
||||
}:
|
||||
self._attr_native_value = round(state, 1)
|
||||
elif self.entity_description.key in {"windangle_value", "gustangle_value"}:
|
||||
self._attr_native_value = fix_angle(state)
|
||||
elif self.entity_description.key in {"windangle", "gustangle"}:
|
||||
self._attr_native_value = process_angle(fix_angle(state))
|
||||
elif self.entity_description.key == "rf_status":
|
||||
elif self.entity_description.netatmo_name == "rf_strength":
|
||||
self._attr_native_value = process_rf(state)
|
||||
elif self.entity_description.key == "wifi_status":
|
||||
elif self.entity_description.netatmo_name == "wifi_strength":
|
||||
self._attr_native_value = process_wifi(state)
|
||||
elif self.entity_description.key == "health_idx":
|
||||
elif self.entity_description.netatmo_name == "health_idx":
|
||||
self._attr_native_value = process_health(state)
|
||||
else:
|
||||
self._attr_native_value = state
|
||||
except KeyError:
|
||||
if self.state:
|
||||
_LOGGER.debug(
|
||||
"No %s data found for %s",
|
||||
self.entity_description.key,
|
||||
self._device_name,
|
||||
)
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@ -580,24 +497,25 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
|
|||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
self.entity_description = NetatmoSensorEntityDescription(
|
||||
key="battery_percent",
|
||||
name="Battery Percent",
|
||||
netatmo_name="battery_percent",
|
||||
entity_registry_enabled_default=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
self.entity_description = BATTERY_SENSOR_DESCRIPTION
|
||||
|
||||
self._module = cast(pyatmo.modules.NRV, netatmo_device.device)
|
||||
self._id = netatmo_device.parent_id
|
||||
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": netatmo_device.device.home.entity_id,
|
||||
SIGNAL_NAME: netatmo_device.signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._module = netatmo_device.device
|
||||
self._id = netatmo_device.parent_id
|
||||
self._attr_name = f"{self._module.name} {self.entity_description.name}"
|
||||
|
||||
self._signal_name = netatmo_device.signal_name
|
||||
self._room_id = self._module.room_id
|
||||
self._model = getattr(self._module.device_type, "value")
|
||||
self._config_url = CONF_URL_ENERGY
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._id}-{self._module.entity_id}-{self.entity_description.key}"
|
||||
|
@ -613,70 +531,54 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
|
|||
return
|
||||
|
||||
self._attr_available = True
|
||||
self._attr_native_value = self._process_battery_state()
|
||||
|
||||
def _process_battery_state(self) -> int | None:
|
||||
"""Construct room status."""
|
||||
if battery_state := self._module.battery_state:
|
||||
return process_battery_percentage(battery_state)
|
||||
|
||||
return None
|
||||
self._attr_native_value = self._module.battery
|
||||
|
||||
|
||||
def process_battery_percentage(data: str) -> int:
|
||||
"""Process battery data and return percent (int) for display."""
|
||||
mapping = {
|
||||
"max": 100,
|
||||
"full": 90,
|
||||
"high": 75,
|
||||
"medium": 50,
|
||||
"low": 25,
|
||||
"very low": 10,
|
||||
}
|
||||
return mapping[data]
|
||||
class NetatmoSensor(NetatmoBase, SensorEntity):
|
||||
"""Implementation of a Netatmo sensor."""
|
||||
|
||||
entity_description: NetatmoSensorEntityDescription
|
||||
|
||||
def fix_angle(angle: int) -> int:
|
||||
"""Fix angle when value is negative."""
|
||||
if angle < 0:
|
||||
return 360 + angle
|
||||
return angle
|
||||
def __init__(
|
||||
self,
|
||||
netatmo_device: NetatmoDevice,
|
||||
description: NetatmoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
self.entity_description = description
|
||||
|
||||
self._module = netatmo_device.device
|
||||
self._id = self._module.entity_id
|
||||
|
||||
def process_angle(angle: int) -> str:
|
||||
"""Process angle and return string for display."""
|
||||
if angle >= 330:
|
||||
return "N"
|
||||
if angle >= 300:
|
||||
return "NW"
|
||||
if angle >= 240:
|
||||
return "W"
|
||||
if angle >= 210:
|
||||
return "SW"
|
||||
if angle >= 150:
|
||||
return "S"
|
||||
if angle >= 120:
|
||||
return "SE"
|
||||
if angle >= 60:
|
||||
return "E"
|
||||
if angle >= 30:
|
||||
return "NE"
|
||||
return "N"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": netatmo_device.device.home.entity_id,
|
||||
SIGNAL_NAME: netatmo_device.signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._attr_name = f"{self._module.name} {self.entity_description.name}"
|
||||
self._room_id = self._module.room_id
|
||||
self._model = getattr(self._module.device_type, "value")
|
||||
self._config_url = CONF_URL_ENERGY
|
||||
|
||||
def process_battery(data: int, model: str) -> str:
|
||||
"""Process battery data and return string for display."""
|
||||
battery_data = BATTERY_VALUES[model]
|
||||
self._attr_unique_id = (
|
||||
f"{self._id}-{self._module.entity_id}-{self.entity_description.key}"
|
||||
)
|
||||
|
||||
if data >= battery_data.full:
|
||||
return "Full"
|
||||
if data >= battery_data.high:
|
||||
return "High"
|
||||
if data >= battery_data.medium:
|
||||
return "Medium"
|
||||
if data >= battery_data.low:
|
||||
return "Low"
|
||||
return "Very Low"
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
if (state := getattr(self._module, self.entity_description.key)) is None:
|
||||
return
|
||||
|
||||
self._attr_native_value = state
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
def process_health(health: int) -> str:
|
||||
|
@ -714,9 +616,57 @@ def process_wifi(strength: int) -> str:
|
|||
return "Full"
|
||||
|
||||
|
||||
class NetatmoRoomSensor(NetatmoBase, SensorEntity):
|
||||
"""Implementation of a Netatmo room sensor."""
|
||||
|
||||
entity_description: NetatmoSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
netatmo_room: NetatmoRoom,
|
||||
description: NetatmoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(netatmo_room.data_handler)
|
||||
self.entity_description = description
|
||||
|
||||
self._room = netatmo_room.room
|
||||
self._id = self._room.entity_id
|
||||
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": netatmo_room.room.home.entity_id,
|
||||
SIGNAL_NAME: netatmo_room.signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self._attr_name = f"{self._room.name} {self.entity_description.name}"
|
||||
self._room_id = self._room.entity_id
|
||||
self._model = f"{self._room.climate_type}"
|
||||
self._config_url = CONF_URL_ENERGY
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._id}-{self._room.entity_id}-{self.entity_description.key}"
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
if (state := getattr(self._room, self.entity_description.key)) is None:
|
||||
return
|
||||
|
||||
self._attr_native_value = state
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
||||
"""Represent a single sensor in a Netatmo."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NetatmoSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
|
@ -729,11 +679,10 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
|||
super().__init__(data_handler)
|
||||
self.entity_description = description
|
||||
|
||||
self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
|
||||
|
||||
self._signal_name = f"{PUBLIC}-{area.uuid}"
|
||||
self._publishers.append(
|
||||
{
|
||||
"name": PUBLICDATA_DATA_CLASS_NAME,
|
||||
"name": PUBLIC,
|
||||
"lat_ne": area.lat_ne,
|
||||
"lon_ne": area.lon_ne,
|
||||
"lat_sw": area.lat_sw,
|
||||
|
@ -743,12 +692,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
|||
}
|
||||
)
|
||||
|
||||
self._station = data_handler.account.public_weather_areas[str(area.uuid)]
|
||||
|
||||
self.area = area
|
||||
self._mode = area.mode
|
||||
self._area_name = area.area_name
|
||||
self._id = self._area_name
|
||||
self._device_name = f"{self._area_name}"
|
||||
self._attr_name = f"{self._device_name} {description.name}"
|
||||
self._attr_name = f"{description.name}"
|
||||
self._show_on_map = area.show_on_map
|
||||
self._attr_unique_id = (
|
||||
f"{self._device_name.replace(' ', '-')}-{description.key}"
|
||||
|
@ -762,17 +713,12 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
|||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def _data(self) -> pyatmo.AsyncPublicData:
|
||||
"""Return data for this entity."""
|
||||
return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name])
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
assert self.device_info and "name" in self.device_info
|
||||
self.data_handler.config_entry.async_on_unload(
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"netatmo-config-{self.device_info['name']}",
|
||||
|
@ -790,22 +736,11 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
|||
)
|
||||
|
||||
self.area = area
|
||||
self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
|
||||
self._publishers = [
|
||||
{
|
||||
"name": PUBLICDATA_DATA_CLASS_NAME,
|
||||
"lat_ne": area.lat_ne,
|
||||
"lon_ne": area.lon_ne,
|
||||
"lat_sw": area.lat_sw,
|
||||
"lon_sw": area.lon_sw,
|
||||
"area_name": area.area_name,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
}
|
||||
]
|
||||
self._signal_name = f"{PUBLIC}-{area.uuid}"
|
||||
self._mode = area.mode
|
||||
self._show_on_map = area.show_on_map
|
||||
await self.data_handler.subscribe(
|
||||
PUBLICDATA_DATA_CLASS_NAME,
|
||||
PUBLIC,
|
||||
self._signal_name,
|
||||
self.async_update_callback,
|
||||
lat_ne=area.lat_ne,
|
||||
|
@ -819,22 +754,26 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
|
|||
"""Update the entity's state."""
|
||||
data = None
|
||||
|
||||
if self.entity_description.key == "temperature":
|
||||
data = self._data.get_latest_temperatures()
|
||||
elif self.entity_description.key == "pressure":
|
||||
data = self._data.get_latest_pressures()
|
||||
elif self.entity_description.key == "humidity":
|
||||
data = self._data.get_latest_humidities()
|
||||
elif self.entity_description.key == "rain":
|
||||
data = self._data.get_latest_rain()
|
||||
elif self.entity_description.key == "sum_rain_1":
|
||||
data = self._data.get_60_min_rain()
|
||||
elif self.entity_description.key == "sum_rain_24":
|
||||
data = self._data.get_24_h_rain()
|
||||
elif self.entity_description.key == "windstrength":
|
||||
data = self._data.get_latest_wind_strengths()
|
||||
elif self.entity_description.key == "guststrength":
|
||||
data = self._data.get_latest_gust_strengths()
|
||||
if self.entity_description.netatmo_name == "temperature":
|
||||
data = self._station.get_latest_temperatures()
|
||||
elif self.entity_description.netatmo_name == "pressure":
|
||||
data = self._station.get_latest_pressures()
|
||||
elif self.entity_description.netatmo_name == "humidity":
|
||||
data = self._station.get_latest_humidities()
|
||||
elif self.entity_description.netatmo_name == "rain":
|
||||
data = self._station.get_latest_rain()
|
||||
elif self.entity_description.netatmo_name == "sum_rain_1":
|
||||
data = self._station.get_60_min_rain()
|
||||
elif self.entity_description.netatmo_name == "sum_rain_24":
|
||||
data = self._station.get_24_h_rain()
|
||||
elif self.entity_description.netatmo_name == "wind_strength":
|
||||
data = self._station.get_latest_wind_strengths()
|
||||
elif self.entity_description.netatmo_name == "gust_strength":
|
||||
data = self._station.get_latest_gust_strengths()
|
||||
elif self.entity_description.netatmo_name == "wind_angle":
|
||||
data = self._station.get_latest_wind_angles()
|
||||
elif self.entity_description.netatmo_name == "gust_angle":
|
||||
data = self._station.get_latest_gust_angles()
|
||||
|
||||
if not data:
|
||||
if self.available:
|
||||
|
|
83
homeassistant/components/netatmo/switch.py
Normal file
83
homeassistant/components/netatmo/switch.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""Support for Netatmo/BTicino/Legrande switches."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pyatmo import modules as NaModules
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH
|
||||
from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
|
||||
from .netatmo_entity_base import NetatmoBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Netatmo switch platform."""
|
||||
|
||||
@callback
|
||||
def _create_entity(netatmo_device: NetatmoDevice) -> None:
|
||||
entity = NetatmoSwitch(netatmo_device)
|
||||
_LOGGER.debug("Adding switch %s", entity)
|
||||
async_add_entities([entity])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, NETATMO_CREATE_SWITCH, _create_entity)
|
||||
)
|
||||
|
||||
|
||||
class NetatmoSwitch(NetatmoBase, SwitchEntity):
|
||||
"""Representation of a Netatmo switch device."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
netatmo_device: NetatmoDevice,
|
||||
) -> None:
|
||||
"""Initialize the Netatmo device."""
|
||||
super().__init__(netatmo_device.data_handler)
|
||||
|
||||
self._switch = cast(NaModules.Switch, netatmo_device.device)
|
||||
|
||||
self._id = self._switch.entity_id
|
||||
self._attr_name = self._device_name = self._switch.name
|
||||
self._model = self._switch.device_type
|
||||
self._config_url = CONF_URL_CONTROL
|
||||
|
||||
self._home_id = self._switch.home.entity_id
|
||||
|
||||
self._signal_name = f"{HOME}-{self._home_id}"
|
||||
self._publishers.extend(
|
||||
[
|
||||
{
|
||||
"name": HOME,
|
||||
"home_id": self._home_id,
|
||||
SIGNAL_NAME: self._signal_name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self._attr_unique_id = f"{self._id}-{self._model}"
|
||||
self._attr_is_on = self._switch.on
|
||||
|
||||
@callback
|
||||
def async_update_callback(self) -> None:
|
||||
"""Update the entity's state."""
|
||||
self._attr_is_on = self._switch.on
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone on."""
|
||||
await self._switch.async_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the zone off."""
|
||||
await self._switch.async_off()
|
|
@ -1436,7 +1436,7 @@ pyalmond==0.0.2
|
|||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==6.2.4
|
||||
pyatmo==7.0.1
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyatome==0.1.1
|
||||
|
|
|
@ -1018,7 +1018,7 @@ pyalmond==0.0.2
|
|||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==6.2.4
|
||||
pyatmo==7.0.1
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.10.3
|
||||
|
|
|
@ -11,19 +11,6 @@ from tests.test_util.aiohttp import AiohttpClientMockResponse
|
|||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
ALL_SCOPES = [
|
||||
"read_station",
|
||||
"read_camera",
|
||||
"access_camera",
|
||||
"write_camera",
|
||||
"read_presence",
|
||||
"access_presence",
|
||||
"write_presence",
|
||||
"read_homecoach",
|
||||
"read_smokedetector",
|
||||
"read_thermostat",
|
||||
"write_thermostat",
|
||||
]
|
||||
|
||||
COMMON_RESPONSE = {
|
||||
"user_id": "91763b24c43d3e344f424e8d",
|
||||
|
@ -43,10 +30,10 @@ DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"]
|
|||
|
||||
async def fake_post_request(*args, **kwargs):
|
||||
"""Return fake data."""
|
||||
if "url" not in kwargs:
|
||||
if "endpoint" not in kwargs:
|
||||
return "{}"
|
||||
|
||||
endpoint = kwargs["url"].split("/")[-1]
|
||||
endpoint = kwargs["endpoint"].split("/")[-1]
|
||||
|
||||
if endpoint in "snapshot_720.jpg":
|
||||
return b"test stream image bytes"
|
||||
|
@ -59,7 +46,7 @@ async def fake_post_request(*args, **kwargs):
|
|||
"setthermmode",
|
||||
"switchhomeschedule",
|
||||
]:
|
||||
payload = f'{{"{endpoint}": true}}'
|
||||
payload = {f"{endpoint}": True, "status": "ok"}
|
||||
|
||||
elif endpoint == "homestatus":
|
||||
home_id = kwargs.get("params", {}).get("home_id")
|
||||
|
@ -70,17 +57,17 @@ async def fake_post_request(*args, **kwargs):
|
|||
|
||||
return AiohttpClientMockResponse(
|
||||
method="POST",
|
||||
url=kwargs["url"],
|
||||
url=kwargs["endpoint"],
|
||||
json=payload,
|
||||
)
|
||||
|
||||
|
||||
async def fake_get_image(*args, **kwargs):
|
||||
"""Return fake data."""
|
||||
if "url" not in kwargs:
|
||||
if "endpoint" not in kwargs:
|
||||
return "{}"
|
||||
|
||||
endpoint = kwargs["url"].split("/")[-1]
|
||||
endpoint = kwargs["endpoint"].split("/")[-1]
|
||||
|
||||
if endpoint in "snapshot_720.jpg":
|
||||
return b"test stream image bytes"
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
from time import time
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyatmo.const import ALL_SCOPES
|
||||
import pytest
|
||||
|
||||
from .common import ALL_SCOPES, fake_get_image, fake_post_request
|
||||
from .common import fake_get_image, fake_post_request
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -60,6 +61,7 @@ def netatmo_auth():
|
|||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
|
||||
) as mock_auth:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_get_image.side_effect = fake_get_image
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
|
|
|
@ -1,61 +1,50 @@
|
|||
{
|
||||
"12:34:56:78:90:ab": {
|
||||
1599152672: {
|
||||
"id": "12345",
|
||||
"type": "person",
|
||||
"time": 1599152672,
|
||||
"camera_id": "12:34:56:78:90:ab",
|
||||
1654191519: {
|
||||
"home_id": "91763b24c43d3e344f424e8b",
|
||||
"entity_id": "000001",
|
||||
"event_type": "human",
|
||||
"event_time": 1654191519,
|
||||
"module_id": "12:34:56:78:90:ab",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage",
|
||||
"url": "https://netatmocameraimage"
|
||||
},
|
||||
"video_id": "98765",
|
||||
"vignette": {
|
||||
"url": "https://netatmocameraimage"
|
||||
},
|
||||
"video_id": "0011",
|
||||
"video_status": "available",
|
||||
"message": "<b>Paulus</b> seen",
|
||||
"media_url": "http:///files/high/index.m3u8",
|
||||
"message": "<b>Bewegung</b> erkannt",
|
||||
"subevents": [],
|
||||
"media_url": "http:///files/high/index.m3u8"
|
||||
},
|
||||
1599152673: {
|
||||
"id": "12346",
|
||||
"type": "person",
|
||||
"time": 1599152673,
|
||||
"camera_id": "12:34:56:78:90:ab",
|
||||
1654189491: {
|
||||
"home_id": "91763b24c43d3e344f424e8b",
|
||||
"entity_id": "000002",
|
||||
"event_type": "person",
|
||||
"event_time": 1654189491,
|
||||
"module_id": "12:34:56:78:90:ab",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage",
|
||||
"url": "https://netatmocameraimage"
|
||||
},
|
||||
"message": "<b>Tobias</b> seen",
|
||||
},
|
||||
1599152674: {
|
||||
"id": "12347",
|
||||
"type": "outdoor",
|
||||
"time": 1599152674,
|
||||
"camera_id": "12:34:56:78:90:ac",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage",
|
||||
},
|
||||
"video_id": "98766",
|
||||
"video_id": "0012",
|
||||
"video_status": "available",
|
||||
"event_list": [
|
||||
{
|
||||
"type": "vehicle",
|
||||
"time": 1599152674,
|
||||
"id": "12347-0",
|
||||
"offset": 0,
|
||||
"message": "Vehicle detected",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "human",
|
||||
"time": 1599152674,
|
||||
"id": "12347-1",
|
||||
"offset": 8,
|
||||
"message": "Person detected",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage",
|
||||
},
|
||||
},
|
||||
],
|
||||
"media_url": "http:///files/high/index.m3u8",
|
||||
"message": "<b>Jane</b> gesehen",
|
||||
"person_id": "1111",
|
||||
"out_of_sight": False,
|
||||
"subevents": [],
|
||||
"media_url": "http:///files/high/index.m3u8"
|
||||
},
|
||||
1654289891: {
|
||||
"home_id": "91763b24c43d3e344f424e8b",
|
||||
"entity_id": "000002",
|
||||
"event_type": "person",
|
||||
"event_time": 1654289891,
|
||||
"module_id": "12:34:56:78:90:ab",
|
||||
"message": "<b>Jane</b> gesehen",
|
||||
"person_id": "1111",
|
||||
"out_of_sight": False,
|
||||
"subevents": []
|
||||
}
|
||||
}
|
||||
}
|
151
tests/components/netatmo/fixtures/getevents.json
Normal file
151
tests/components/netatmo/fixtures/getevents.json
Normal file
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"body": {
|
||||
"home": {
|
||||
"id": "91763b24c43d3e344f424e8b",
|
||||
"events": [
|
||||
{
|
||||
"id": "11111111111111111f7763a6d",
|
||||
"type": "outdoor",
|
||||
"time": 1645794709,
|
||||
"module_id": "12:34:56:00:a5:a4",
|
||||
"video_id": "11111111-2222-3333-4444-b42f0fc4cfad",
|
||||
"video_status": "available",
|
||||
"subevents": [
|
||||
{
|
||||
"id": "11111111-2222-3333-4444-013560107fce",
|
||||
"type": "human",
|
||||
"time": 1645794709,
|
||||
"verified": true,
|
||||
"offset": 0,
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/000000a722374"
|
||||
},
|
||||
"vignette": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/0000009625c0f"
|
||||
},
|
||||
"message": "Person erfasst"
|
||||
},
|
||||
{
|
||||
"id": "11111111-2222-3333-4444-0b0bc962df43",
|
||||
"type": "vehicle",
|
||||
"time": 1645794716,
|
||||
"verified": true,
|
||||
"offset": 15,
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/00000033f9f96"
|
||||
},
|
||||
"vignette": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/000000cba08af"
|
||||
},
|
||||
"message": "Fahrzeug erfasst"
|
||||
},
|
||||
{
|
||||
"id": "11111111-2222-3333-4444-129e72195968",
|
||||
"type": "human",
|
||||
"time": 1645794716,
|
||||
"verified": true,
|
||||
"offset": 15,
|
||||
"snapshot": {
|
||||
"filename": "vod/11111/events/22222/snapshot_129e72195968.jpg"
|
||||
},
|
||||
"vignette": {
|
||||
"filename": "vod/11111/events/22222/vignette_129e72195968.jpg"
|
||||
},
|
||||
"message": "Person erfasst"
|
||||
},
|
||||
{
|
||||
"id": "11111111-2222-3333-4444-dae4d7e4f24e",
|
||||
"type": "human",
|
||||
"time": 1645794718,
|
||||
"verified": true,
|
||||
"offset": 17,
|
||||
"snapshot": {
|
||||
"filename": "vod/11111/events/22222/snapshot_dae4d7e4f24e.jpg"
|
||||
},
|
||||
"vignette": {
|
||||
"filename": "vod/11111/events/22222/vignette_dae4d7e4f24e.jpg"
|
||||
},
|
||||
"message": "Person erfasst"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1111111111111111e7e40c353",
|
||||
"type": "connection",
|
||||
"time": 1645784799,
|
||||
"module_id": "12:34:56:00:a5:a4",
|
||||
"message": "Front verbunden"
|
||||
},
|
||||
{
|
||||
"id": "11111111111111144e3115860",
|
||||
"type": "boot",
|
||||
"time": 1645784775,
|
||||
"module_id": "12:34:56:00:a5:a4",
|
||||
"message": "Front gestartet"
|
||||
},
|
||||
{
|
||||
"id": "11111111111111169804049ca",
|
||||
"type": "disconnection",
|
||||
"time": 1645773806,
|
||||
"module_id": "12:34:56:00:a5:a4",
|
||||
"message": "Front getrennt"
|
||||
},
|
||||
{
|
||||
"id": "1111111111111117cb8147ffd",
|
||||
"type": "outdoor",
|
||||
"time": 1645712826,
|
||||
"module_id": "12:34:56:00:a5:a4",
|
||||
"video_id": "11111111-2222-3333-4444-5091e1903f8d",
|
||||
"video_status": "available",
|
||||
"subevents": [
|
||||
{
|
||||
"id": "11111111-2222-3333-4444-b7d28e3ccc38",
|
||||
"type": "human",
|
||||
"time": 1645712826,
|
||||
"verified": true,
|
||||
"offset": 0,
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/000000a0ca642"
|
||||
},
|
||||
"vignette": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/00000031b0ed4"
|
||||
},
|
||||
"message": "Person erfasst"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1111111111111119df3d2de6",
|
||||
"type": "person_home",
|
||||
"time": 1645902000,
|
||||
"module_id": "12:34:56:00:f1:62",
|
||||
"message": "Home Assistant Cloud definiert John Doe und Jane Doe als \"Zu Hause\""
|
||||
},
|
||||
{
|
||||
"id": "1111111111111112c91b3628",
|
||||
"type": "person",
|
||||
"time": 1645901266,
|
||||
"module_id": "12:34:56:00:f1:62",
|
||||
"snapshot": {
|
||||
"url": "https://netatmocameraimage.blob.core.windows.net/production/0000081d4f42875d9"
|
||||
},
|
||||
"video_id": "11111111-2222-3333-4444-314d161525db",
|
||||
"video_status": "available",
|
||||
"message": "<b>John Doe</b> gesehen",
|
||||
"person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
|
||||
"out_of_sight": false
|
||||
},
|
||||
{
|
||||
"id": "1111111111111115166b1283",
|
||||
"type": "tag_open",
|
||||
"time": 1645897638,
|
||||
"module_id": "12:34:56:00:86:99",
|
||||
"message": "Window Hall: immer noch offen"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "ok",
|
||||
"time_exec": 0.24369096755981445,
|
||||
"time_server": 1645897231
|
||||
}
|
|
@ -19,7 +19,13 @@
|
|||
"id": "3688132631",
|
||||
"name": "Hall",
|
||||
"type": "custom",
|
||||
"module_ids": ["12:34:56:00:f1:62"]
|
||||
"module_ids": [
|
||||
"12:34:56:00:f1:62",
|
||||
"12:34:56:10:f1:66",
|
||||
"12:34:56:00:e3:9b",
|
||||
"12:34:56:00:86:99",
|
||||
"0009999992"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2833524037",
|
||||
|
@ -32,6 +38,44 @@
|
|||
"name": "Cocina",
|
||||
"type": "kitchen",
|
||||
"module_ids": ["12:34:56:03:a0:ac"]
|
||||
},
|
||||
{
|
||||
"id": "2940411588",
|
||||
"name": "Child",
|
||||
"type": "custom",
|
||||
"module_ids": ["12:34:56:26:cc:01"]
|
||||
},
|
||||
{
|
||||
"id": "222452125",
|
||||
"name": "Bureau",
|
||||
"type": "electrical_cabinet",
|
||||
"module_ids": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"],
|
||||
"modules": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"],
|
||||
"therm_relay": "12:34:56:20:f5:44",
|
||||
"true_temperature_available": true
|
||||
},
|
||||
{
|
||||
"id": "100007519",
|
||||
"name": "Cabinet",
|
||||
"type": "electrical_cabinet",
|
||||
"module_ids": [
|
||||
"12:34:56:00:16:0e",
|
||||
"12:34:56:00:16:0e#0",
|
||||
"12:34:56:00:16:0e#1",
|
||||
"12:34:56:00:16:0e#2",
|
||||
"12:34:56:00:16:0e#3",
|
||||
"12:34:56:00:16:0e#4",
|
||||
"12:34:56:00:16:0e#5",
|
||||
"12:34:56:00:16:0e#6",
|
||||
"12:34:56:00:16:0e#7",
|
||||
"12:34:56:00:16:0e#8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1002003001",
|
||||
"name": "Corridor",
|
||||
"type": "corridor",
|
||||
"module_ids": ["10:20:30:bd:b8:1e"]
|
||||
}
|
||||
],
|
||||
"modules": [
|
||||
|
@ -75,7 +119,334 @@
|
|||
"type": "NACamera",
|
||||
"name": "Hall",
|
||||
"setup_date": 1544828430,
|
||||
"room_id": "3688132631"
|
||||
"room_id": "3688132631",
|
||||
"reachable": true,
|
||||
"modules_bridged": ["12:34:56:00:86:99", "12:34:56:00:e3:9b"]
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:a5:a4",
|
||||
"type": "NOC",
|
||||
"name": "Garden",
|
||||
"setup_date": 1544828430,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:20:f5:44",
|
||||
"type": "OTH",
|
||||
"name": "Modulating Relay",
|
||||
"setup_date": 1607443936,
|
||||
"room_id": "222452125",
|
||||
"reachable": true,
|
||||
"modules_bridged": ["12:34:56:20:f5:8c"],
|
||||
"hk_device_id": "12:34:56:20:d0:c5",
|
||||
"capabilities": [
|
||||
{
|
||||
"name": "automatism",
|
||||
"available": true
|
||||
}
|
||||
],
|
||||
"max_modules_nb": 21
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:20:f5:8c",
|
||||
"type": "OTM",
|
||||
"name": "Bureau Modulate",
|
||||
"setup_date": 1607443939,
|
||||
"room_id": "222452125",
|
||||
"bridge": "12:34:56:20:f5:44"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:10:f1:66",
|
||||
"type": "NDB",
|
||||
"name": "Netatmo-Doorbell",
|
||||
"setup_date": 1602691361,
|
||||
"room_id": "3688132631",
|
||||
"reachable": true,
|
||||
"hk_device_id": "123456007df1",
|
||||
"customer_id": "1000010",
|
||||
"network_lock": false,
|
||||
"quick_display_zone": 62
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:e3:9b",
|
||||
"type": "NIS",
|
||||
"setup_date": 1620479901,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"name": "Sirene in hall"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
"name": "Window Hall",
|
||||
"setup_date": 1581177375,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"category": "window"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:30:d5:d4",
|
||||
"type": "NBG",
|
||||
"name": "module iDiamant",
|
||||
"setup_date": 1562262465,
|
||||
"room_id": "222452125",
|
||||
"modules_bridged": ["0009999992"]
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
"type": "NBR",
|
||||
"name": "Entrance Blinds",
|
||||
"setup_date": 1578551339,
|
||||
"room_id": "3688132631",
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:37:11:ca",
|
||||
"type": "NAMain",
|
||||
"name": "NetatmoIndoor",
|
||||
"setup_date": 1419453350,
|
||||
"reachable": true,
|
||||
"modules_bridged": [
|
||||
"12:34:56:07:bb:3e",
|
||||
"12:34:56:03:1b:e4",
|
||||
"12:34:56:36:fc:de",
|
||||
"12:34:56:05:51:20"
|
||||
],
|
||||
"customer_id": "C00016",
|
||||
"hardware_version": 251,
|
||||
"public_ext_data": false,
|
||||
"public_ext_counter": 0,
|
||||
"alarm_config": {
|
||||
"default_alarm": [
|
||||
{
|
||||
"db_alarm_number": 0
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 1
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 2
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 6
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 4
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 5
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 7
|
||||
},
|
||||
{
|
||||
"db_alarm_number": 22
|
||||
}
|
||||
],
|
||||
"personnalized": [
|
||||
{
|
||||
"threshold": 20,
|
||||
"data_type": 1,
|
||||
"direction": 0,
|
||||
"db_alarm_number": 8
|
||||
},
|
||||
{
|
||||
"threshold": 17,
|
||||
"data_type": 1,
|
||||
"direction": 1,
|
||||
"db_alarm_number": 9
|
||||
},
|
||||
{
|
||||
"threshold": 65,
|
||||
"data_type": 4,
|
||||
"direction": 0,
|
||||
"db_alarm_number": 16
|
||||
},
|
||||
{
|
||||
"threshold": 19,
|
||||
"data_type": 8,
|
||||
"direction": 0,
|
||||
"db_alarm_number": 22
|
||||
}
|
||||
]
|
||||
},
|
||||
"module_offset": {
|
||||
"12:34:56:80:bb:26": {
|
||||
"a": 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:36:fc:de",
|
||||
"type": "NAModule1",
|
||||
"name": "Outdoor",
|
||||
"setup_date": 1448565785,
|
||||
"bridge": "12:34:56:37:11:ca"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:03:1b:e4",
|
||||
"type": "NAModule2",
|
||||
"name": "Garden",
|
||||
"setup_date": 1543579864,
|
||||
"bridge": "12:34:56:37:11:ca"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:05:51:20",
|
||||
"type": "NAModule3",
|
||||
"name": "Rain",
|
||||
"setup_date": 1591770206,
|
||||
"bridge": "12:34:56:37:11:ca"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:07:bb:3e",
|
||||
"type": "NAModule4",
|
||||
"name": "Bedroom",
|
||||
"setup_date": 1484997703,
|
||||
"bridge": "12:34:56:37:11:ca"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:26:68:92",
|
||||
"type": "NHC",
|
||||
"name": "Indoor",
|
||||
"setup_date": 1571342643
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:26:cc:01",
|
||||
"type": "BNS",
|
||||
"name": "Child",
|
||||
"setup_date": 1571634243
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:60:40",
|
||||
"type": "NLG",
|
||||
"name": "Prise Control",
|
||||
"setup_date": 1641841257,
|
||||
"room_id": "1310352496",
|
||||
"modules_bridged": [
|
||||
"12:34:56:80:00:12:ac:f2",
|
||||
"12:34:56:80:00:c3:69:3c",
|
||||
"12:34:56:00:00:a1:4c:da",
|
||||
"12:34:56:00:01:01:01:a1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"type": "NLP",
|
||||
"name": "Prise",
|
||||
"setup_date": 1641841262,
|
||||
"room_id": "1310352496",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:c3:69:3c",
|
||||
"type": "NLT",
|
||||
"name": "Commande sans fil",
|
||||
"setup_date": 1641841262,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e",
|
||||
"type": "NLE",
|
||||
"name": "Écocompteur",
|
||||
"setup_date": 1644496884,
|
||||
"room_id": "100007519",
|
||||
"modules_bridged": [
|
||||
"12:34:56:00:16:0e#0",
|
||||
"12:34:56:00:16:0e#1",
|
||||
"12:34:56:00:16:0e#2",
|
||||
"12:34:56:00:16:0e#3",
|
||||
"12:34:56:00:16:0e#4",
|
||||
"12:34:56:00:16:0e#5",
|
||||
"12:34:56:00:16:0e#6",
|
||||
"12:34:56:00:16:0e#7",
|
||||
"12:34:56:00:16:0e#8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#0",
|
||||
"type": "NLE",
|
||||
"name": "Line 1",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#1",
|
||||
"type": "NLE",
|
||||
"name": "Line 2",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#2",
|
||||
"type": "NLE",
|
||||
"name": "Line 3",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#3",
|
||||
"type": "NLE",
|
||||
"name": "Line 4",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#4",
|
||||
"type": "NLE",
|
||||
"name": "Line 5",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#5",
|
||||
"type": "NLE",
|
||||
"name": "Total",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#6",
|
||||
"type": "NLE",
|
||||
"name": "Gas",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#7",
|
||||
"type": "NLE",
|
||||
"name": "Hot water",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#8",
|
||||
"type": "NLE",
|
||||
"name": "Cold water",
|
||||
"setup_date": 1644496886,
|
||||
"room_id": "100007519",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:00:a1:4c:da",
|
||||
"type": "NLPC",
|
||||
"name": "Consumption meter",
|
||||
"setup_date": 1638376602,
|
||||
"room_id": "100008999",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"type": "NLFN",
|
||||
"name": "Bathroom light",
|
||||
"setup_date": 1598367404,
|
||||
"room_id": "1002003001",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
}
|
||||
],
|
||||
"schedules": [
|
||||
|
|
|
@ -14,6 +14,25 @@
|
|||
"vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,",
|
||||
"is_local": true
|
||||
},
|
||||
{
|
||||
"type": "NOC",
|
||||
"firmware_revision": 3002000,
|
||||
"monitoring": "on",
|
||||
"sd_status": 4,
|
||||
"connection": "wifi",
|
||||
"homekit_status": "upgradable",
|
||||
"floodlight": "auto",
|
||||
"timelapse_available": true,
|
||||
"id": "12:34:56:00:a5:a4",
|
||||
"vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,",
|
||||
"is_local": false,
|
||||
"network_lock": false,
|
||||
"firmware_name": "3.2.0",
|
||||
"wifi_strength": 62,
|
||||
"alim_status": 2,
|
||||
"locked": false,
|
||||
"wifi_state": "high"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:fa:d0",
|
||||
"type": "NAPlug",
|
||||
|
@ -50,6 +69,805 @@
|
|||
"rf_strength": 59,
|
||||
"bridge": "12:34:56:00:fa:d0",
|
||||
"battery_state": "full"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:26:cc:01",
|
||||
"type": "BNS",
|
||||
"firmware_revision": 32,
|
||||
"wifi_strength": 50,
|
||||
"boiler_valve_comfort_boost": false,
|
||||
"boiler_status": true,
|
||||
"cooler_status": false
|
||||
},
|
||||
{
|
||||
"type": "NDB",
|
||||
"last_ftp_event": {
|
||||
"type": 3,
|
||||
"time": 1631444443,
|
||||
"id": 3
|
||||
},
|
||||
"id": "12:34:56:10:f1:66",
|
||||
"websocket_connected": true,
|
||||
"vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
|
||||
"is_local": false,
|
||||
"alim_status": 2,
|
||||
"connection": "wifi",
|
||||
"firmware_name": "2.18.0",
|
||||
"firmware_revision": 2018000,
|
||||
"homekit_status": "configured",
|
||||
"max_peers_reached": false,
|
||||
"sd_status": 4,
|
||||
"wifi_strength": 66,
|
||||
"wifi_state": "medium"
|
||||
},
|
||||
{
|
||||
"boiler_control": "onoff",
|
||||
"dhw_control": "none",
|
||||
"firmware_revision": 22,
|
||||
"hardware_version": 222,
|
||||
"id": "12:34:56:20:f5:44",
|
||||
"outdoor_temperature": 8.2,
|
||||
"sequence_id": 19764,
|
||||
"type": "OTH",
|
||||
"wifi_strength": 57
|
||||
},
|
||||
{
|
||||
"battery_level": 4176,
|
||||
"boiler_status": false,
|
||||
"boiler_valve_comfort_boost": false,
|
||||
"firmware_revision": 6,
|
||||
"id": "12:34:56:20:f5:8c",
|
||||
"last_message": 1637684297,
|
||||
"last_seen": 1637684297,
|
||||
"radio_id": 2,
|
||||
"reachable": true,
|
||||
"rf_strength": 64,
|
||||
"type": "OTM",
|
||||
"bridge": "12:34:56:20:f5:44",
|
||||
"battery_state": "full"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:30:d5:d4",
|
||||
"type": "NBG",
|
||||
"firmware_revision": 39,
|
||||
"wifi_strength": 65,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
"type": "NBR",
|
||||
"current_position": 0,
|
||||
"target_position": 0,
|
||||
"target_position_step": 100,
|
||||
"firmware_revision": 16,
|
||||
"rf_strength": 0,
|
||||
"last_seen": 1638353156,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:30:d5:d4"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
"battery_state": "high",
|
||||
"battery_level": 5240,
|
||||
"firmware_revision": 58,
|
||||
"rf_state": "full",
|
||||
"rf_strength": 58,
|
||||
"last_seen": 1642698124,
|
||||
"last_activity": 1627757310,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_news"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:e3:9b",
|
||||
"type": "NIS",
|
||||
"battery_state": "low",
|
||||
"battery_level": 5438,
|
||||
"firmware_revision": 209,
|
||||
"rf_state": "medium",
|
||||
"rf_strength": 62,
|
||||
"last_seen": 1644569790,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_sound",
|
||||
"monitoring": "off"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:60:40",
|
||||
"type": "NLG",
|
||||
"offload": false,
|
||||
"firmware_revision": 211,
|
||||
"last_seen": 1644567372,
|
||||
"wifi_strength": 51,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"type": "NLP",
|
||||
"on": true,
|
||||
"offload": false,
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1644569425,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:c3:69:3c",
|
||||
"type": "NLT",
|
||||
"battery_state": "full",
|
||||
"battery_level": 3300,
|
||||
"firmware_revision": 42,
|
||||
"last_seen": 0,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e",
|
||||
"type": "NLE",
|
||||
"firmware_revision": 14,
|
||||
"wifi_strength": 38
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#0",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#1",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#2",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#3",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#4",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#5",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#6",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#7",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#8",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:00:a1:4c:da",
|
||||
"type": "NLPC",
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1646511241,
|
||||
"power": 476,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"brightness": 100,
|
||||
"firmware_revision": 52,
|
||||
"last_seen": 1604940167,
|
||||
"on": false,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"type": "NLFN",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"type": "NDB",
|
||||
"last_ftp_event": {
|
||||
"type": 3,
|
||||
"time": 1631444443,
|
||||
"id": 3
|
||||
},
|
||||
"id": "12:34:56:10:f1:66",
|
||||
"websocket_connected": true,
|
||||
"vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
|
||||
"is_local": false,
|
||||
"alim_status": 2,
|
||||
"connection": "wifi",
|
||||
"firmware_name": "2.18.0",
|
||||
"firmware_revision": 2018000,
|
||||
"homekit_status": "configured",
|
||||
"max_peers_reached": false,
|
||||
"sd_status": 4,
|
||||
"wifi_strength": 66,
|
||||
"wifi_state": "medium"
|
||||
},
|
||||
{
|
||||
"boiler_control": "onoff",
|
||||
"dhw_control": "none",
|
||||
"firmware_revision": 22,
|
||||
"hardware_version": 222,
|
||||
"id": "12:34:56:20:f5:44",
|
||||
"outdoor_temperature": 8.2,
|
||||
"sequence_id": 19764,
|
||||
"type": "OTH",
|
||||
"wifi_strength": 57
|
||||
},
|
||||
{
|
||||
"battery_level": 4176,
|
||||
"boiler_status": false,
|
||||
"boiler_valve_comfort_boost": false,
|
||||
"firmware_revision": 6,
|
||||
"id": "12:34:56:20:f5:8c",
|
||||
"last_message": 1637684297,
|
||||
"last_seen": 1637684297,
|
||||
"radio_id": 2,
|
||||
"reachable": true,
|
||||
"rf_strength": 64,
|
||||
"type": "OTM",
|
||||
"bridge": "12:34:56:20:f5:44",
|
||||
"battery_state": "full"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:30:d5:d4",
|
||||
"type": "NBG",
|
||||
"firmware_revision": 39,
|
||||
"wifi_strength": 65,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
"type": "NBR",
|
||||
"current_position": 0,
|
||||
"target_position": 0,
|
||||
"target_position_step": 100,
|
||||
"firmware_revision": 16,
|
||||
"rf_strength": 0,
|
||||
"last_seen": 1638353156,
|
||||
"reachable": true,
|
||||
"therm_measured_temperature": 5,
|
||||
"heating_power_request": 1,
|
||||
"therm_setpoint_temperature": 7,
|
||||
"therm_setpoint_mode": "away",
|
||||
"therm_setpoint_start_time": 0,
|
||||
"therm_setpoint_end_time": 0,
|
||||
"anticipating": false,
|
||||
"open_window": false
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
"battery_state": "high",
|
||||
"battery_level": 5240,
|
||||
"firmware_revision": 58,
|
||||
"rf_state": "full",
|
||||
"rf_strength": 58,
|
||||
"last_seen": 1642698124,
|
||||
"last_activity": 1627757310,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_news"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:e3:9b",
|
||||
"type": "NIS",
|
||||
"battery_state": "low",
|
||||
"battery_level": 5438,
|
||||
"firmware_revision": 209,
|
||||
"rf_state": "medium",
|
||||
"rf_strength": 62,
|
||||
"last_seen": 1644569790,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_sound",
|
||||
"monitoring": "off"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:60:40",
|
||||
"type": "NLG",
|
||||
"offload": false,
|
||||
"firmware_revision": 211,
|
||||
"last_seen": 1644567372,
|
||||
"wifi_strength": 51,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"type": "NLP",
|
||||
"on": true,
|
||||
"offload": false,
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1644569425,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:c3:69:3c",
|
||||
"type": "NLT",
|
||||
"battery_state": "full",
|
||||
"battery_level": 3300,
|
||||
"firmware_revision": 42,
|
||||
"last_seen": 0,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e",
|
||||
"type": "NLE",
|
||||
"firmware_revision": 14,
|
||||
"wifi_strength": 38
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#0",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#1",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#2",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#3",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#4",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#5",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#6",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#7",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#8",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:00:a1:4c:da",
|
||||
"type": "NLPC",
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1646511241,
|
||||
"power": 476,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"brightness": 100,
|
||||
"firmware_revision": 52,
|
||||
"last_seen": 1604940167,
|
||||
"on": false,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"type": "NLFN",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"type": "NDB",
|
||||
"last_ftp_event": {
|
||||
"type": 3,
|
||||
"time": 1631444443,
|
||||
"id": 3
|
||||
},
|
||||
"id": "12:34:56:10:f1:66",
|
||||
"websocket_connected": true,
|
||||
"vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
|
||||
"is_local": false,
|
||||
"alim_status": 2,
|
||||
"connection": "wifi",
|
||||
"firmware_name": "2.18.0",
|
||||
"firmware_revision": 2018000,
|
||||
"homekit_status": "configured",
|
||||
"max_peers_reached": false,
|
||||
"sd_status": 4,
|
||||
"wifi_strength": 66,
|
||||
"wifi_state": "medium"
|
||||
},
|
||||
{
|
||||
"boiler_control": "onoff",
|
||||
"dhw_control": "none",
|
||||
"firmware_revision": 22,
|
||||
"hardware_version": 222,
|
||||
"id": "12:34:56:20:f5:44",
|
||||
"outdoor_temperature": 8.2,
|
||||
"sequence_id": 19764,
|
||||
"type": "OTH",
|
||||
"wifi_strength": 57
|
||||
},
|
||||
{
|
||||
"battery_level": 4176,
|
||||
"boiler_status": false,
|
||||
"boiler_valve_comfort_boost": false,
|
||||
"firmware_revision": 6,
|
||||
"id": "12:34:56:20:f5:8c",
|
||||
"last_message": 1637684297,
|
||||
"last_seen": 1637684297,
|
||||
"radio_id": 2,
|
||||
"reachable": true,
|
||||
"rf_strength": 64,
|
||||
"type": "OTM",
|
||||
"bridge": "12:34:56:20:f5:44",
|
||||
"battery_state": "full"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:30:d5:d4",
|
||||
"type": "NBG",
|
||||
"firmware_revision": 39,
|
||||
"wifi_strength": 65,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
"type": "NBR",
|
||||
"current_position": 0,
|
||||
"target_position": 0,
|
||||
"target_position_step": 100,
|
||||
"firmware_revision": 16,
|
||||
"rf_strength": 0,
|
||||
"last_seen": 1638353156,
|
||||
"reachable": true,
|
||||
"therm_measured_temperature": 5,
|
||||
"heating_power_request": 1,
|
||||
"therm_setpoint_temperature": 7,
|
||||
"therm_setpoint_mode": "away",
|
||||
"therm_setpoint_start_time": 0,
|
||||
"therm_setpoint_end_time": 0,
|
||||
"anticipating": false,
|
||||
"open_window": false
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
"battery_state": "high",
|
||||
"battery_level": 5240,
|
||||
"firmware_revision": 58,
|
||||
"rf_state": "full",
|
||||
"rf_strength": 58,
|
||||
"last_seen": 1642698124,
|
||||
"last_activity": 1627757310,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_news"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:e3:9b",
|
||||
"type": "NIS",
|
||||
"battery_state": "low",
|
||||
"battery_level": 5438,
|
||||
"firmware_revision": 209,
|
||||
"rf_state": "medium",
|
||||
"rf_strength": 62,
|
||||
"last_seen": 1644569790,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_sound",
|
||||
"monitoring": "off"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:60:40",
|
||||
"type": "NLG",
|
||||
"offload": false,
|
||||
"firmware_revision": 211,
|
||||
"last_seen": 1644567372,
|
||||
"wifi_strength": 51,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"type": "NLP",
|
||||
"on": true,
|
||||
"offload": false,
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1644569425,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:c3:69:3c",
|
||||
"type": "NLT",
|
||||
"battery_state": "full",
|
||||
"battery_level": 3300,
|
||||
"firmware_revision": 42,
|
||||
"last_seen": 0,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e",
|
||||
"type": "NLE",
|
||||
"firmware_revision": 14,
|
||||
"wifi_strength": 38
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#0",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#1",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#2",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#3",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#4",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#5",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#6",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#7",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#8",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:00:a1:4c:da",
|
||||
"type": "NLPC",
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1646511241,
|
||||
"power": 476,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"brightness": 100,
|
||||
"firmware_revision": 52,
|
||||
"last_seen": 1604940167,
|
||||
"on": false,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"type": "NLFN",
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"type": "NDB",
|
||||
"last_ftp_event": {
|
||||
"type": 3,
|
||||
"time": 1631444443,
|
||||
"id": 3
|
||||
},
|
||||
"id": "12:34:56:10:f1:66",
|
||||
"websocket_connected": true,
|
||||
"vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
|
||||
"is_local": false,
|
||||
"alim_status": 2,
|
||||
"connection": "wifi",
|
||||
"firmware_name": "2.18.0",
|
||||
"firmware_revision": 2018000,
|
||||
"homekit_status": "configured",
|
||||
"max_peers_reached": false,
|
||||
"sd_status": 4,
|
||||
"wifi_strength": 66,
|
||||
"wifi_state": "medium"
|
||||
},
|
||||
{
|
||||
"boiler_control": "onoff",
|
||||
"dhw_control": "none",
|
||||
"firmware_revision": 22,
|
||||
"hardware_version": 222,
|
||||
"id": "12:34:56:20:f5:44",
|
||||
"outdoor_temperature": 8.2,
|
||||
"sequence_id": 19764,
|
||||
"type": "OTH",
|
||||
"wifi_strength": 57
|
||||
},
|
||||
{
|
||||
"battery_level": 4176,
|
||||
"boiler_status": false,
|
||||
"boiler_valve_comfort_boost": false,
|
||||
"firmware_revision": 6,
|
||||
"id": "12:34:56:20:f5:8c",
|
||||
"last_message": 1637684297,
|
||||
"last_seen": 1637684297,
|
||||
"radio_id": 2,
|
||||
"reachable": true,
|
||||
"rf_strength": 64,
|
||||
"type": "OTM",
|
||||
"bridge": "12:34:56:20:f5:44",
|
||||
"battery_state": "full"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:30:d5:d4",
|
||||
"type": "NBG",
|
||||
"firmware_revision": 39,
|
||||
"wifi_strength": 65,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "0009999992",
|
||||
"type": "NBR",
|
||||
"current_position": 0,
|
||||
"target_position": 0,
|
||||
"target_position_step": 100,
|
||||
"firmware_revision": 16,
|
||||
"rf_strength": 0,
|
||||
"last_seen": 1638353156,
|
||||
"reachable": true,
|
||||
"therm_measured_temperature": 5,
|
||||
"heating_power_request": 1,
|
||||
"therm_setpoint_temperature": 7,
|
||||
"therm_setpoint_mode": "away",
|
||||
"therm_setpoint_start_time": 0,
|
||||
"therm_setpoint_end_time": 0,
|
||||
"anticipating": false,
|
||||
"open_window": false
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:86:99",
|
||||
"type": "NACamDoorTag",
|
||||
"battery_state": "high",
|
||||
"battery_level": 5240,
|
||||
"firmware_revision": 58,
|
||||
"rf_state": "full",
|
||||
"rf_strength": 58,
|
||||
"last_seen": 1642698124,
|
||||
"last_activity": 1627757310,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_news"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:e3:9b",
|
||||
"type": "NIS",
|
||||
"battery_state": "low",
|
||||
"battery_level": 5438,
|
||||
"firmware_revision": 209,
|
||||
"rf_state": "medium",
|
||||
"rf_strength": 62,
|
||||
"last_seen": 1644569790,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:00:f1:62",
|
||||
"status": "no_sound",
|
||||
"monitoring": "off"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:60:40",
|
||||
"type": "NLG",
|
||||
"offload": false,
|
||||
"firmware_revision": 211,
|
||||
"last_seen": 1644567372,
|
||||
"wifi_strength": 51,
|
||||
"reachable": true
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"type": "NLP",
|
||||
"on": true,
|
||||
"offload": false,
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1644569425,
|
||||
"power": 0,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:80:00:c3:69:3c",
|
||||
"type": "NLT",
|
||||
"battery_state": "full",
|
||||
"battery_level": 3300,
|
||||
"firmware_revision": 42,
|
||||
"last_seen": 0,
|
||||
"reachable": false,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e",
|
||||
"type": "NLE",
|
||||
"firmware_revision": 14,
|
||||
"wifi_strength": 38
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#0",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#1",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#2",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#3",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#4",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#5",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#6",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#7",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:16:0e#8",
|
||||
"type": "NLE",
|
||||
"bridge": "12:34:56:00:16:0e"
|
||||
},
|
||||
{
|
||||
"id": "12:34:56:00:00:a1:4c:da",
|
||||
"type": "NLPC",
|
||||
"firmware_revision": 62,
|
||||
"last_seen": 1646511241,
|
||||
"power": 476,
|
||||
"reachable": true,
|
||||
"bridge": "12:34:56:80:60:40"
|
||||
}
|
||||
],
|
||||
"rooms": [
|
||||
|
@ -85,6 +903,19 @@
|
|||
"therm_setpoint_end_time": 0,
|
||||
"anticipating": false,
|
||||
"open_window": false
|
||||
},
|
||||
{
|
||||
"id": "2940411588",
|
||||
"reachable": true,
|
||||
"anticipating": false,
|
||||
"heating_power_request": 0,
|
||||
"open_window": false,
|
||||
"humidity": 68,
|
||||
"therm_measured_temperature": 19.9,
|
||||
"therm_setpoint_temperature": 21.5,
|
||||
"therm_setpoint_start_time": 1647793285,
|
||||
"therm_setpoint_end_time": null,
|
||||
"therm_setpoint_mode": "home"
|
||||
}
|
||||
],
|
||||
"id": "91763b24c43d3e344f424e8b",
|
||||
|
|
|
@ -93,26 +93,36 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
|
|||
assert hass.states.get(camera_entity_indoor).state == "streaming"
|
||||
assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto"
|
||||
|
||||
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
"camera", "turn_off", service_data={"entity_id": "camera.hall"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
camera_id="12:34:56:00:f1:62",
|
||||
monitoring="off",
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:f1:62",
|
||||
"monitoring": "off",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
"camera", "turn_on", service_data={"entity_id": "camera.hall"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
camera_id="12:34:56:00:f1:62",
|
||||
monitoring="on",
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:f1:62",
|
||||
"monitoring": "on",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
@ -135,15 +145,13 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut
|
|||
|
||||
assert cam is not None
|
||||
assert cam.state == STATE_STREAMING
|
||||
assert cam.name == "Hall"
|
||||
|
||||
stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
|
||||
assert stream_source == stream_uri
|
||||
|
||||
requests_mock.get(
|
||||
uri + "/live/snapshot_720.jpg",
|
||||
content=IMAGE_BYTES_FROM_STREAM,
|
||||
)
|
||||
image = await camera.async_get_image(hass, camera_entity_indoor)
|
||||
|
||||
assert image.content == IMAGE_BYTES_FROM_STREAM
|
||||
|
||||
|
||||
|
@ -156,10 +164,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth)
|
|||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
uri = (
|
||||
"https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/"
|
||||
"6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,"
|
||||
)
|
||||
uri = "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,"
|
||||
stream_uri = uri + "/live/files/high/index.m3u8"
|
||||
camera_entity_indoor = "camera.garden"
|
||||
cam = hass.states.get(camera_entity_indoor)
|
||||
|
@ -170,10 +175,6 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth)
|
|||
stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
|
||||
assert stream_source == stream_uri
|
||||
|
||||
requests_mock.get(
|
||||
uri + "/live/snapshot_720.jpg",
|
||||
content=IMAGE_BYTES_FROM_STREAM,
|
||||
)
|
||||
image = await camera.async_get_image(hass, camera_entity_indoor)
|
||||
assert image.content == IMAGE_BYTES_FROM_STREAM
|
||||
|
||||
|
@ -192,32 +193,26 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth):
|
|||
"person": "Richard Doe",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"pyatmo.camera.AsyncCameraData.async_set_persons_away"
|
||||
) as mock_set_persons_away:
|
||||
with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away:
|
||||
await hass.services.async_call(
|
||||
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_persons_away.assert_called_once_with(
|
||||
person_id="91827376-7e04-5298-83af-a0cb8372dff3",
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
)
|
||||
|
||||
data = {
|
||||
"entity_id": "camera.hall",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"pyatmo.camera.AsyncCameraData.async_set_persons_away"
|
||||
) as mock_set_persons_away:
|
||||
with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away:
|
||||
await hass.services.async_call(
|
||||
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_persons_away.assert_called_once_with(
|
||||
person_id=None,
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
)
|
||||
|
||||
|
||||
|
@ -289,16 +284,13 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth):
|
|||
"persons": "John Doe",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"pyatmo.camera.AsyncCameraData.async_set_persons_home"
|
||||
) as mock_set_persons_home:
|
||||
with patch("pyatmo.home.Home.async_set_persons_home") as mock_set_persons_home:
|
||||
await hass.services.async_call(
|
||||
"netatmo", SERVICE_SET_PERSONS_HOME, service_data=data
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_persons_home.assert_called_once_with(
|
||||
person_ids=["91827374-7e04-5298-83ad-a0cb8372dff1"],
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
)
|
||||
|
||||
|
||||
|
@ -316,16 +308,49 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth):
|
|||
"camera_light_mode": "on",
|
||||
}
|
||||
|
||||
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
|
||||
expected_data = {
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:a5:a4",
|
||||
"floodlight": "on",
|
||||
},
|
||||
],
|
||||
}
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
"netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
camera_id="12:34:56:00:a5:a4",
|
||||
floodlight="on",
|
||||
mock_set_state.assert_called_once_with(expected_data)
|
||||
|
||||
|
||||
async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo_auth):
|
||||
"""Test service to set the indoor camera light mode."""
|
||||
with selected_platforms(["camera"]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
data = {
|
||||
"entity_id": "camera.hall",
|
||||
"camera_light_mode": "on",
|
||||
}
|
||||
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state, pytest.raises(
|
||||
HomeAssistantError
|
||||
) as excinfo:
|
||||
await hass.services.async_call(
|
||||
"netatmo",
|
||||
SERVICE_SET_CAMERA_LIGHT,
|
||||
service_data=data,
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_set_state.assert_not_called()
|
||||
assert excinfo.value.args == ("NACamera <Hall> does not have a floodlight",)
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
|
@ -342,13 +367,13 @@ async def test_camera_reconnect_webhook(hass, config_entry):
|
|||
with patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.components.netatmo.PLATFORMS", ["camera"]
|
||||
"homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]
|
||||
), patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
) as mock_webhook:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
mock_webhook.return_value = "https://example.com"
|
||||
|
@ -429,44 +454,6 @@ async def test_setup_component_no_devices(hass, config_entry):
|
|||
"""Fake error during requesting backend data."""
|
||||
nonlocal fake_post_hits
|
||||
fake_post_hits += 1
|
||||
return "{}"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.components.netatmo.PLATFORMS", ["camera"]
|
||||
), patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_no_data
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert fake_post_hits == 4
|
||||
|
||||
|
||||
async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
|
||||
"""Test setup with no devices."""
|
||||
fake_post_hits = 0
|
||||
|
||||
async def fake_post(*args, **kwargs):
|
||||
"""Return fake data."""
|
||||
nonlocal fake_post_hits
|
||||
fake_post_hits += 1
|
||||
|
||||
if "url" not in kwargs:
|
||||
return "{}"
|
||||
|
||||
endpoint = kwargs["url"].split("/")[-1]
|
||||
|
||||
if "snapshot_720.jpg" in endpoint:
|
||||
raise pyatmo.exceptions.ApiError()
|
||||
|
||||
return await fake_post_request(*args, **kwargs)
|
||||
|
||||
with patch(
|
||||
|
@ -478,7 +465,45 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_no_data
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert fake_post_hits == 9
|
||||
|
||||
|
||||
async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
|
||||
"""Test setup with no devices."""
|
||||
fake_post_hits = 0
|
||||
|
||||
async def fake_post(*args, **kwargs):
|
||||
"""Return fake data."""
|
||||
nonlocal fake_post_hits
|
||||
fake_post_hits += 1
|
||||
|
||||
if "endpoint" not in kwargs:
|
||||
return "{}"
|
||||
|
||||
endpoint = kwargs["endpoint"].split("/")[-1]
|
||||
|
||||
if "snapshot_720.jpg" in endpoint:
|
||||
raise pyatmo.ApiError()
|
||||
|
||||
return await fake_post_request(*args, **kwargs)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]
|
||||
), patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_get_image.side_effect = fake_post
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
|
|
|
@ -413,9 +413,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
|
|||
climate_entity_livingroom = "climate.livingroom"
|
||||
|
||||
# Test setting a valid schedule
|
||||
with patch(
|
||||
"pyatmo.climate.AsyncClimate.async_switch_home_schedule"
|
||||
) as mock_switch_home_schedule:
|
||||
with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_schedule:
|
||||
await hass.services.async_call(
|
||||
"netatmo",
|
||||
SERVICE_SET_SCHEDULE,
|
||||
|
@ -423,7 +421,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
|
|||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_switch_home_schedule.assert_called_once_with(
|
||||
mock_switch_schedule.assert_called_once_with(
|
||||
schedule_id="b1b54a2f45795764f59d50d8"
|
||||
)
|
||||
|
||||
|
@ -442,9 +440,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
|
|||
)
|
||||
|
||||
# Test setting an invalid schedule
|
||||
with patch(
|
||||
"pyatmo.climate.AsyncClimate.async_switch_home_schedule"
|
||||
) as mock_switch_home_schedule:
|
||||
with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule:
|
||||
await hass.services.async_call(
|
||||
"netatmo",
|
||||
SERVICE_SET_SCHEDULE,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Test the Netatmo config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyatmo.const import ALL_SCOPES
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.netatmo import config_flow
|
||||
|
@ -14,8 +16,6 @@ from homeassistant.components.netatmo.const import (
|
|||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .common import ALL_SCOPES
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
|
|
110
tests/components/netatmo/test_cover.py
Normal file
110
tests/components/netatmo/test_cover.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
"""The tests for Netatmo cover."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
from .common import selected_platforms
|
||||
|
||||
|
||||
async def test_cover_setup_and_services(hass, config_entry, netatmo_auth):
|
||||
"""Test setup and services."""
|
||||
with selected_platforms(["cover"]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
switch_entity = "cover.entrance_blinds"
|
||||
|
||||
assert hass.states.get(switch_entity).state == "closed"
|
||||
|
||||
# Test cover open
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: switch_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "0009999992",
|
||||
"target_position": 100,
|
||||
"bridge": "12:34:56:30:d5:d4",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Test cover close
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: switch_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "0009999992",
|
||||
"target_position": 0,
|
||||
"bridge": "12:34:56:30:d5:d4",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Test stop cover
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_STOP_COVER,
|
||||
{ATTR_ENTITY_ID: switch_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "0009999992",
|
||||
"target_position": -1,
|
||||
"bridge": "12:34:56:30:d5:d4",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Test set cover position
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{ATTR_ENTITY_ID: switch_entity, ATTR_POSITION: 50},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "0009999992",
|
||||
"target_position": 50,
|
||||
"bridge": "12:34:56:30:d5:d4",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
|
@ -7,11 +7,6 @@ from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN
|
|||
from homeassistant.components.netatmo.const import (
|
||||
CLIMATE_TRIGGERS,
|
||||
INDOOR_CAMERA_TRIGGERS,
|
||||
MODEL_NACAMERA,
|
||||
MODEL_NAPLUG,
|
||||
MODEL_NATHERM1,
|
||||
MODEL_NOC,
|
||||
MODEL_NRV,
|
||||
NETATMO_EVENT,
|
||||
OUTDOOR_CAMERA_TRIGGERS,
|
||||
)
|
||||
|
@ -52,10 +47,10 @@ def calls(hass):
|
|||
@pytest.mark.parametrize(
|
||||
"platform,device_type,event_types",
|
||||
[
|
||||
("camera", MODEL_NOC, OUTDOOR_CAMERA_TRIGGERS),
|
||||
("camera", MODEL_NACAMERA, INDOOR_CAMERA_TRIGGERS),
|
||||
("climate", MODEL_NRV, CLIMATE_TRIGGERS),
|
||||
("climate", MODEL_NATHERM1, CLIMATE_TRIGGERS),
|
||||
("camera", "NOC", OUTDOOR_CAMERA_TRIGGERS),
|
||||
("camera", "NACamera", INDOOR_CAMERA_TRIGGERS),
|
||||
("climate", "NRV", CLIMATE_TRIGGERS),
|
||||
("climate", "NATherm1", CLIMATE_TRIGGERS),
|
||||
],
|
||||
)
|
||||
async def test_get_triggers(
|
||||
|
@ -110,15 +105,15 @@ async def test_get_triggers(
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"platform,camera_type,event_type",
|
||||
[("camera", MODEL_NOC, trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS]
|
||||
+ [("camera", MODEL_NACAMERA, trigger) for trigger in INDOOR_CAMERA_TRIGGERS]
|
||||
[("camera", "NOC", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS]
|
||||
+ [("camera", "NACamera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS]
|
||||
+ [
|
||||
("climate", MODEL_NRV, trigger)
|
||||
("climate", "NRV", trigger)
|
||||
for trigger in CLIMATE_TRIGGERS
|
||||
if trigger not in SUBTYPES
|
||||
]
|
||||
+ [
|
||||
("climate", MODEL_NATHERM1, trigger)
|
||||
("climate", "NATherm1", trigger)
|
||||
for trigger in CLIMATE_TRIGGERS
|
||||
if trigger not in SUBTYPES
|
||||
],
|
||||
|
@ -188,12 +183,12 @@ async def test_if_fires_on_event(
|
|||
@pytest.mark.parametrize(
|
||||
"platform,camera_type,event_type,sub_type",
|
||||
[
|
||||
("climate", MODEL_NRV, trigger, subtype)
|
||||
("climate", "NRV", trigger, subtype)
|
||||
for trigger in SUBTYPES
|
||||
for subtype in SUBTYPES[trigger]
|
||||
]
|
||||
+ [
|
||||
("climate", MODEL_NATHERM1, trigger, subtype)
|
||||
("climate", "NATherm1", trigger, subtype)
|
||||
for trigger in SUBTYPES
|
||||
for subtype in SUBTYPES[trigger]
|
||||
],
|
||||
|
@ -267,7 +262,7 @@ async def test_if_fires_on_event_with_subtype(
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"platform,device_type,event_type",
|
||||
[("climate", MODEL_NAPLUG, trigger) for trigger in CLIMATE_TRIGGERS],
|
||||
[("climate", "NAPLUG", trigger) for trigger in CLIMATE_TRIGGERS],
|
||||
)
|
||||
async def test_if_invalid_device(
|
||||
hass, device_reg, entity_reg, platform, device_type, event_type
|
||||
|
|
|
@ -18,7 +18,7 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
@ -39,16 +39,27 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
|
|||
"expires_in": 60,
|
||||
"refresh_token": REDACTED,
|
||||
"scope": [
|
||||
"read_station",
|
||||
"read_camera",
|
||||
"access_camera",
|
||||
"write_camera",
|
||||
"read_presence",
|
||||
"access_doorbell",
|
||||
"access_presence",
|
||||
"write_presence",
|
||||
"read_bubendorff",
|
||||
"read_camera",
|
||||
"read_carbonmonoxidedetector",
|
||||
"read_doorbell",
|
||||
"read_homecoach",
|
||||
"read_magellan",
|
||||
"read_mx",
|
||||
"read_presence",
|
||||
"read_smarther",
|
||||
"read_smokedetector",
|
||||
"read_station",
|
||||
"read_thermostat",
|
||||
"write_bubendorff",
|
||||
"write_camera",
|
||||
"write_magellan",
|
||||
"write_mx",
|
||||
"write_presence",
|
||||
"write_smarther",
|
||||
"write_thermostat",
|
||||
],
|
||||
"type": "Bearer",
|
||||
|
@ -88,5 +99,5 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
|
|||
"webhook_registered": False,
|
||||
}
|
||||
|
||||
for home in result["data"]["AsyncClimateTopology"]["homes"]:
|
||||
for home in result["data"]["account"]["homes"]:
|
||||
assert home["coordinates"] == REDACTED
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
"""The tests for Netatmo component."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from time import time
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pyatmo
|
||||
from pyatmo.const import ALL_SCOPES
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.netatmo import DOMAIN
|
||||
|
@ -15,7 +14,6 @@ from homeassistant.setup import async_setup_component
|
|||
from homeassistant.util import dt
|
||||
|
||||
from .common import (
|
||||
ALL_SCOPES,
|
||||
FAKE_WEBHOOK_ACTIVATION,
|
||||
fake_post_request,
|
||||
selected_platforms,
|
||||
|
@ -60,7 +58,7 @@ async def test_setup_component(hass, config_entry):
|
|||
) as mock_impl, patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
) as mock_webhook:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
@ -100,9 +98,9 @@ async def test_setup_component_with_config(hass, config_entry):
|
|||
) as mock_webhook, patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.components.netatmo.PLATFORMS", ["sensor"]
|
||||
"homeassistant.components.netatmo.data_handler.PLATFORMS", ["sensor"]
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
|
||||
|
@ -112,7 +110,7 @@ async def test_setup_component_with_config(hass, config_entry):
|
|||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert fake_post_hits == 9
|
||||
assert fake_post_hits == 8
|
||||
mock_impl.assert_called_once()
|
||||
mock_webhook.assert_called_once()
|
||||
|
||||
|
@ -162,7 +160,7 @@ async def test_setup_without_https(hass, config_entry, caplog):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
) as mock_async_generate_url:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_async_generate_url.return_value = "http://example.com"
|
||||
assert await async_setup_component(
|
||||
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
|
||||
|
@ -200,7 +198,7 @@ async def test_setup_with_cloud(hass, config_entry):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
assert await async_setup_component(
|
||||
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
|
||||
)
|
||||
|
@ -266,7 +264,7 @@ async def test_setup_with_cloudhook(hass):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
@ -289,52 +287,6 @@ async def test_setup_with_cloudhook(hass):
|
|||
assert not hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
|
||||
async def test_setup_component_api_error(hass, config_entry):
|
||||
"""Test error on setup of the netatmo component."""
|
||||
with patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
) as mock_impl, patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = (
|
||||
pyatmo.exceptions.ApiError()
|
||||
)
|
||||
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_auth.assert_called_once()
|
||||
mock_impl.assert_called_once()
|
||||
|
||||
|
||||
async def test_setup_component_api_timeout(hass, config_entry):
|
||||
"""Test timeout on setup of the netatmo component."""
|
||||
with patch(
|
||||
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
|
||||
) as mock_auth, patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
|
||||
) as mock_impl, patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = (
|
||||
asyncio.exceptions.TimeoutError()
|
||||
)
|
||||
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_auth.assert_called_once()
|
||||
mock_impl.assert_called_once()
|
||||
|
||||
|
||||
async def test_setup_component_with_delay(hass, config_entry):
|
||||
"""Test setup of the netatmo component with delayed startup."""
|
||||
hass.state = CoreState.not_running
|
||||
|
@ -348,9 +300,9 @@ async def test_setup_component_with_delay(hass, config_entry):
|
|||
) as mock_impl, patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
) as mock_webhook, patch(
|
||||
"pyatmo.AbstractAsyncAuth.async_post_request", side_effect=fake_post_request
|
||||
) as mock_post_request, patch(
|
||||
"homeassistant.components.netatmo.PLATFORMS", ["light"]
|
||||
"pyatmo.AbstractAsyncAuth.async_post_api_request", side_effect=fake_post_request
|
||||
) as mock_post_api_request, patch(
|
||||
"homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"]
|
||||
):
|
||||
|
||||
assert await async_setup_component(
|
||||
|
@ -359,7 +311,7 @@ async def test_setup_component_with_delay(hass, config_entry):
|
|||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_post_request.call_count == 8
|
||||
assert mock_post_api_request.call_count == 7
|
||||
|
||||
mock_impl.assert_called_once()
|
||||
mock_webhook.assert_not_called()
|
||||
|
@ -422,7 +374,7 @@ async def test_setup_component_invalid_token_scope(hass):
|
|||
) as mock_impl, patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
) as mock_webhook:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
assert await async_setup_component(hass, "netatmo", {})
|
||||
|
@ -465,7 +417,7 @@ async def test_setup_component_invalid_token(hass, config_entry):
|
|||
) as mock_webhook, patch(
|
||||
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session"
|
||||
) as mock_session:
|
||||
mock_auth.return_value.async_post_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
|
||||
mock_session.return_value.async_ensure_token_valid.side_effect = (
|
||||
|
|
|
@ -14,8 +14,8 @@ from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhoo
|
|||
from tests.test_util.aiohttp import AiohttpClientMockResponse
|
||||
|
||||
|
||||
async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
|
||||
"""Test setup and services."""
|
||||
async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth):
|
||||
"""Test camera ligiht setup and services."""
|
||||
with selected_platforms(["light"]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
|
@ -53,7 +53,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
|
|||
assert hass.states.get(light_entity).state == "on"
|
||||
|
||||
# Test turning light off
|
||||
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
|
@ -62,13 +62,11 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
camera_id="12:34:56:00:a5:a4",
|
||||
floodlight="auto",
|
||||
{"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]}
|
||||
)
|
||||
|
||||
# Test turning light on
|
||||
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
|
@ -77,9 +75,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
home_id="91763b24c43d3e344f424e8b",
|
||||
camera_id="12:34:56:00:a5:a4",
|
||||
floodlight="on",
|
||||
{"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]}
|
||||
)
|
||||
|
||||
|
||||
|
@ -93,7 +89,7 @@ async def test_setup_component_no_devices(hass, config_entry):
|
|||
fake_post_hits += 1
|
||||
return AiohttpClientMockResponse(
|
||||
method="POST",
|
||||
url=kwargs["url"],
|
||||
url=kwargs["endpoint"],
|
||||
json={},
|
||||
)
|
||||
|
||||
|
@ -106,7 +102,7 @@ async def test_setup_component_no_devices(hass, config_entry):
|
|||
), patch(
|
||||
"homeassistant.components.netatmo.webhook_generate_url"
|
||||
):
|
||||
mock_auth.return_value.async_post_request.side_effect = (
|
||||
mock_auth.return_value.async_post_api_request.side_effect = (
|
||||
fake_post_request_no_data
|
||||
)
|
||||
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
|
||||
|
@ -125,3 +121,57 @@ async def test_setup_component_no_devices(hass, config_entry):
|
|||
|
||||
assert hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
|
||||
"""Test setup and services."""
|
||||
with selected_platforms(["light"]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light_entity = "light.bathroom_light"
|
||||
|
||||
assert hass.states.get(light_entity).state == "off"
|
||||
|
||||
# Test turning light off
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: light_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"on": False,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Test turning light on
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: light_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:00:01:01:01:a1",
|
||||
"on": True,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
|
|
@ -70,13 +70,13 @@ async def test_async_browse_media(hass):
|
|||
|
||||
# Test successful event listing
|
||||
media = await async_browse_media(
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672"
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519"
|
||||
)
|
||||
assert media
|
||||
|
||||
# Test successful event resolve
|
||||
media = await async_resolve_media(
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None
|
||||
hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519", None
|
||||
)
|
||||
assert media == PlayMedia(
|
||||
url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL"
|
||||
|
|
|
@ -19,7 +19,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
|
|||
await hass.async_block_till_done()
|
||||
|
||||
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
|
||||
select_entity = "select.netatmo_myhome"
|
||||
select_entity = "select.myhome"
|
||||
|
||||
assert hass.states.get(select_entity).state == "Default"
|
||||
|
||||
|
@ -40,9 +40,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
|
|||
]
|
||||
|
||||
# Test setting a different schedule
|
||||
with patch(
|
||||
"pyatmo.climate.AsyncClimate.async_switch_home_schedule"
|
||||
) as mock_switch_home_schedule:
|
||||
with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule:
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
|
|
|
@ -4,7 +4,6 @@ from unittest.mock import patch
|
|||
import pytest
|
||||
|
||||
from homeassistant.components.netatmo import sensor
|
||||
from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import TEST_TIME, selected_platforms
|
||||
|
@ -17,7 +16,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth):
|
|||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
prefix = "sensor.mystation_"
|
||||
prefix = "sensor.netatmoindoor_"
|
||||
|
||||
assert hass.states.get(f"{prefix}temperature").state == "24.6"
|
||||
assert hass.states.get(f"{prefix}humidity").state == "36"
|
||||
|
@ -102,77 +101,19 @@ async def test_process_health(health, expected):
|
|||
assert sensor.process_health(health) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, data, expected",
|
||||
[
|
||||
(MODULE_TYPE_WIND, 5591, "Full"),
|
||||
(MODULE_TYPE_WIND, 5181, "High"),
|
||||
(MODULE_TYPE_WIND, 4771, "Medium"),
|
||||
(MODULE_TYPE_WIND, 4361, "Low"),
|
||||
(MODULE_TYPE_WIND, 4300, "Very Low"),
|
||||
],
|
||||
)
|
||||
async def test_process_battery(model, data, expected):
|
||||
"""Test battery level translation."""
|
||||
assert sensor.process_battery(data, model) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"angle, expected",
|
||||
[
|
||||
(0, "N"),
|
||||
(40, "NE"),
|
||||
(70, "E"),
|
||||
(130, "SE"),
|
||||
(160, "S"),
|
||||
(220, "SW"),
|
||||
(250, "W"),
|
||||
(310, "NW"),
|
||||
(340, "N"),
|
||||
],
|
||||
)
|
||||
async def test_process_angle(angle, expected):
|
||||
"""Test wind direction translation."""
|
||||
assert sensor.process_angle(angle) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"angle, expected",
|
||||
[(-1, 359), (-40, 320)],
|
||||
)
|
||||
async def test_fix_angle(angle, expected):
|
||||
"""Test wind angle fix."""
|
||||
assert sensor.fix_angle(angle) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uid, name, expected",
|
||||
[
|
||||
("12:34:56:37:11:ca-reachable", "netatmo_mystation_reachable", "True"),
|
||||
("12:34:56:03:1b:e4-rf_status", "netatmo_mystation_yard_radio", "Full"),
|
||||
(
|
||||
"12:34:56:05:25:6e-rf_status",
|
||||
"netatmo_valley_road_rain_gauge_radio",
|
||||
"Medium",
|
||||
),
|
||||
(
|
||||
"12:34:56:36:fc:de-rf_status_lvl",
|
||||
"netatmo_mystation_netatmooutdoor_radio_level",
|
||||
"65",
|
||||
),
|
||||
(
|
||||
"12:34:56:37:11:ca-wifi_status_lvl",
|
||||
"netatmo_mystation_wifi_level",
|
||||
"45",
|
||||
),
|
||||
("12:34:56:37:11:ca-reachable", "mystation_reachable", "True"),
|
||||
("12:34:56:03:1b:e4-rf_status", "mystation_yard_radio", "Full"),
|
||||
(
|
||||
"12:34:56:37:11:ca-wifi_status",
|
||||
"netatmo_mystation_wifi_status",
|
||||
"mystation_wifi_strength",
|
||||
"Full",
|
||||
),
|
||||
(
|
||||
"12:34:56:37:11:ca-temp_trend",
|
||||
"netatmo_mystation_temperature_trend",
|
||||
"mystation_temperature_trend",
|
||||
"stable",
|
||||
),
|
||||
(
|
||||
|
@ -182,33 +123,47 @@ async def test_fix_angle(angle, expected):
|
|||
),
|
||||
("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"),
|
||||
("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"),
|
||||
("12:34:56:03:1b:e4-windangle", "netatmo_mystation_garden_direction", "SW"),
|
||||
("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"),
|
||||
(
|
||||
"12:34:56:03:1b:e4-windangle_value",
|
||||
"netatmo_mystation_garden_angle",
|
||||
"netatmoindoor_garden_angle",
|
||||
"217",
|
||||
),
|
||||
("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"),
|
||||
(
|
||||
"12:34:56:03:1b:e4-gustangle",
|
||||
"netatmo_mystation_garden_gust_direction",
|
||||
"netatmoindoor_garden_gust_direction",
|
||||
"S",
|
||||
),
|
||||
(
|
||||
"12:34:56:03:1b:e4-gustangle_value",
|
||||
"netatmo_mystation_garden_gust_angle_value",
|
||||
"netatmoindoor_garden_gust_angle",
|
||||
"206",
|
||||
),
|
||||
(
|
||||
"12:34:56:03:1b:e4-guststrength",
|
||||
"netatmo_mystation_garden_gust_strength",
|
||||
"netatmoindoor_garden_gust_strength",
|
||||
"9",
|
||||
),
|
||||
(
|
||||
"12:34:56:03:1b:e4-rf_status",
|
||||
"netatmoindoor_garden_rf_strength",
|
||||
"Full",
|
||||
),
|
||||
(
|
||||
"12:34:56:26:68:92-health_idx",
|
||||
"netatmo_baby_bedroom_health",
|
||||
"baby_bedroom_health",
|
||||
"Fine",
|
||||
),
|
||||
(
|
||||
"12:34:56:26:68:92-wifi_status",
|
||||
"baby_bedroom_wifi",
|
||||
"High",
|
||||
),
|
||||
("Home-max-windangle_value", "home_max_wind_angle", "17"),
|
||||
("Home-max-gustangle_value", "home_max_gust_angle", "217"),
|
||||
("Home-max-guststrength", "home_max_gust_strength", "31"),
|
||||
("Home-max-sum_rain_1", "home_max_sum_rain_1", "0.2"),
|
||||
],
|
||||
)
|
||||
async def test_weather_sensor_enabling(
|
||||
|
|
65
tests/components/netatmo/test_switch.py
Normal file
65
tests/components/netatmo/test_switch.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
"""The tests for Netatmo switch."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
from .common import selected_platforms
|
||||
|
||||
|
||||
async def test_switch_setup_and_services(hass, config_entry, netatmo_auth):
|
||||
"""Test setup and services."""
|
||||
with selected_platforms(["switch"]):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
switch_entity = "switch.prise"
|
||||
|
||||
assert hass.states.get(switch_entity).state == "on"
|
||||
|
||||
# Test turning switch off
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: switch_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"on": False,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Test turning switch on
|
||||
with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: switch_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_state.assert_called_once_with(
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"id": "12:34:56:80:00:12:ac:f2",
|
||||
"on": True,
|
||||
"bridge": "12:34:56:80:60:40",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue