Make Netatmo use async pyatmo (#49717)

* Split initialization from data retrival

* Await class initialization

* Async camera

* More async

* Remove stale code

* Clean up

* Update tests

* Fix test

* Improve error handling

* Bump pyatmo version to 5.0.0

* Add tests

* Add cloudhook test

* Increase coverage

* Add test with no camera devices

* Add test for ApiError

* Add test for timeout

* Clean up

* Catch pyatmo ApiError

* Fix PublicData

* Fix media source bug

* Increase coverage for light

* Test webhook with delayed start

* Increase coverage

* Clean up leftover data classes

* Make nonprivate

* Review comments

* Clean up stale code

* Increase cov

* Clean up code

* Code clean up

* Revert delay

* Update homeassistant/components/netatmo/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netatmo/sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Address comment

* Raise cov

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Tobias Sauerwein 2021-05-20 14:59:19 +02:00 committed by GitHub
parent e06a2a53c4
commit ceec871340
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 846 additions and 476 deletions

View file

@ -19,7 +19,11 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import CoreState, HomeAssistant from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
@ -102,8 +106,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not entry.unique_id: if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) AUTH: api.AsyncConfigEntryNetatmoAuth(
aiohttp_client.async_get_clientsession(hass), session
)
} }
data_handler = NetatmoDataHandler(hass, entry) data_handler = NetatmoDataHandler(hass, entry)
@ -122,6 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
{"type": "None", "data": {"push_type": "webhook_deactivation"}}, {"type": "None", "data": {"push_type": "webhook_deactivation"}},
) )
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
async def register_webhook(event): async def register_webhook(event):
if CONF_WEBHOOK_ID not in entry.data: if CONF_WEBHOOK_ID not in entry.data:
@ -175,11 +183,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
handle_event, handle_event,
) )
activation_timeout = async_call_later(hass, 10, unregister_webhook) activation_timeout = async_call_later(hass, 30, unregister_webhook)
await hass.async_add_executor_job( await hass.data[DOMAIN][entry.entry_id][AUTH].async_addwebhook(webhook_url)
hass.data[DOMAIN][entry.entry_id][AUTH].addwebhook, webhook_url
)
_LOGGER.info("Register Netatmo webhook: %s", webhook_url) _LOGGER.info("Register Netatmo webhook: %s", webhook_url)
except pyatmo.ApiError as err: except pyatmo.ApiError as err:
_LOGGER.error("Error during webhook registration - %s", err) _LOGGER.error("Error during webhook registration - %s", err)
@ -202,9 +208,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry.""" """Unload a config entry."""
if CONF_WEBHOOK_ID in entry.data: if CONF_WEBHOOK_ID in entry.data:
await hass.async_add_executor_job( webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook()
)
_LOGGER.info("Unregister Netatmo webhook") _LOGGER.info("Unregister Netatmo webhook")
await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup()

View file

@ -1,34 +1,24 @@
"""API for Netatmo bound to HASS OAuth.""" """API for Netatmo bound to HASS OAuth."""
from asyncio import run_coroutine_threadsafe from aiohttp import ClientSession
import pyatmo import pyatmo
from homeassistant import config_entries, core
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmoOAuth2): class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth):
"""Provide Netatmo authentication tied to an OAuth2 based config entry.""" """Provide Netatmo authentication tied to an OAuth2 based config entry."""
def __init__( def __init__(
self, self,
hass: core.HomeAssistant, websession: ClientSession,
config_entry: config_entries.ConfigEntry, oauth_session: config_entry_oauth2_flow.OAuth2Session,
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
) -> None: ) -> None:
"""Initialize Netatmo Auth.""" """Initialize the auth."""
self.hass = hass super().__init__(websession)
self.session = config_entry_oauth2_flow.OAuth2Session( self._oauth_session = oauth_session
hass, config_entry, implementation
)
super().__init__(token=self.session.token)
def refresh_tokens( async def async_get_access_token(self):
self, """Return a valid access token for Netatmo API."""
) -> dict: if not self._oauth_session.valid_token:
"""Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" await self._oauth_session.async_ensure_token_valid()
run_coroutine_threadsafe( return self._oauth_session.token["access_token"]
self.session.async_ensure_token_valid(), self.hass.loop
).result()
return self.session.token

View file

@ -1,8 +1,8 @@
"""Support for the Netatmo cameras.""" """Support for the Netatmo cameras."""
import logging import logging
import aiohttp
import pyatmo import pyatmo
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.camera import SUPPORT_STREAM, Camera
@ -46,58 +46,40 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.info( _LOGGER.info(
"Cameras are currently not supported with this authentication method" "Cameras are currently not supported with this authentication method"
) )
return
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class( await data_handler.register_data_class(
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
) )
data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
if CAMERA_DATA_CLASS_NAME not in data_handler.data: if not data_class or not data_class.raw_data:
raise PlatformNotReady raise PlatformNotReady
async def get_entities(): all_cameras = []
"""Retrieve Netatmo entities.""" for home in data_class.cameras.values():
for camera in home.values():
all_cameras.append(camera)
if not data_handler.data.get(CAMERA_DATA_CLASS_NAME): entities = [
return [] NetatmoCamera(
data_handler,
camera["id"],
camera["type"],
camera["home_id"],
DEFAULT_QUALITY,
)
for camera in all_cameras
]
data_class = data_handler.data[CAMERA_DATA_CLASS_NAME] for person_id, person_data in data_handler.data[
CAMERA_DATA_CLASS_NAME
].persons.items():
hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO)
entities = [] _LOGGER.debug("Adding cameras %s", entities)
try: async_add_entities(entities, True)
all_cameras = []
for home in data_class.cameras.values():
for camera in home.values():
all_cameras.append(camera)
for camera in all_cameras:
_LOGGER.debug("Adding camera %s %s", camera["id"], camera["name"])
entities.append(
NetatmoCamera(
data_handler,
camera["id"],
camera["type"],
camera["home_id"],
DEFAULT_QUALITY,
)
)
for person_id, person_data in data_handler.data[
CAMERA_DATA_CLASS_NAME
].persons.items():
hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get(
ATTR_PSEUDO
)
except pyatmo.NoDevice:
_LOGGER.debug("No cameras found")
return entities
async_add_entities(await get_entities(), True)
await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
@ -188,33 +170,17 @@ class NetatmoCamera(NetatmoBase, Camera):
self.async_write_ha_state() self.async_write_ha_state()
return return
def camera_image(self): async def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
try: try:
if self._localurl: return await self._data.async_get_live_snapshot(camera_id=self._id)
response = requests.get( except (
f"{self._localurl}/live/snapshot_720.jpg", timeout=10 aiohttp.ClientPayloadError,
) pyatmo.exceptions.ApiError,
elif self._vpnurl: aiohttp.ContentTypeError,
response = requests.get( ) as err:
f"{self._vpnurl}/live/snapshot_720.jpg", _LOGGER.debug("Could not fetch live camera image (%s)", err)
timeout=10, return None
verify=True,
)
else:
_LOGGER.error("Welcome/Presence VPN URL is None")
(self._vpnurl, self._localurl) = self._data.camera_urls(
camera_id=self._id
)
return None
except requests.exceptions.RequestException as error:
_LOGGER.info("Welcome/Presence URL changed: %s", error)
self._data.update_camera_urls(camera_id=self._id)
(self._vpnurl, self._localurl) = self._data.camera_urls(camera_id=self._id)
return None
return response.content
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@ -255,15 +221,17 @@ class NetatmoCamera(NetatmoBase, Camera):
"""Return true if on.""" """Return true if on."""
return self.is_streaming return self.is_streaming
def turn_off(self): async def async_turn_off(self):
"""Turn off camera.""" """Turn off camera."""
self._data.set_state( await self._data.async_set_state(
home_id=self._home_id, camera_id=self._id, monitoring="off" home_id=self._home_id, camera_id=self._id, monitoring="off"
) )
def turn_on(self): async def async_turn_on(self):
"""Turn on camera.""" """Turn on camera."""
self._data.set_state(home_id=self._home_id, camera_id=self._id, monitoring="on") await self._data.async_set_state(
home_id=self._home_id, camera_id=self._id, monitoring="on"
)
async def stream_source(self): async def stream_source(self):
"""Return the stream source.""" """Return the stream source."""
@ -312,7 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera):
] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
return events return events
def _service_set_persons_home(self, **kwargs): async def _service_set_persons_home(self, **kwargs):
"""Service to change current home schedule.""" """Service to change current home schedule."""
persons = kwargs.get(ATTR_PERSONS) persons = kwargs.get(ATTR_PERSONS)
person_ids = [] person_ids = []
@ -321,10 +289,12 @@ class NetatmoCamera(NetatmoBase, Camera):
if data.get("pseudo") == person: if data.get("pseudo") == person:
person_ids.append(pid) person_ids.append(pid)
self._data.set_persons_home(person_ids=person_ids, home_id=self._home_id) await self._data.async_set_persons_home(
person_ids=person_ids, home_id=self._home_id
)
_LOGGER.debug("Set %s as at home", persons) _LOGGER.debug("Set %s as at home", persons)
def _service_set_person_away(self, **kwargs): async def _service_set_person_away(self, **kwargs):
"""Service to mark a person as away or set the home as empty.""" """Service to mark a person as away or set the home as empty."""
person = kwargs.get(ATTR_PERSON) person = kwargs.get(ATTR_PERSON)
person_id = None person_id = None
@ -333,25 +303,25 @@ class NetatmoCamera(NetatmoBase, Camera):
if data.get("pseudo") == person: if data.get("pseudo") == person:
person_id = pid person_id = pid
if person_id is not None: if person_id:
self._data.set_persons_away( await self._data.async_set_persons_away(
person_id=person_id, person_id=person_id,
home_id=self._home_id, home_id=self._home_id,
) )
_LOGGER.debug("Set %s as away", person) _LOGGER.debug("Set %s as away", person)
else: else:
self._data.set_persons_away( await self._data.async_set_persons_away(
person_id=person_id, person_id=person_id,
home_id=self._home_id, home_id=self._home_id,
) )
_LOGGER.debug("Set home as empty") _LOGGER.debug("Set home as empty")
def _service_set_camera_light(self, **kwargs): async def _service_set_camera_light(self, **kwargs):
"""Service to set light mode.""" """Service to set light mode."""
mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE)
_LOGGER.debug("Turn %s camera light for '%s'", mode, self._name) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._name)
self._data.set_state( await self._data.async_set_state(
home_id=self._home_id, home_id=self._home_id,
camera_id=self._id, camera_id=self._id,
floodlight=mode, floodlight=mode,

View file

@ -116,47 +116,39 @@ async def async_setup_entry(hass, entry, async_add_entities):
) )
home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME)
if not home_data or home_data.raw_data == {}:
raise PlatformNotReady
if HOMEDATA_DATA_CLASS_NAME not in data_handler.data: if HOMEDATA_DATA_CLASS_NAME not in data_handler.data:
raise PlatformNotReady raise PlatformNotReady
async def get_entities(): entities = []
"""Retrieve Netatmo entities.""" for home_id in get_all_home_ids(home_data):
entities = [] for room_id in home_data.rooms[home_id]:
signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}"
await data_handler.register_data_class(
HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id
)
home_status = data_handler.data.get(signal_name)
if home_status and room_id in home_status.rooms:
entities.append(NetatmoThermostat(data_handler, home_id, room_id))
for home_id in get_all_home_ids(home_data): hass.data[DOMAIN][DATA_SCHEDULES][home_id] = {
_LOGGER.debug("Setting up home %s", home_id) schedule_id: schedule_data.get("name")
for room_id in home_data.rooms[home_id].keys(): for schedule_id, schedule_data in (
room_name = home_data.rooms[home_id][room_id]["name"] data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules[home_id].items()
_LOGGER.debug("Setting up room %s (%s)", room_name, room_id)
signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}"
await data_handler.register_data_class(
HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id
)
home_status = data_handler.data.get(signal_name)
if home_status and room_id in home_status.rooms:
entities.append(NetatmoThermostat(data_handler, home_id, room_id))
hass.data[DOMAIN][DATA_SCHEDULES][home_id] = {
schedule_id: schedule_data.get("name")
for schedule_id, schedule_data in (
data_handler.data[HOMEDATA_DATA_CLASS_NAME]
.schedules[home_id]
.items()
)
}
hass.data[DOMAIN][DATA_HOMES] = {
home_id: home_data.get("name")
for home_id, home_data in (
data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items()
) )
} }
return entities hass.data[DOMAIN][DATA_HOMES] = {
home_id: home_data.get("name")
for home_id, home_data in (
data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items()
)
}
async_add_entities(await get_entities(), True) _LOGGER.debug("Adding climate devices %s", entities)
async_add_entities(entities, True)
await data_handler.unregister_data_class(HOMEDATA_DATA_CLASS_NAME, None)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
@ -164,7 +156,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
{vol.Required(ATTR_SCHEDULE_NAME): cv.string}, {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
"_service_set_schedule", "_async_service_set_schedule",
) )
@ -205,7 +197,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._model = NA_THERM self._model = NA_THERM
break break
self._state = None
self._device_name = self._data.rooms[home_id][room_id]["name"] self._device_name = self._data.rooms[home_id][room_id]["name"]
self._name = f"{MANUFACTURER} {self._device_name}" self._name = f"{MANUFACTURER} {self._device_name}"
self._current_temperature = None self._current_temperature = None
@ -357,24 +348,24 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return CURRENT_HVAC_HEAT return CURRENT_HVAC_HEAT
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
def set_hvac_mode(self, hvac_mode: str) -> None: async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode.""" """Set new target hvac mode."""
if hvac_mode == HVAC_MODE_OFF: if hvac_mode == HVAC_MODE_OFF:
self.turn_off() await self.async_turn_off()
elif hvac_mode == HVAC_MODE_AUTO: elif hvac_mode == HVAC_MODE_AUTO:
if self.hvac_mode == HVAC_MODE_OFF: if self.hvac_mode == HVAC_MODE_OFF:
self.turn_on() await self.async_turn_on()
self.set_preset_mode(PRESET_SCHEDULE) await self.async_set_preset_mode(PRESET_SCHEDULE)
elif hvac_mode == HVAC_MODE_HEAT: elif hvac_mode == HVAC_MODE_HEAT:
self.set_preset_mode(PRESET_BOOST) await self.async_set_preset_mode(PRESET_BOOST)
def set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
if self.hvac_mode == HVAC_MODE_OFF: if self.hvac_mode == HVAC_MODE_OFF:
self.turn_on() await self.async_turn_on()
if self.target_temperature == 0: if self.target_temperature == 0:
self._home_status.set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
self._id, self._id,
STATE_NETATMO_HOME, STATE_NETATMO_HOME,
) )
@ -384,14 +375,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
and self._model == NA_VALVE and self._model == NA_VALVE
and self.hvac_mode == HVAC_MODE_HEAT and self.hvac_mode == HVAC_MODE_HEAT
): ):
self._home_status.set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
self._id, self._id,
STATE_NETATMO_HOME, STATE_NETATMO_HOME,
) )
elif ( elif (
preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE
): ):
self._home_status.set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
self._id, self._id,
STATE_NETATMO_MANUAL, STATE_NETATMO_MANUAL,
DEFAULT_MAX_TEMP, DEFAULT_MAX_TEMP,
@ -400,13 +391,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]
and self.hvac_mode == HVAC_MODE_HEAT and self.hvac_mode == HVAC_MODE_HEAT
): ):
self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) await self._home_status.async_set_room_thermpoint(
self._id, STATE_NETATMO_HOME
)
elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]:
self._home_status.set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
self._id, PRESET_MAP_NETATMO[preset_mode] self._id, PRESET_MAP_NETATMO[preset_mode]
) )
elif preset_mode in [PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY]: elif preset_mode in [PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY]:
self._home_status.set_thermmode(PRESET_MAP_NETATMO[preset_mode]) await self._home_status.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode])
else: else:
_LOGGER.error("Preset mode '%s' not available", preset_mode) _LOGGER.error("Preset mode '%s' not available", preset_mode)
@ -422,12 +415,14 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
"""Return a list of available preset modes.""" """Return a list of available preset modes."""
return SUPPORT_PRESET return SUPPORT_PRESET
def set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature for 2 hours.""" """Set new target temperature for 2 hours."""
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None: if temp is None:
return return
self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_MANUAL, temp) await self._home_status.async_set_room_thermpoint(
self._id, STATE_NETATMO_MANUAL, temp
)
self.async_write_ha_state() self.async_write_ha_state()
@ -449,21 +444,23 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return attr return attr
def turn_off(self): async def async_turn_off(self):
"""Turn the entity off.""" """Turn the entity off."""
if self._model == NA_VALVE: if self._model == NA_VALVE:
self._home_status.set_room_thermpoint( await self._home_status.async_set_room_thermpoint(
self._id, self._id,
STATE_NETATMO_MANUAL, STATE_NETATMO_MANUAL,
DEFAULT_MIN_TEMP, DEFAULT_MIN_TEMP,
) )
elif self.hvac_mode != HVAC_MODE_OFF: elif self.hvac_mode != HVAC_MODE_OFF:
self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_OFF) await self._home_status.async_set_room_thermpoint(
self._id, STATE_NETATMO_OFF
)
self.async_write_ha_state() self.async_write_ha_state()
def turn_on(self): async def async_turn_on(self):
"""Turn the entity on.""" """Turn the entity on."""
self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME)
self.async_write_ha_state() self.async_write_ha_state()
@property @property
@ -475,6 +472,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
def async_update_callback(self): def async_update_callback(self):
"""Update the entity's state.""" """Update the entity's state."""
self._home_status = self.data_handler.data[self._home_status_class] self._home_status = self.data_handler.data[self._home_status_class]
if self._home_status is None:
if self.available:
self._connected = False
return
self._room_status = self._home_status.rooms.get(self._id) self._room_status = self._home_status.rooms.get(self._id)
self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id)
@ -570,7 +572,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
return {} return {}
def _service_set_schedule(self, **kwargs): async def _async_service_set_schedule(self, **kwargs):
schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_name = kwargs.get(ATTR_SCHEDULE_NAME)
schedule_id = None schedule_id = None
for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items():
@ -581,7 +583,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
_LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME))
return return
self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) await self._data.async_switch_home_schedule(
home_id=self._home_id, schedule_id=schedule_id
)
_LOGGER.debug( _LOGGER.debug(
"Setting %s schedule to %s (%s)", "Setting %s schedule to %s (%s)",
self._home_id, self._home_id,

View file

@ -1,9 +1,9 @@
"""The Netatmo data handler.""" """The Netatmo data handler."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections import deque from collections import deque
from datetime import timedelta from datetime import timedelta
from functools import partial
from itertools import islice from itertools import islice
import logging import logging
from time import time from time import time
@ -19,22 +19,22 @@ from .const import AUTH, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CAMERA_DATA_CLASS_NAME = "CameraData" CAMERA_DATA_CLASS_NAME = "AsyncCameraData"
WEATHERSTATION_DATA_CLASS_NAME = "WeatherStationData" WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData"
HOMECOACH_DATA_CLASS_NAME = "HomeCoachData" HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData"
HOMEDATA_DATA_CLASS_NAME = "HomeData" HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData"
HOMESTATUS_DATA_CLASS_NAME = "HomeStatus" HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus"
PUBLICDATA_DATA_CLASS_NAME = "PublicData" PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData"
NEXT_SCAN = "next_scan" NEXT_SCAN = "next_scan"
DATA_CLASSES = { DATA_CLASSES = {
WEATHERSTATION_DATA_CLASS_NAME: pyatmo.WeatherStationData, WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData,
HOMECOACH_DATA_CLASS_NAME: pyatmo.HomeCoachData, HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData,
CAMERA_DATA_CLASS_NAME: pyatmo.CameraData, CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData,
HOMEDATA_DATA_CLASS_NAME: pyatmo.HomeData, HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData,
HOMESTATUS_DATA_CLASS_NAME: pyatmo.HomeStatus, HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus,
PUBLICDATA_DATA_CLASS_NAME: pyatmo.PublicData, PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData,
} }
BATCH_SIZE = 3 BATCH_SIZE = 3
@ -57,7 +57,7 @@ class NetatmoDataHandler:
self.hass = hass self.hass = hass
self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] self._auth = hass.data[DOMAIN][entry.entry_id][AUTH]
self.listeners: list[CALLBACK_TYPE] = [] self.listeners: list[CALLBACK_TYPE] = []
self._data_classes: dict = {} self.data_classes: dict = {}
self.data = {} self.data = {}
self._queue = deque() self._queue = deque()
self._webhook: bool = False self._webhook: bool = False
@ -87,21 +87,19 @@ class NetatmoDataHandler:
for data_class in islice(self._queue, 0, BATCH_SIZE): for data_class in islice(self._queue, 0, BATCH_SIZE):
if data_class[NEXT_SCAN] > time(): if data_class[NEXT_SCAN] > time():
continue continue
self._data_classes[data_class["name"]][NEXT_SCAN] = ( self.data_classes[data_class["name"]][NEXT_SCAN] = (
time() + data_class["interval"] time() + data_class["interval"]
) )
await self.async_fetch_data( await self.async_fetch_data(data_class["name"])
data_class["class"], data_class["name"], **data_class["kwargs"]
)
self._queue.rotate(BATCH_SIZE) self._queue.rotate(BATCH_SIZE)
@callback @callback
def async_force_update(self, data_class_entry): def async_force_update(self, data_class_entry):
"""Prioritize data retrieval for given data class entry.""" """Prioritize data retrieval for given data class entry."""
self._data_classes[data_class_entry][NEXT_SCAN] = time() self.data_classes[data_class_entry][NEXT_SCAN] = time()
self._queue.rotate(-(self._queue.index(self._data_classes[data_class_entry]))) self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry])))
async def async_cleanup(self): async def async_cleanup(self):
"""Clean up the Netatmo data handler.""" """Clean up the Netatmo data handler."""
@ -122,19 +120,10 @@ class NetatmoDataHandler:
_LOGGER.debug("%s camera reconnected", MANUFACTURER) _LOGGER.debug("%s camera reconnected", MANUFACTURER)
self.async_force_update(CAMERA_DATA_CLASS_NAME) self.async_force_update(CAMERA_DATA_CLASS_NAME)
async def async_fetch_data(self, data_class, data_class_entry, **kwargs): async def async_fetch_data(self, data_class_entry):
"""Fetch data and notify.""" """Fetch data and notify."""
try: try:
self.data[data_class_entry] = await self.hass.async_add_executor_job( await self.data[data_class_entry].async_update()
partial(data_class, **kwargs),
self._auth,
)
for update_callback in self._data_classes[data_class_entry][
"subscriptions"
]:
if update_callback:
update_callback()
except pyatmo.NoDevice as err: except pyatmo.NoDevice as err:
_LOGGER.debug(err) _LOGGER.debug(err)
@ -143,42 +132,46 @@ class NetatmoDataHandler:
except pyatmo.ApiError as err: except pyatmo.ApiError as err:
_LOGGER.debug(err) _LOGGER.debug(err)
except asyncio.TimeoutError as err:
_LOGGER.debug(err)
return
for update_callback in self.data_classes[data_class_entry]["subscriptions"]:
if update_callback:
update_callback()
async def register_data_class( async def register_data_class(
self, data_class_name, data_class_entry, update_callback, **kwargs self, data_class_name, data_class_entry, update_callback, **kwargs
): ):
"""Register data class.""" """Register data class."""
if data_class_entry in self._data_classes: if data_class_entry in self.data_classes:
self._data_classes[data_class_entry]["subscriptions"].append( self.data_classes[data_class_entry]["subscriptions"].append(update_callback)
update_callback
)
return return
self._data_classes[data_class_entry] = { self.data_classes[data_class_entry] = {
"class": DATA_CLASSES[data_class_name],
"name": data_class_entry, "name": data_class_entry,
"interval": DEFAULT_INTERVALS[data_class_name], "interval": DEFAULT_INTERVALS[data_class_name],
NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name],
"kwargs": kwargs,
"subscriptions": [update_callback], "subscriptions": [update_callback],
} }
await self.async_fetch_data( self.data[data_class_entry] = DATA_CLASSES[data_class_name](
DATA_CLASSES[data_class_name], data_class_entry, **kwargs self._auth, **kwargs
) )
self._queue.append(self._data_classes[data_class_entry]) await self.async_fetch_data(data_class_entry)
self._queue.append(self.data_classes[data_class_entry])
_LOGGER.debug("Data class %s added", data_class_entry) _LOGGER.debug("Data class %s added", data_class_entry)
async def unregister_data_class(self, data_class_entry, update_callback): async def unregister_data_class(self, data_class_entry, update_callback):
"""Unregister data class.""" """Unregister data class."""
if update_callback not in self._data_classes[data_class_entry]["subscriptions"]: self.data_classes[data_class_entry]["subscriptions"].remove(update_callback)
return
self._data_classes[data_class_entry]["subscriptions"].remove(update_callback) if not self.data_classes[data_class_entry].get("subscriptions"):
self._queue.remove(self.data_classes[data_class_entry])
if not self._data_classes[data_class_entry].get("subscriptions"): self.data_classes.pop(data_class_entry)
self._queue.remove(self._data_classes[data_class_entry]) self.data.pop(data_class_entry)
self._data_classes.pop(data_class_entry)
_LOGGER.debug("Data class %s removed", data_class_entry) _LOGGER.debug("Data class %s removed", data_class_entry)
@property @property

View file

@ -1,8 +1,6 @@
"""Support for the Netatmo camera lights.""" """Support for the Netatmo camera lights."""
import logging import logging
import pyatmo
from homeassistant.components.light import LightEntity from homeassistant.components.light import LightEntity
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
@ -34,41 +32,29 @@ async def async_setup_entry(hass, entry, async_add_entities):
await data_handler.register_data_class( await data_handler.register_data_class(
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME, None
) )
data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
if CAMERA_DATA_CLASS_NAME not in data_handler.data: if not data_class or data_class.raw_data == {}:
raise PlatformNotReady raise PlatformNotReady
async def get_entities(): all_cameras = []
"""Retrieve Netatmo entities.""" for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values():
for camera in home.values():
all_cameras.append(camera)
entities = [] entities = [
all_cameras = [] NetatmoLight(
data_handler,
camera["id"],
camera["type"],
camera["home_id"],
)
for camera in all_cameras
if camera["type"] == "NOC"
]
try: _LOGGER.debug("Adding camera lights %s", entities)
for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): async_add_entities(entities, True)
for camera in home.values():
all_cameras.append(camera)
except pyatmo.NoDevice:
_LOGGER.debug("No cameras found")
for camera in all_cameras:
if camera["type"] == "NOC":
_LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"])
entities.append(
NetatmoLight(
data_handler,
camera["id"],
camera["type"],
camera["home_id"],
)
)
return entities
async_add_entities(await get_entities(), True)
await data_handler.unregister_data_class(CAMERA_DATA_CLASS_NAME, None)
class NetatmoLight(NetatmoBase, LightEntity): class NetatmoLight(NetatmoBase, LightEntity):
@ -136,19 +122,19 @@ class NetatmoLight(NetatmoBase, LightEntity):
"""Return true if light is on.""" """Return true if light is on."""
return self._is_on return self._is_on
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn camera floodlight on.""" """Turn camera floodlight on."""
_LOGGER.debug("Turn camera '%s' on", self._name) _LOGGER.debug("Turn camera '%s' on", self._name)
self._data.set_state( await self._data.async_set_state(
home_id=self._home_id, home_id=self._home_id,
camera_id=self._id, camera_id=self._id,
floodlight="on", floodlight="on",
) )
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn camera floodlight into auto mode.""" """Turn camera floodlight into auto mode."""
_LOGGER.debug("Turn camera '%s' to auto mode", self._name) _LOGGER.debug("Turn camera '%s' to auto mode", self._name)
self._data.set_state( await self._data.async_set_state(
home_id=self._home_id, home_id=self._home_id,
camera_id=self._id, camera_id=self._id,
floodlight="auto", floodlight="auto",

View file

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

View file

@ -159,7 +159,7 @@ def async_parse_identifier(
item: MediaSourceItem, item: MediaSourceItem,
) -> tuple[str, str, int | None]: ) -> tuple[str, str, int | None]:
"""Parse identifier.""" """Parse identifier."""
if not item.identifier: if "/" not in item.identifier:
return "events", "", None return "events", "", None
source, path = item.identifier.lstrip("/").split("/", 1) source, path = item.identifier.lstrip("/").split("/", 1)

View file

@ -7,7 +7,7 @@ from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME from .const import DATA_DEVICE_IDS, DOMAIN, MANUFACTURER, MODELS, SIGNAL_NAME
from .data_handler import NetatmoDataHandler from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,7 +29,6 @@ class NetatmoBase(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Entity created.""" """Entity created."""
_LOGGER.debug("New client %s", self.entity_id)
for data_class in self._data_classes: for data_class in self._data_classes:
signal_name = data_class[SIGNAL_NAME] signal_name = data_class[SIGNAL_NAME]
@ -41,7 +40,7 @@ class NetatmoBase(Entity):
home_id=data_class["home_id"], home_id=data_class["home_id"],
) )
elif data_class["name"] == "PublicData": elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME:
await self.data_handler.register_data_class( await self.data_handler.register_data_class(
data_class["name"], data_class["name"],
signal_name, signal_name,
@ -57,7 +56,9 @@ class NetatmoBase(Entity):
data_class["name"], signal_name, self.async_update_callback data_class["name"], signal_name, self.async_update_callback
) )
await self.data_handler.unregister_data_class(signal_name, None) for sub in self.data_handler.data_classes[signal_name].get("subscriptions"):
if sub is None:
await self.data_handler.unregister_data_class(signal_name, None)
registry = await self.hass.helpers.device_registry.async_get_registry() registry = await self.hass.helpers.device_registry.async_get_registry()
device = registry.async_get_device({(DOMAIN, self._id)}, set()) device = registry.async_get_device({(DOMAIN, self._id)}, set())

View file

@ -132,18 +132,8 @@ async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Netatmo weather and homecoach platform.""" """Set up the Netatmo weather and homecoach platform."""
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class(
WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME, None
)
await data_handler.register_data_class(
HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME, None
)
async def find_entities(data_class_name): async def find_entities(data_class_name):
"""Find all entities.""" """Find all entities."""
if data_class_name not in data_handler.data:
raise PlatformNotReady
all_module_infos = {} all_module_infos = {}
data = data_handler.data data = data_handler.data
@ -167,11 +157,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.debug("Skipping module %s", module.get("module_name")) _LOGGER.debug("Skipping module %s", module.get("module_name"))
continue continue
_LOGGER.debug(
"Adding module %s %s",
module.get("module_name"),
module.get("_id"),
)
conditions = [ conditions = [
c.lower() c.lower()
for c in data_class.get_monitored_conditions(module_id=module["_id"]) for c in data_class.get_monitored_conditions(module_id=module["_id"])
@ -188,14 +173,19 @@ async def async_setup_entry(hass, entry, async_add_entities):
NetatmoSensor(data_handler, data_class_name, module, condition) NetatmoSensor(data_handler, data_class_name, module, condition)
) )
await data_handler.unregister_data_class(data_class_name, None) _LOGGER.debug("Adding weather sensors %s", entities)
return entities return entities
for data_class_name in [ for data_class_name in [
WEATHERSTATION_DATA_CLASS_NAME, WEATHERSTATION_DATA_CLASS_NAME,
HOMECOACH_DATA_CLASS_NAME, HOMECOACH_DATA_CLASS_NAME,
]: ]:
await data_handler.register_data_class(data_class_name, data_class_name, None)
data_class = data_handler.data.get(data_class_name)
if not data_class or not data_class.raw_data:
raise PlatformNotReady
async_add_entities(await find_entities(data_class_name), True) async_add_entities(await find_entities(data_class_name), True)
device_registry = await hass.helpers.device_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry()
@ -410,6 +400,8 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
self._state = None self._state = None
return return
self.async_write_ha_state()
def fix_angle(angle: int) -> int: def fix_angle(angle: int) -> int:
"""Fix angle when value is negative.""" """Fix angle when value is negative."""
@ -615,13 +607,6 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
@callback @callback
def async_update_callback(self): def async_update_callback(self):
"""Update the entity's state.""" """Update the entity's state."""
if self._data is None:
if self._state is None:
return
_LOGGER.warning("No data from update")
self._state = None
return
data = None data = None
if self.type == "temperature": if self.type == "temperature":
@ -655,3 +640,5 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
self._state = round(sum(values) / len(values), 1) self._state = round(sum(values) / len(values), 1)
elif self._mode == "max": elif self._mode == "max":
self._state = max(values) self._state = max(values)
self.async_write_ha_state()

View file

@ -1295,7 +1295,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==4.2.3 pyatmo==5.0.1
# homeassistant.components.atome # homeassistant.components.atome
pyatome==0.1.1 pyatome==0.1.1

View file

@ -720,7 +720,7 @@ pyarlo==0.2.4
pyatag==0.3.5.3 pyatag==0.3.5.3
# homeassistant.components.netatmo # homeassistant.components.netatmo
pyatmo==4.2.3 pyatmo==5.0.1
# homeassistant.components.apple_tv # homeassistant.components.apple_tv
pyatv==0.7.7 pyatv==0.7.7

View file

@ -1,5 +1,7 @@
"""Common methods used across tests for Netatmo.""" """Common methods used across tests for Netatmo."""
from contextlib import contextmanager
import json import json
from unittest.mock import patch
from homeassistant.components.webhook import async_handle_webhook from homeassistant.components.webhook import async_handle_webhook
from homeassistant.util.aiohttp import MockRequest from homeassistant.util.aiohttp import MockRequest
@ -35,13 +37,19 @@ FAKE_WEBHOOK_ACTIVATION = {
"push_type": "webhook_activation", "push_type": "webhook_activation",
} }
DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"]
def fake_post_request(**args):
async def fake_post_request(*args, **kwargs):
"""Return fake data.""" """Return fake data."""
if "url" not in args: if "url" not in kwargs:
return "{}" return "{}"
endpoint = args["url"].split("/")[-1] endpoint = kwargs["url"].split("/")[-1]
if endpoint in "snapshot_720.jpg":
return b"test stream image bytes"
if endpoint in [ if endpoint in [
"setpersonsaway", "setpersonsaway",
"setpersonshome", "setpersonshome",
@ -55,7 +63,7 @@ def fake_post_request(**args):
return json.loads(load_fixture(f"netatmo/{endpoint}.json")) return json.loads(load_fixture(f"netatmo/{endpoint}.json"))
def fake_post_request_no_data(**args): async def fake_post_request_no_data(*args, **kwargs):
"""Fake error during requesting backend data.""" """Fake error during requesting backend data."""
return "{}" return "{}"
@ -68,3 +76,12 @@ async def simulate_webhook(hass, webhook_id, response):
) )
await async_handle_webhook(hass, webhook_id, request) await async_handle_webhook(hass, webhook_id, request)
await hass.async_block_till_done() await hass.async_block_till_done()
@contextmanager
def selected_platforms(platforms):
"""Restrict loaded platforms to list given."""
with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch("homeassistant.components.webhook.async_generate_url"):
yield

View file

@ -1,17 +1,16 @@
"""Provide common Netatmo fixtures.""" """Provide common Netatmo fixtures."""
from contextlib import contextmanager
from time import time from time import time
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pytest import pytest
from .common import ALL_SCOPES, TEST_TIME, fake_post_request, fake_post_request_no_data from .common import ALL_SCOPES, fake_post_request
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@pytest.fixture(name="config_entry") @pytest.fixture(name="config_entry")
async def mock_config_entry_fixture(hass): def mock_config_entry_fixture(hass):
"""Mock a config entry.""" """Mock a config entry."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain="netatmo", domain="netatmo",
@ -54,81 +53,13 @@ async def mock_config_entry_fixture(hass):
return mock_entry return mock_entry
@contextmanager @pytest.fixture
def selected_platforms(platforms=["camera", "climate", "light", "sensor"]): def netatmo_auth():
"""Restrict loaded platforms to list given.""" """Restrict loaded platforms to list given."""
with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth"
) as mock_auth, patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch(
"homeassistant.components.webhook.async_generate_url"
):
mock_auth.return_value.post_request.side_effect = fake_post_request
yield
@pytest.fixture(name="entry")
async def mock_entry_fixture(hass, config_entry):
"""Mock setup of all platforms."""
with selected_platforms():
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="sensor_entry")
async def mock_sensor_entry_fixture(hass, config_entry):
"""Mock setup of sensor platform."""
with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
yield config_entry
@pytest.fixture(name="camera_entry")
async def mock_camera_entry_fixture(hass, config_entry):
"""Mock setup of camera platform."""
with selected_platforms(["camera"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="light_entry")
async def mock_light_entry_fixture(hass, config_entry):
"""Mock setup of light platform."""
with selected_platforms(["light"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="climate_entry")
async def mock_climate_entry_fixture(hass, config_entry):
"""Mock setup of climate platform."""
with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="entry_error")
async def mock_entry_error_fixture(hass, config_entry):
"""Mock erroneous setup of platforms."""
with patch( with patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth, patch( ) as mock_auth:
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", mock_auth.return_value.async_post_request.side_effect = fake_post_request
), patch( mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
"homeassistant.components.webhook.async_generate_url" mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
): yield
mock_auth.return_value.post_request.side_effect = fake_post_request_no_data
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
yield config_entry

View file

@ -1,6 +1,9 @@
"""The tests for Netatmo camera.""" """The tests for Netatmo camera."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pyatmo
import pytest
from homeassistant.components import camera from homeassistant.components import camera
from homeassistant.components.camera import STATE_STREAMING from homeassistant.components.camera import STATE_STREAMING
@ -13,14 +16,19 @@ from homeassistant.components.netatmo.const import (
from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.util import dt from homeassistant.util import dt
from .common import fake_post_request, simulate_webhook from .common import fake_post_request, selected_platforms, simulate_webhook
from tests.common import async_capture_events, async_fire_time_changed from tests.common import async_capture_events, async_fire_time_changed
async def test_setup_component_with_webhook(hass, camera_entry): async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
"""Test setup with webhook.""" """Test setup with webhook."""
webhook_id = camera_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["camera"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
await hass.async_block_till_done() await hass.async_block_till_done()
camera_entity_indoor = "camera.netatmo_hall" camera_entity_indoor = "camera.netatmo_hall"
@ -58,7 +66,7 @@ async def test_setup_component_with_webhook(hass, camera_entry):
} }
await simulate_webhook(hass, webhook_id, response) await simulate_webhook(hass, webhook_id, response)
assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).state == "streaming"
assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "on" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "on"
response = { response = {
@ -84,12 +92,39 @@ async def test_setup_component_with_webhook(hass, camera_entry):
assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_indoor).state == "streaming"
assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto"
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call(
"camera", "turn_off", service_data={"entity_id": "camera.netatmo_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",
)
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call(
"camera", "turn_on", service_data={"entity_id": "camera.netatmo_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",
)
IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" IMAGE_BYTES_FROM_STREAM = b"test stream image bytes"
async def test_camera_image_local(hass, camera_entry, requests_mock): async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_auth):
"""Test retrieval or local camera image.""" """Test retrieval or local camera image."""
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() await hass.async_block_till_done()
uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d"
@ -111,8 +146,13 @@ async def test_camera_image_local(hass, camera_entry, requests_mock):
assert image.content == IMAGE_BYTES_FROM_STREAM assert image.content == IMAGE_BYTES_FROM_STREAM
async def test_camera_image_vpn(hass, camera_entry, requests_mock): async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth):
"""Test retrieval of remote camera image.""" """Test retrieval of remote camera image."""
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() await hass.async_block_till_done()
uri = ( uri = (
@ -137,8 +177,13 @@ async def test_camera_image_vpn(hass, camera_entry, requests_mock):
assert image.content == IMAGE_BYTES_FROM_STREAM assert image.content == IMAGE_BYTES_FROM_STREAM
async def test_service_set_person_away(hass, camera_entry): async def test_service_set_person_away(hass, config_entry, netatmo_auth):
"""Test service to set person as away.""" """Test service to set person as away."""
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() await hass.async_block_till_done()
data = { data = {
@ -146,7 +191,9 @@ async def test_service_set_person_away(hass, camera_entry):
"person": "Richard Doe", "person": "Richard Doe",
} }
with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: with patch(
"pyatmo.camera.AsyncCameraData.async_set_persons_away"
) as mock_set_persons_away:
await hass.services.async_call( await hass.services.async_call(
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
) )
@ -160,7 +207,9 @@ async def test_service_set_person_away(hass, camera_entry):
"entity_id": "camera.netatmo_hall", "entity_id": "camera.netatmo_hall",
} }
with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: with patch(
"pyatmo.camera.AsyncCameraData.async_set_persons_away"
) as mock_set_persons_away:
await hass.services.async_call( await hass.services.async_call(
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
) )
@ -171,8 +220,13 @@ async def test_service_set_person_away(hass, camera_entry):
) )
async def test_service_set_persons_home(hass, camera_entry): async def test_service_set_persons_home(hass, config_entry, netatmo_auth):
"""Test service to set persons as home.""" """Test service to set persons as home."""
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() await hass.async_block_till_done()
data = { data = {
@ -180,7 +234,9 @@ async def test_service_set_persons_home(hass, camera_entry):
"persons": "John Doe", "persons": "John Doe",
} }
with patch("pyatmo.camera.CameraData.set_persons_home") as mock_set_persons_home: with patch(
"pyatmo.camera.AsyncCameraData.async_set_persons_home"
) as mock_set_persons_home:
await hass.services.async_call( await hass.services.async_call(
"netatmo", SERVICE_SET_PERSONS_HOME, service_data=data "netatmo", SERVICE_SET_PERSONS_HOME, service_data=data
) )
@ -191,8 +247,13 @@ async def test_service_set_persons_home(hass, camera_entry):
) )
async def test_service_set_camera_light(hass, camera_entry): async def test_service_set_camera_light(hass, config_entry, netatmo_auth):
"""Test service to set the outdoor camera light mode.""" """Test service to set the outdoor 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() await hass.async_block_till_done()
data = { data = {
@ -200,7 +261,7 @@ async def test_service_set_camera_light(hass, camera_entry):
"camera_light_mode": "on", "camera_light_mode": "on",
} }
with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call( await hass.services.async_call(
"netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data "netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data
) )
@ -214,16 +275,26 @@ async def test_service_set_camera_light(hass, camera_entry):
async def test_camera_reconnect_webhook(hass, config_entry): async def test_camera_reconnect_webhook(hass, config_entry):
"""Test webhook event on camera reconnect.""" """Test webhook event on camera reconnect."""
fake_post_hits = 0
async def fake_post(*args, **kwargs):
"""Fake error during requesting backend data."""
nonlocal fake_post_hits
fake_post_hits += 1
return await fake_post_request(*args, **kwargs)
with patch( with patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth.post_request" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_post, patch( ) as mock_auth, patch(
"homeassistant.components.netatmo.PLATFORMS", ["camera"] "homeassistant.components.netatmo.PLATFORMS", ["camera"]
), patch( ), patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch( ), patch(
"homeassistant.components.webhook.async_generate_url" "homeassistant.components.webhook.async_generate_url"
) as mock_webhook: ) as mock_webhook:
mock_post.side_effect = fake_post_request mock_auth.return_value.async_post_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" mock_webhook.return_value = "https://example.com"
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
@ -238,8 +309,9 @@ async def test_camera_reconnect_webhook(hass, config_entry):
await simulate_webhook(hass, webhook_id, response) await simulate_webhook(hass, webhook_id, response)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_post.assert_called() assert fake_post_hits == 5
mock_post.reset_mock()
calls = fake_post_hits
# Fake camera reconnect # Fake camera reconnect
response = { response = {
@ -253,11 +325,16 @@ async def test_camera_reconnect_webhook(hass, config_entry):
dt.utcnow() + timedelta(seconds=60), dt.utcnow() + timedelta(seconds=60),
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_post.assert_called() assert fake_post_hits > calls
async def test_webhook_person_event(hass, camera_entry): async def test_webhook_person_event(hass, config_entry, netatmo_auth):
"""Test that person events are handled.""" """Test that person events are handled."""
with selected_platforms(["camera"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
test_netatmo_event = async_capture_events(hass, NETATMO_EVENT) test_netatmo_event = async_capture_events(hass, NETATMO_EVENT)
assert not test_netatmo_event assert not test_netatmo_event
@ -282,7 +359,80 @@ async def test_webhook_person_event(hass, camera_entry):
"push_type": "NACamera-person", "push_type": "NACamera-person",
} }
webhook_id = camera_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
await simulate_webhook(hass, webhook_id, fake_webhook_event) await simulate_webhook(hass, webhook_id, fake_webhook_event)
assert test_netatmo_event assert test_netatmo_event
async def test_setup_component_no_devices(hass, config_entry):
"""Test setup with no devices."""
fake_post_hits = 0
async def fake_post_no_data(*args, **kwargs):
"""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.webhook.async_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 == 1
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(
"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.webhook.async_generate_url"
):
mock_auth.return_value.async_post_request.side_effect = fake_post
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()
camera_entity_indoor = "camera.netatmo_hall"
with pytest.raises(Exception) as excinfo:
await camera.async_get_image(hass, camera_entity_indoor)
assert excinfo.value.args == ("Unable to get image",)
assert fake_post_hits == 6

View file

@ -26,12 +26,17 @@ from homeassistant.components.netatmo.const import (
) )
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID
from .common import simulate_webhook from .common import selected_platforms, simulate_webhook
async def test_webhook_event_handling_thermostats(hass, climate_entry): async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_auth):
"""Test service and webhook event handling with thermostats.""" """Test service and webhook event handling with thermostats."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_livingroom = "climate.netatmo_livingroom" climate_entity_livingroom = "climate.netatmo_livingroom"
assert hass.states.get(climate_entity_livingroom).state == "auto" assert hass.states.get(climate_entity_livingroom).state == "auto"
@ -199,9 +204,16 @@ async def test_webhook_event_handling_thermostats(hass, climate_entry):
) )
async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): async def test_service_preset_mode_frost_guard_thermostat(
hass, config_entry, netatmo_auth
):
"""Test service with frost guard preset for thermostats.""" """Test service with frost guard preset for thermostats."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_livingroom = "climate.netatmo_livingroom" climate_entity_livingroom = "climate.netatmo_livingroom"
assert hass.states.get(climate_entity_livingroom).state == "auto" assert hass.states.get(climate_entity_livingroom).state == "auto"
@ -267,9 +279,14 @@ async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry):
) )
async def test_service_preset_modes_thermostat(hass, climate_entry): async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth):
"""Test service with preset modes for thermostats.""" """Test service with preset modes for thermostats."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_livingroom = "climate.netatmo_livingroom" climate_entity_livingroom = "climate.netatmo_livingroom"
assert hass.states.get(climate_entity_livingroom).state == "auto" assert hass.states.get(climate_entity_livingroom).state == "auto"
@ -341,10 +358,15 @@ async def test_service_preset_modes_thermostat(hass, climate_entry):
assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30
async def test_webhook_event_handling_no_data(hass, climate_entry): async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth):
"""Test service and webhook event handling with erroneous data.""" """Test service and webhook event handling with erroneous data."""
with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Test webhook without home entry # Test webhook without home entry
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
response = { response = {
"push_type": "home_event_changed", "push_type": "home_event_changed",
@ -385,14 +407,19 @@ async def test_webhook_event_handling_no_data(hass, climate_entry):
await simulate_webhook(hass, webhook_id, response) await simulate_webhook(hass, webhook_id, response)
async def test_service_schedule_thermostats(hass, climate_entry, caplog): async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_auth):
"""Test service for selecting Netatmo schedule with thermostats.""" """Test service for selecting Netatmo schedule with thermostats."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_livingroom = "climate.netatmo_livingroom" climate_entity_livingroom = "climate.netatmo_livingroom"
# Test setting a valid schedule # Test setting a valid schedule
with patch( with patch(
"pyatmo.thermostat.HomeData.switch_home_schedule" "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule"
) as mock_switch_home_schedule: ) as mock_switch_home_schedule:
await hass.services.async_call( await hass.services.async_call(
"netatmo", "netatmo",
@ -421,7 +448,7 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog):
# Test setting an invalid schedule # Test setting an invalid schedule
with patch( with patch(
"pyatmo.thermostat.HomeData.switch_home_schedule" "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule"
) as mock_switch_home_schedule: ) as mock_switch_home_schedule:
await hass.services.async_call( await hass.services.async_call(
"netatmo", "netatmo",
@ -435,9 +462,16 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog):
assert "summer is not a valid schedule" in caplog.text assert "summer is not a valid schedule" in caplog.text
async def test_service_preset_mode_already_boost_valves(hass, climate_entry): async def test_service_preset_mode_already_boost_valves(
hass, config_entry, netatmo_auth
):
"""Test service with boost preset for valves when already in boost mode.""" """Test service with boost preset for valves when already in boost mode."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
assert hass.states.get(climate_entity_entrada).state == "auto" assert hass.states.get(climate_entity_entrada).state == "auto"
@ -508,9 +542,14 @@ async def test_service_preset_mode_already_boost_valves(hass, climate_entry):
assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30
async def test_service_preset_mode_boost_valves(hass, climate_entry): async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth):
"""Test service with boost preset for valves.""" """Test service with boost preset for valves."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
# Test service setting the preset mode to "boost" # Test service setting the preset mode to "boost"
@ -553,8 +592,13 @@ async def test_service_preset_mode_boost_valves(hass, climate_entry):
assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30
async def test_service_preset_mode_invalid(hass, climate_entry, caplog): async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_auth):
"""Test service with invalid preset.""" """Test service with invalid preset."""
with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
@ -566,9 +610,14 @@ async def test_service_preset_mode_invalid(hass, climate_entry, caplog):
assert "Preset mode 'invalid' not available" in caplog.text assert "Preset mode 'invalid' not available" in caplog.text
async def test_valves_service_turn_off(hass, climate_entry): async def test_valves_service_turn_off(hass, config_entry, netatmo_auth):
"""Test service turn off for valves.""" """Test service turn off for valves."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
# Test turning valve off # Test turning valve off
@ -606,9 +655,14 @@ async def test_valves_service_turn_off(hass, climate_entry):
assert hass.states.get(climate_entity_entrada).state == "off" assert hass.states.get(climate_entity_entrada).state == "off"
async def test_valves_service_turn_on(hass, climate_entry): async def test_valves_service_turn_on(hass, config_entry, netatmo_auth):
"""Test service turn on for valves.""" """Test service turn on for valves."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
# Test turning valve on # Test turning valve on
@ -661,9 +715,14 @@ async def test_get_all_home_ids():
assert climate.get_all_home_ids(home_data) == expected assert climate.get_all_home_ids(home_data) == expected
async def test_webhook_home_id_mismatch(hass, climate_entry): async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth):
"""Test service turn on for valves.""" """Test service turn on for valves."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
assert hass.states.get(climate_entity_entrada).state == "auto" assert hass.states.get(climate_entity_entrada).state == "auto"
@ -694,9 +753,14 @@ async def test_webhook_home_id_mismatch(hass, climate_entry):
assert hass.states.get(climate_entity_entrada).state == "auto" assert hass.states.get(climate_entity_entrada).state == "auto"
async def test_webhook_set_point(hass, climate_entry): async def test_webhook_set_point(hass, config_entry, netatmo_auth):
"""Test service turn on for valves.""" """Test service turn on for valves."""
webhook_id = climate_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["climate"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.netatmo_entrada"
# Fake backend response for valve being turned on # Fake backend response for valve being turned on

View file

@ -1,15 +1,26 @@
"""The tests for Netatmo component.""" """The tests for Netatmo component."""
import asyncio
from datetime import timedelta
from time import time from time import time
from unittest.mock import patch from unittest.mock import AsyncMock, patch
import pyatmo
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.netatmo import DOMAIN from homeassistant.components.netatmo import DOMAIN
from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import CoreState
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from .common import FAKE_WEBHOOK_ACTIVATION, fake_post_request, simulate_webhook from .common import (
FAKE_WEBHOOK_ACTIVATION,
fake_post_request,
selected_platforms,
simulate_webhook,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.cloud import mock_cloud from tests.components.cloud import mock_cloud
# Fake webhook thermostat mode change to "Max" # Fake webhook thermostat mode change to "Max"
@ -57,13 +68,15 @@ async def test_setup_component(hass):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
) as mock_auth, patch( ) as mock_auth, patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
) as mock_impl, patch( ) as mock_impl, patch(
"homeassistant.components.webhook.async_generate_url" "homeassistant.components.webhook.async_generate_url"
) as mock_webhook: ) as mock_webhook:
mock_auth.return_value.post_request.side_effect = fake_post_request mock_auth.return_value.async_post_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", {}) assert await async_setup_component(hass, "netatmo", {})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -86,38 +99,54 @@ async def test_setup_component(hass):
async def test_setup_component_with_config(hass, config_entry): async def test_setup_component_with_config(hass, config_entry):
"""Test setup of the netatmo component with dev account.""" """Test setup of the netatmo component with dev account."""
fake_post_hits = 0
async def fake_post(*args, **kwargs):
"""Fake error during requesting backend data."""
nonlocal fake_post_hits
fake_post_hits += 1
return await fake_post_request(*args, **kwargs)
with patch( with patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
) as mock_impl, patch( ) as mock_impl, patch(
"homeassistant.components.webhook.async_generate_url" "homeassistant.components.webhook.async_generate_url"
) as mock_webhook, patch( ) as mock_webhook, patch(
"pyatmo.auth.NetatmoOAuth2.post_request" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
) as fake_post_requests, patch( ) as mock_auth, patch(
"homeassistant.components.netatmo.PLATFORMS", ["sensor"] "homeassistant.components.netatmo.PLATFORMS", ["sensor"]
): ):
mock_auth.return_value.async_post_request.side_effect = fake_post
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
assert await async_setup_component( assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
fake_post_requests.assert_called() assert fake_post_hits == 3
mock_impl.assert_called_once() mock_impl.assert_called_once()
mock_webhook.assert_called_once() mock_webhook.assert_called_once()
assert config_entry.state == config_entries.ENTRY_STATE_LOADED assert hass.config_entries.async_entries(DOMAIN)
assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0
assert len(hass.states.async_all()) > 0
async def test_setup_component_with_webhook(hass, entry): async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
"""Test setup and teardown of the netatmo component with webhook registration.""" """Test setup and teardown of the netatmo component with webhook registration."""
webhook_id = entry.data[CONF_WEBHOOK_ID] with selected_platforms(["camera", "climate", "light", "sensor"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
assert len(hass.states.async_all()) > 0 assert len(hass.states.async_all()) > 0
webhook_id = entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
# Assert webhook is established successfully # Assert webhook is established successfully
@ -134,36 +163,30 @@ async def test_setup_component_with_webhook(hass, entry):
assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert len(hass.config_entries.async_entries(DOMAIN)) == 0
async def test_setup_without_https(hass, config_entry): async def test_setup_without_https(hass, config_entry, caplog):
"""Test if set up with cloud link and without https.""" """Test if set up with cloud link and without https."""
hass.config.components.add("cloud") hass.config.components.add("cloud")
with patch( with patch(
"homeassistant.helpers.network.get_url", "homeassistant.helpers.network.get_url",
return_value="https://example.nabu.casa", return_value="http://example.nabu.casa",
), patch( ), patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth, patch( ) as mock_auth, patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch( ), patch(
"homeassistant.components.webhook.async_generate_url" "homeassistant.components.webhook.async_generate_url"
) as mock_webhook: ) as mock_async_generate_url:
mock_auth.return_value.post_request.side_effect = fake_post_request mock_auth.return_value.async_post_request.side_effect = fake_post_request
mock_webhook.return_value = "https://example.com" mock_async_generate_url.return_value = "http://example.com"
assert await async_setup_component( assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_auth.assert_called_once()
mock_async_generate_url.assert_called_once()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] assert "https and port 443 is required to register the webhook" in caplog.text
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
# Assert webhook is established successfully
climate_entity_livingroom = "climate.netatmo_livingroom"
assert hass.states.get(climate_entity_livingroom).state == "auto"
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK)
await hass.async_block_till_done()
assert hass.states.get(climate_entity_livingroom).state == "heat"
async def test_setup_with_cloud(hass, config_entry): async def test_setup_with_cloud(hass, config_entry):
@ -181,7 +204,7 @@ async def test_setup_with_cloud(hass, config_entry):
) as fake_create_cloudhook, patch( ) as fake_create_cloudhook, patch(
"homeassistant.components.cloud.async_delete_cloudhook" "homeassistant.components.cloud.async_delete_cloudhook"
) as fake_delete_cloudhook, patch( ) as fake_delete_cloudhook, patch(
"homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth, patch( ) as mock_auth, patch(
"homeassistant.components.netatmo.PLATFORMS", [] "homeassistant.components.netatmo.PLATFORMS", []
), patch( ), patch(
@ -189,7 +212,7 @@ async def test_setup_with_cloud(hass, config_entry):
), patch( ), patch(
"homeassistant.components.webhook.async_generate_url" "homeassistant.components.webhook.async_generate_url"
): ):
mock_auth.return_value.post_request.side_effect = fake_post_request mock_auth.return_value.async_post_request.side_effect = fake_post_request
assert await async_setup_component( assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
) )
@ -210,3 +233,199 @@ async def test_setup_with_cloud(hass, config_entry):
await hass.async_block_till_done() await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN) assert not hass.config_entries.async_entries(DOMAIN)
async def test_setup_with_cloudhook(hass):
"""Test if set up with active cloud subscription and cloud hook."""
config_entry = MockConfigEntry(
domain="netatmo",
data={
"auth_implementation": "cloud",
"cloudhook_url": "https://hooks.nabu.casa/ABCD",
"token": {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
"expires_at": time() + 1000,
"scope": "read_station",
},
},
)
config_entry.add_to_hass(hass)
await mock_cloud(hass)
await hass.async_block_till_done()
with patch(
"homeassistant.components.cloud.async_is_logged_in", return_value=True
), patch(
"homeassistant.components.cloud.async_active_subscription", return_value=True
), patch(
"homeassistant.components.cloud.async_create_cloudhook",
return_value="https://hooks.nabu.casa/ABCD",
) as fake_create_cloudhook, patch(
"homeassistant.components.cloud.async_delete_cloudhook"
) as fake_delete_cloudhook, patch(
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth, patch(
"homeassistant.components.netatmo.PLATFORMS", []
), patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch(
"homeassistant.components.webhook.async_generate_url"
):
mock_auth.return_value.async_post_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", {})
assert hass.components.cloud.async_active_subscription() is True
assert (
hass.config_entries.async_entries("netatmo")[0].data["cloudhook_url"]
== "https://hooks.nabu.casa/ABCD"
)
await hass.async_block_till_done()
assert hass.config_entries.async_entries(DOMAIN)
fake_create_cloudhook.assert_not_called()
for config_entry in hass.config_entries.async_entries("netatmo"):
await hass.config_entries.async_remove(config_entry.entry_id)
fake_delete_cloudhook.assert_called_once()
await hass.async_block_till_done()
assert not hass.config_entries.async_entries(DOMAIN)
async def test_setup_component_api_error(hass):
"""Test error on setup of the netatmo component."""
config_entry = MockConfigEntry(
domain="netatmo",
data={
"auth_implementation": "cloud",
"token": {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
"expires_at": time() + 1000,
"scope": "read_station",
},
},
)
config_entry.add_to_hass(hass)
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.webhook.async_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):
"""Test timeout on setup of the netatmo component."""
config_entry = MockConfigEntry(
domain="netatmo",
data={
"auth_implementation": "cloud",
"token": {
"refresh_token": "mock-refresh-token",
"access_token": "mock-access-token",
"type": "Bearer",
"expires_in": 60,
"expires_at": time() + 1000,
"scope": "read_station",
},
},
)
config_entry.add_to_hass(hass)
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.webhook.async_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
with patch(
"pyatmo.AbstractAsyncAuth.async_addwebhook", side_effect=AsyncMock()
) as mock_addwebhook, patch(
"pyatmo.AbstractAsyncAuth.async_dropwebhook", side_effect=AsyncMock()
) as mock_dropwebhook, patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
) as mock_impl, patch(
"homeassistant.components.webhook.async_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"]
):
assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
)
await hass.async_block_till_done()
assert mock_post_request.call_count == 5
mock_impl.assert_called_once()
mock_webhook.assert_not_called()
await hass.async_start()
await hass.async_block_till_done()
mock_webhook.assert_called_once()
# Fake webhook activation
await simulate_webhook(
hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION
)
await hass.async_block_till_done()
mock_addwebhook.assert_called_once()
mock_dropwebhook.assert_not_awaited()
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(seconds=60),
)
await hass.async_block_till_done()
assert hass.config_entries.async_entries(DOMAIN)
assert len(hass.states.async_all()) > 0
await hass.async_stop()
mock_dropwebhook.assert_called_once()

View file

@ -1,19 +1,25 @@
"""The tests for Netatmo light.""" """The tests for Netatmo light."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from homeassistant.components.light import ( from homeassistant.components.light import (
DOMAIN as LIGHT_DOMAIN, DOMAIN as LIGHT_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
) )
from homeassistant.components.netatmo import DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID
from .common import FAKE_WEBHOOK_ACTIVATION, simulate_webhook from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhook
async def test_light_setup_and_services(hass, light_entry): async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
"""Test setup and services.""" """Test setup and services."""
webhook_id = light_entry.data[CONF_WEBHOOK_ID] with selected_platforms(["light"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
# Fake webhook activation # Fake webhook activation
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
@ -45,7 +51,7 @@ async def test_light_setup_and_services(hass, light_entry):
assert hass.states.get(light_entity).state == "on" assert hass.states.get(light_entity).state == "on"
# Test turning light off # Test turning light off
with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
@ -60,7 +66,7 @@ async def test_light_setup_and_services(hass, light_entry):
) )
# Test turning light on # Test turning light on
with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -73,3 +79,43 @@ async def test_light_setup_and_services(hass, light_entry):
camera_id="12:34:56:00:a5:a4", camera_id="12:34:56:00:a5:a4",
floodlight="on", floodlight="on",
) )
async def test_setup_component_no_devices(hass, config_entry):
"""Test setup with no devices."""
fake_post_hits = 0
async def fake_post_request_no_data(*args, **kwargs):
"""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", ["light"]
), patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch(
"homeassistant.components.webhook.async_generate_url"
):
mock_auth.return_value.async_post_request.side_effect = (
fake_post_request_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()
# Fake webhook activation
await simulate_webhook(
hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION
)
await hass.async_block_till_done()
assert fake_post_hits == 1
assert hass.config_entries.async_entries(DOMAIN)
assert len(hass.states.async_all()) == 0

View file

@ -51,6 +51,16 @@ async def test_async_browse_media(hass):
) )
assert str(excinfo.value) == "Unknown source directory." assert str(excinfo.value) == "Unknown source directory."
# Test invalid base
with pytest.raises(ValueError) as excinfo:
await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/")
assert str(excinfo.value) == "Invalid media source URI"
# Test successful listing
media = await media_source.async_browse_media(
hass, f"{const.URI_SCHEME}{DOMAIN}/events"
)
# Test successful listing # Test successful listing
media = await media_source.async_browse_media( media = await media_source.async_browse_media(
hass, f"{const.URI_SCHEME}{DOMAIN}/events/" hass, f"{const.URI_SCHEME}{DOMAIN}/events/"

View file

@ -1,23 +1,22 @@
"""The tests for the Netatmo sensor platform.""" """The tests for the Netatmo sensor platform."""
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from homeassistant.components.netatmo import sensor from homeassistant.components.netatmo import sensor
from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt
from .common import TEST_TIME from .common import TEST_TIME, selected_platforms
from .conftest import selected_platforms
from tests.common import async_fire_time_changed
async def test_weather_sensor(hass, sensor_entry): async def test_weather_sensor(hass, config_entry, netatmo_auth):
"""Test weather sensor setup.""" """Test weather sensor setup."""
with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
prefix = "sensor.netatmo_mystation_" prefix = "sensor.netatmo_mystation_"
assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}temperature").state == "24.6"
@ -26,8 +25,15 @@ async def test_weather_sensor(hass, sensor_entry):
assert hass.states.get(f"{prefix}pressure").state == "1017.3" assert hass.states.get(f"{prefix}pressure").state == "1017.3"
async def test_public_weather_sensor(hass, sensor_entry): async def test_public_weather_sensor(hass, config_entry, netatmo_auth):
"""Test public weather sensor setup.""" """Test public weather sensor setup."""
with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) > 0
prefix = "sensor.netatmo_home_max_" prefix = "sensor.netatmo_home_max_"
assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}temperature").state == "27.4"
@ -40,7 +46,6 @@ async def test_public_weather_sensor(hass, sensor_entry):
assert hass.states.get(f"{prefix}humidity").state == "63.2" assert hass.states.get(f"{prefix}humidity").state == "63.2"
assert hass.states.get(f"{prefix}pressure").state == "1010.3" assert hass.states.get(f"{prefix}pressure").state == "1010.3"
assert len(hass.states.async_all()) > 0
entities_before_change = len(hass.states.async_all()) entities_before_change = len(hass.states.async_all())
valid_option = { valid_option = {
@ -53,7 +58,7 @@ async def test_public_weather_sensor(hass, sensor_entry):
"mode": "max", "mode": "max",
} }
result = await hass.config_entries.options.async_init(sensor_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={"new_area": "Home avg"} result["flow_id"], user_input={"new_area": "Home avg"}
) )
@ -63,18 +68,11 @@ async def test_public_weather_sensor(hass, sensor_entry):
result = await hass.config_entries.options.async_configure( result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={} result["flow_id"], user_input={}
) )
await hass.async_block_till_done()
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
assert hass.states.get(f"{prefix}temperature").state == "27.4" await hass.async_block_till_done()
assert hass.states.get(f"{prefix}humidity").state == "76"
assert hass.states.get(f"{prefix}pressure").state == "1014.4"
assert len(hass.states.async_all()) == entities_before_change assert len(hass.states.async_all()) == entities_before_change
assert hass.states.get(f"{prefix}temperature").state == "27.4"
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -213,7 +211,9 @@ async def test_fix_angle(angle, expected):
), ),
], ],
) )
async def test_weather_sensor_enabling(hass, config_entry, uid, name, expected): async def test_weather_sensor_enabling(
hass, config_entry, uid, name, expected, netatmo_auth
):
"""Test enabling of by default disabled sensors.""" """Test enabling of by default disabled sensors."""
with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]):
states_before = len(hass.states.async_all()) states_before = len(hass.states.async_all())

View file

@ -27,7 +27,6 @@
"type": "NATherm1", "type": "NATherm1",
"firmware_revision": 65, "firmware_revision": 65,
"rf_strength": 58, "rf_strength": 58,
"battery_level": 3793,
"boiler_valve_comfort_boost": false, "boiler_valve_comfort_boost": false,
"boiler_status": false, "boiler_status": false,
"anticipating": false, "anticipating": false,
@ -40,7 +39,6 @@
"type": "NRV", "type": "NRV",
"firmware_revision": 79, "firmware_revision": 79,
"rf_strength": 51, "rf_strength": 51,
"battery_level": 3025,
"bridge": "12:34:56:00:fa:d0", "bridge": "12:34:56:00:fa:d0",
"battery_state": "full" "battery_state": "full"
}, },
@ -50,7 +48,6 @@
"type": "NRV", "type": "NRV",
"firmware_revision": 79, "firmware_revision": 79,
"rf_strength": 59, "rf_strength": 59,
"battery_level": 2329,
"bridge": "12:34:56:00:fa:d0", "bridge": "12:34:56:00:fa:d0",
"battery_state": "full" "battery_state": "full"
} }