diff --git a/.coveragerc b/.coveragerc index 72d6e1e92933..d406698cbb11 100644 --- a/.coveragerc +++ b/.coveragerc @@ -635,8 +635,6 @@ omit = homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py - homeassistant/components/netatmo/climate.py - homeassistant/components/netatmo/const.py homeassistant/components/netatmo/data_handler.py homeassistant/components/netatmo/helper.py homeassistant/components/netatmo/light.py diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index c2a6e484771c..3b05e263f023 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,6 +2,7 @@ import logging from typing import List, Optional +import pyatmo import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -251,7 +252,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Handle webhook events.""" data = event["data"] - if not data.get("home"): + if data.get("home") is None: return home = data["home"] @@ -569,7 +570,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id = sid if not schedule_id: - _LOGGER.error("You passed an invalid schedule") + _LOGGER.error( + "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME) + ) return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) @@ -586,7 +589,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {**super().device_info, "suggested_area": self._room_data["name"]} -def interpolate(batterylevel, module_type): +def interpolate(batterylevel: int, module_type: str) -> int: """Interpolate battery level depending on device type.""" na_battery_levels = { NA_THERM: { @@ -628,7 +631,7 @@ def interpolate(batterylevel, module_type): return int(pct) -def get_all_home_ids(home_data): +def get_all_home_ids(home_data: pyatmo.HomeData) -> List[str]: """Get all the home ids returned by NetAtmo API.""" if home_data is None: return [] diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py new file mode 100644 index 000000000000..c8014a9b2a93 --- /dev/null +++ b/tests/components/netatmo/common.py @@ -0,0 +1,44 @@ +"""Common methods used across tests for Netatmo.""" +import json + +from tests.common import load_fixture + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +ALL_SCOPES = [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "write_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", +] + + +def fake_post_request(**args): + """Return fake data.""" + if "url" not in args: + return "{}" + + endpoint = args["url"].split("/")[-1] + if endpoint in [ + "setpersonsaway", + "setpersonshome", + "setstate", + "setroomthermpoint", + "setthermmode", + "switchhomeschedule", + ]: + return f'{{"{endpoint}": true}}' + + return json.loads(load_fixture(f"netatmo/{endpoint}.json")) + + +def fake_post_request_no_data(**args): + """Fake error during requesting backend data.""" + return "{}" diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py new file mode 100644 index 000000000000..b18b70f323ec --- /dev/null +++ b/tests/components/netatmo/conftest.py @@ -0,0 +1,127 @@ +"""Provide common Netatmo fixtures.""" +from contextlib import contextmanager +from time import time +from unittest.mock import patch + +import pytest + +from .common import ALL_SCOPES, fake_post_request, fake_post_request_no_data + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +async def mock_config_entry_fixture(hass): + """Mock a config entry.""" + mock_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": " ".join(ALL_SCOPES), + }, + }, + options={ + "weather_areas": { + "Home avg": { + "lat_ne": 32.2345678, + "lon_ne": -117.1234567, + "lat_sw": 32.1234567, + "lon_sw": -117.2345678, + "show_on_map": False, + "area_name": "Home avg", + "mode": "avg", + }, + "Home max": { + "lat_ne": 32.2345678, + "lon_ne": -117.1234567, + "lat_sw": 32.1234567, + "lon_sw": -117.2345678, + "show_on_map": True, + "area_name": "Home max", + "mode": "max", + }, + } + }, + ) + mock_entry.add_to_hass(hass) + + return mock_entry + + +@contextmanager +def selected_platforms(platforms=["camera", "climate", "light", "sensor"]): + """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) + return config_entry + + +@pytest.fixture(name="sensor_entry") +async def mock_sensor_entry_fixture(hass, config_entry): + """Mock setup of sensor platform.""" + with selected_platforms(["sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + return 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) + 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) + 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( + "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_no_data + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + return config_entry diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py new file mode 100644 index 000000000000..e7ad16697696 --- /dev/null +++ b/tests/components/netatmo/test_climate.py @@ -0,0 +1,539 @@ +"""The tests for the Netatmo climate platform.""" +from unittest.mock import Mock + +import pytest + +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_BOOST, +) +from homeassistant.components.netatmo import climate +from homeassistant.components.netatmo.climate import ( + NA_THERM, + NA_VALVE, + PRESET_FROST_GUARD, + PRESET_SCHEDULE, +) +from homeassistant.components.netatmo.const import ( + ATTR_SCHEDULE_NAME, + SERVICE_SET_SCHEDULE, +) +from homeassistant.components.webhook import async_handle_webhook +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID +from homeassistant.util.aiohttp import MockRequest + + +async def simulate_webhook(hass, webhook_id, response): + """Simulate a webhook event.""" + request = MockRequest(content=response, mock_source="test") + await async_handle_webhook(hass, webhook_id, request) + await hass.async_block_till_done() + + +async def test_webhook_event_handling_thermostats(hass, climate_entry): + """Test service and webhook event handling with thermostats.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.netatmo_livingroom" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 12 + + # Test service setting the temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_TEMPERATURE: 21}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat manual set point + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2746182631",' + b'"home": { "id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "country": "DE",' + b'"rooms": [{ "id": "2746182631", "name": "Livingroom", "type": "livingroom",' + b'"therm_setpoint_mode": "manual", "therm_setpoint_temperature": 21,' + b'"therm_setpoint_end_time": 1612734552}], "modules": [{"id": "12:34:56:00:01:ae",' + b'"name": "Livingroom", "type": "NATherm1"}]}, "mode": "manual", "event_type": "set_point",' + b'"temperature": 21, "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 21 + + # Test service setting the HVAC mode to "heat" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat mode change to "Max" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2746182631",' + b'"home": {"id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "country": "DE",' + b'"rooms": [{"id": "2746182631", "name": "Livingroom", "type": "livingroom",' + b'"therm_setpoint_mode": "max", "therm_setpoint_end_time": 1612749189}],' + b'"modules": [{"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}]},' + b'"mode": "max", "event_type": "set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 + + # Test service setting the HVAC mode to "off" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook turn thermostat off + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2746182631",' + b'"home": {"id": "91763b24c43d3e344f424e8b","name": "MYHOME","country": "DE",' + b'"rooms": [{"id": "2746182631","name": "Livingroom","type": "livingroom",' + b'"therm_setpoint_mode": "off"}],"modules": [{"id": "12:34:56:00:01:ae",' + b'"name": "Livingroom", "type": "NATherm1"}]}, "mode": "off", "event_type": "set_point",' + b'"push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "off" + + # Test service setting the HVAC mode to "auto" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat mode cancel set point + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b","room_id": "2746182631",' + b'"home": {"id": "91763b24c43d3e344f424e8b","name": "MYHOME","country": "DE",' + b'"rooms": [{"id": "2746182631","name": "Livingroom","type": "livingroom",' + b'"therm_setpoint_mode": "home"}], "modules": [{"id": "12:34:56:00:01:ae",' + b'"name": "Livingroom", "type": "NATherm1"}]}, "mode": "home",' + b'"event_type": "cancel_set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + + +async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): + """Test service with frost guard preset for thermostats.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.netatmo_livingroom" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + + # Test service setting the preset mode to "frost guard" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_PRESET_MODE: PRESET_FROST_GUARD, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat mode change to "Frost Guard" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b","user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"event_type": "therm_mode", "home": {"id": "91763b24c43d3e344f424e8b",' + b'"therm_mode": "hg"}, "mode": "hg", "previous_mode": "schedule",' + b'"push_type":"home_event_changed"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Frost Guard" + ) + + # Test service setting the preset mode to "frost guard" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: climate_entity_livingroom, + ATTR_PRESET_MODE: PRESET_SCHEDULE, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # Test webhook thermostat mode change to "Schedule" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b","user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"event_type": "therm_mode", "home": {"id": "91763b24c43d3e344f424e8b",' + b'"therm_mode": "schedule"}, "mode": "schedule", "previous_mode": "hg",' + b'"push_type": "home_event_changed"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + + +async def test_service_preset_modes_thermostat(hass, climate_entry): + """Test service with preset modes for thermostats.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_livingroom = "climate.netatmo_livingroom" + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] + == "Schedule" + ) + + # Test service setting the preset mode to "away" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake webhook thermostat mode change to "Away" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b","user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", ' + b'"event_type": "therm_mode","home": {"id": "91763b24c43d3e344f424e8b",' + b'"therm_mode": "away"},"mode": "away","previous_mode": "schedule",' + b'"push_type": "home_event_changed"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "auto" + assert ( + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" + ) + + # Test service setting the preset mode to "boost" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_PRESET_MODE: PRESET_BOOST}, + blocking=True, + ) + await hass.async_block_till_done() + + # TFakeest webhook thermostat mode change to "Max" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email":"john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2746182631",' + b'"home": {"id": "91763b24c43d3e344f424e8b", "name": "MYHOME", "country": "DE",' + b'"rooms": [{"id": "2746182631", "name": "Livingroom", "type": "livingroom",' + b'"therm_setpoint_mode": "max", "therm_setpoint_end_time": 1612749189}],' + b'"modules": [{"id": "12:34:56:00:01:ae", "name": "Livingroom", "type": "NATherm1"}]},' + b'"mode": "max", "event_type": "set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_livingroom).state == "heat" + assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 + + +async def test_webhook_event_handling_no_data(hass, climate_entry): + """Test service and webhook event handling with erroneous data.""" + # Test webhook without home entry + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"push_type": "home_event_changed"}' + ) + await simulate_webhook(hass, webhook_id, response) + + # Test webhook with different home id + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "3d3e344f491763b24c424e8b",' + b'"room_id": "2746182631", "home": {"id": "3d3e344f491763b24c424e8b",' + b'"name": "MYHOME","country": "DE", "rooms": [], "modules": []}, "mode": "home",' + b'"event_type": "cancel_set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + # Test webhook without room entries + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"room_id": "2746182631", "home": {"id": "91763b24c43d3e344f424e8b",' + b'"name": "MYHOME", "country": "DE", "rooms": [], "modules": []}, "mode": "home",' + b'"event_type": "cancel_set_point","push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + +async def test_service_schedule_thermostats(hass, climate_entry, caplog): + """Test service for selecting Netatmo schedule with thermostats.""" + climate_entity_livingroom = "climate.netatmo_livingroom" + + # Test setting a valid schedule + await hass.services.async_call( + "netatmo", + SERVICE_SET_SCHEDULE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_SCHEDULE_NAME: "Winter"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert ( + "Setting 91763b24c43d3e344f424e8b schedule to Winter (b1b54a2f45795764f59d50d8)" + in caplog.text + ) + + # Test setting an invalid schedule + await hass.services.async_call( + "netatmo", + SERVICE_SET_SCHEDULE, + {ATTR_ENTITY_ID: climate_entity_livingroom, ATTR_SCHEDULE_NAME: "summer"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert "summer is not a invalid schedule" in caplog.text + + +async def test_service_preset_mode_already_boost_valves(hass, climate_entry): + """Test service with boost preset for valves when already in boost mode.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_entrada = "climate.netatmo_entrada" + + assert hass.states.get(climate_entity_entrada).state == "auto" + assert ( + hass.states.get(climate_entity_entrada).attributes["preset_mode"] + == "Frost Guard" + ) + assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 7 + + # Test webhook valve mode change to "Max" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"room_id": "2833524037", "home": {"id": "91763b24c43d3e344f424e8b", "name": "MYHOME",' + b'"country": "DE","rooms": [{"id": "2833524037", "name": "Entrada", "type": "lobby",' + b'"therm_setpoint_mode": "max", "therm_setpoint_end_time": 1612749189}],' + b'"modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}]},' + b'"mode": "max","event_type": "set_point","push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + # Test service setting the preset mode to "boost" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: climate_entity_entrada, ATTR_PRESET_MODE: PRESET_BOOST}, + blocking=True, + ) + await hass.async_block_till_done() + + # Test webhook valve mode change to "Max" + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"room_id": "2833524037", "home": {"id": "91763b24c43d3e344f424e8b",' + b'"name": "MYHOME","country": "DE","rooms": [{"id": "2833524037", "name": "Entrada",' + b'"type": "lobby", "therm_setpoint_mode": "max", "therm_setpoint_end_time": 1612749189}],' + b'"modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}]},' + b'"mode": "max", "event_type": "set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_entrada).state == "heat" + assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 + + +async def test_service_preset_mode_boost_valves(hass, climate_entry): + """Test service with boost preset for valves.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_entrada = "climate.netatmo_entrada" + + # Test service setting the preset mode to "boost" + assert hass.states.get(climate_entity_entrada).state == "auto" + assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 7 + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: climate_entity_entrada, ATTR_PRESET_MODE: PRESET_BOOST}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake backend response + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b",' + b'"room_id": "2833524037", "home": {"id": "91763b24c43d3e344f424e8b", "name": "MYHOME",' + b'"country": "DE", "rooms": [{"id": "2833524037","name": "Entrada","type": "lobby",' + b'"therm_setpoint_mode": "max","therm_setpoint_end_time": 1612749189}],' + b'"modules": [{"id": "12:34:56:00:01:ae", "name": "Entrada", "type": "NRV"}]},' + b'"mode": "max", "event_type": "set_point", "push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_entrada).state == "heat" + assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 + + +async def test_service_preset_mode_invalid(hass, climate_entry, caplog): + """Test service with invalid preset.""" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: "climate.netatmo_cocina", ATTR_PRESET_MODE: "invalid"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert "Preset mode 'invalid' not available" in caplog.text + + +async def test_valves_service_turn_off(hass, climate_entry): + """Test service turn off for valves.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_entrada = "climate.netatmo_entrada" + + # Test turning valve off + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: climate_entity_entrada}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake backend response for valve being turned off + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2833524037",' + b'"home": {"id": "91763b24c43d3e344f424e8b","name": "MYHOME","country": "DE",' + b'"rooms": [{"id": "2833524037","name": "Entrada","type": "lobby",' + b'"therm_setpoint_mode": "off"}], "modules": [{"id": "12:34:56:00:01:ae","name": "Entrada",' + b'"type": "NRV"}]}, "mode": "off", "event_type": "set_point", "push_type":"display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_entrada).state == "off" + + +async def test_valves_service_turn_on(hass, climate_entry): + """Test service turn on for valves.""" + webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + climate_entity_entrada = "climate.netatmo_entrada" + + # Test turning valve on + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: climate_entity_entrada}, + blocking=True, + ) + await hass.async_block_till_done() + + # Fake backend response for valve being turned on + response = ( + b'{"user_id": "91763b24c43d3e344f424e8b", "user": {"id": "91763b24c43d3e344f424e8b",' + b'"email": "john@doe.com"}, "home_id": "91763b24c43d3e344f424e8b", "room_id": "2833524037",' + b'"home": {"id": "91763b24c43d3e344f424e8b","name": "MYHOME","country": "DE",' + b'"rooms": [{"id": "2833524037","name": "Entrada","type": "lobby",' + b'"therm_setpoint_mode": "home"}], "modules": [{"id": "12:34:56:00:01:ae",' + b'"name": "Entrada", "type": "NRV"}]}, "mode": "home", "event_type": "cancel_set_point",' + b'"push_type": "display_change"}' + ) + await simulate_webhook(hass, webhook_id, response) + + assert hass.states.get(climate_entity_entrada).state == "auto" + + +@pytest.mark.parametrize( + "batterylevel, module_type, expected", + [ + (4101, NA_THERM, 100), + (3601, NA_THERM, 80), + (3450, NA_THERM, 65), + (3301, NA_THERM, 50), + (3001, NA_THERM, 20), + (2799, NA_THERM, 0), + (3201, NA_VALVE, 100), + (2701, NA_VALVE, 80), + (2550, NA_VALVE, 65), + (2401, NA_VALVE, 50), + (2201, NA_VALVE, 20), + (2001, NA_VALVE, 0), + ], +) +async def test_interpolate(batterylevel, module_type, expected): + """Test interpolation of battery levels depending on device type.""" + assert climate.interpolate(batterylevel, module_type) == expected + + +async def test_get_all_home_ids(): + """Test extracting all home ids returned by NetAtmo API.""" + # Test with backend returning no data + assert climate.get_all_home_ids(None) == [] + + # Test with fake data + home_data = Mock() + home_data.homes = { + "123": {"id": "123", "name": "Home 1", "modules": [], "therm_schedules": []}, + "987": {"id": "987", "name": "Home 2", "modules": [], "therm_schedules": []}, + } + expected = ["123", "987"] + assert climate.get_all_home_ids(home_data) == expected diff --git a/tests/fixtures/netatmo/gethomedata.json b/tests/fixtures/netatmo/gethomedata.json new file mode 100644 index 000000000000..db7d6aa438d8 --- /dev/null +++ b/tests/fixtures/netatmo/gethomedata.json @@ -0,0 +1,318 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + "pseudo": "John Doe" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1560600726, + "out_of_sight": true, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 3, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "pseudo": "Jane Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1560626666, + "out_of_sight": false, + "face": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + }, + "pseudo": "Richard Doe" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff4", + "last_seen": 1560621666, + "out_of_sight": true, + "face": { + "id": "d0ef44fad765b980720710a9", + "version": 1, + "key": "ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d0ef44fad765b980720710a9ab029da89f84a95c2d1730fb67fc40cb2d74b80869ecdf2bb8b72039d2c69928" + } + } + ], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Hall", + "modules": [ + { + "id": "12:34:56:00:f2:f1", + "type": "NIS", + "battery_percent": 84, + "rf": 68, + "status": "no_news", + "monitoring": "on", + "alim_source": "battery", + "tamper_detection_enabled": true, + "name": "Welcome's Siren" + } + ], + "use_pin_code": false, + "last_setup": 1544828430 + }, + { + "id": "12:34:56:00:a5:a4", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,", + "is_local": false, + "sd_status": "on", + "alim_status": "on", + "name": "Garden", + "last_setup": 1563737661, + "light_mode_status": "auto" + } + ], + "smokedetectors": [ + { + "id": "12:34:56:00:8b:a2", + "type": "NSD", + "last_setup": 1567261859, + "name": "Hall" + }, + { + "id": "12:34:56:00:8b:ac", + "type": "NSD", + "last_setup": 1567262759, + "name": "Kitchen" + } + ], + "events": [ + { + "id": "a1b2c3d4e5f6abcdef123456", + "type": "person", + "time": 1560604700, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123457", + "type": "person_away", + "time": 1560602400, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "message": "John Doe hat das Haus verlassen", + "sub_message": "John Doe gilt als abwesend, da das mit diesem Profil verbundene Telefon den Bereich des Hauses verlassen hat." + }, + { + "id": "a1b2c3d4e5f6abcdef123458", + "type": "person", + "time": 1560601200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "video_status": "deleted", + "is_arrival": false, + "message": "John Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef123459", + "type": "person", + "time": 1560600100, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "snapshot": { + "id": "d74fad765b9100ef480720a9", + "version": 1, + "key": "a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + "video_id": "12345678-36bc-4b9a-9762-5194e707ed51", + "video_status": "available", + "is_arrival": false, + "message": "Jane Doe gesehen" + }, + { + "id": "a1b2c3d4e5f6abcdef12345a", + "type": "person", + "time": 1560603600, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "person_id": "91827375-7e04-5298-83ae-a0cb8372dff3", + "snapshot": { + "id": "532dde8d17554c022ab071b8", + "version": 1, + "key": "9fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b89fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "video_id": "12345678-1234-46cb-ad8f-23d893874099", + "video_status": "available", + "is_arrival": false, + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345b", + "type": "movement", + "time": 1560506200, + "camera_id": "12:34:56:00:f1:62", + "device_id": "12:34:56:00:f1:62", + "category": "human", + "snapshot": { + "id": "532dde8d17554c022ab071b9", + "version": 1, + "key": "8fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28", + "url": "https://netatmocameraimage.blob.core.windows.net/production/532dde8d17554c022ab071b98fbe490fffacf45b8416241946541b031a004a09b6747feb6c38c3ccbc456b28" + }, + "vignette": { + "id": "5dc021b5dea854bd2321707a", + "version": 1, + "key": "58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944", + "url": "https://netatmocameraimage.blob.core.windows.net/production/5dc021b5dea854bd2321707a58c5a05bd6bd908f6bf368865ef7355231c44215f8eb7ae458c919b2c67b4944" + }, + "video_id": "12345678-1234-46cb-ad8f-23d89387409a", + "video_status": "available", + "message": "Bewegung erkannt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345c", + "type": "sound_test", + "time": 1560506210, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 0, + "message": "Hall: Alarmton erfolgreich getestet" + }, + { + "id": "a1b2c3d4e5f6abcdef12345d", + "type": "wifi_status", + "time": 1560506220, + "camera_id": "12:34:56:00:8b:a2", + "device_id": "12:34:56:00:8b:a2", + "sub_type": 1, + "message": "Hall:WLAN-Verbindung erfolgreich hergestellt" + }, + { + "id": "a1b2c3d4e5f6abcdef12345e", + "type": "outdoor", + "time": 1560643100, + "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:00:a5:a4", + "video_id": "string", + "video_status": "available", + "event_list": [ + { + "type": "string", + "time": 1560643100, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal détecté", + "snapshot": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa" + }, + "vignette": { + "id": "5715e16849c75xxxx00000000xxxxx", + "version": 1, + "key": "7ac578d05030d0e170643a787ee0a29663dxxx00000xxxxx00000", + "url": "https://netatmocameraimage.blob.core.windows.net/production/1aa00000" + } + }, + { + "type": "string", + "time": 1560506222, + "offset": 0, + "id": "c81bcf7b-2cfg-4ac9-8455-487ed00c0000", + "message": "Animal détecté", + "snapshot": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c53b-aze7a.jpg" + }, + "vignette": { + "filename": "vod\/af74631d-8311-42dc-825b-82e3abeaab09\/events\/c5.jpg" + } + } + ] + } + ] + }, + { + "id": "91763b24c43d3e344f424e8c", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [ + { + "id": "12:34:56:00:a5:a5", + "type": "NOC", + "status": "on", + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTz,,", + "is_local": true, + "sd_status": "on", + "alim_status": "on", + "name": "Street", + "last_setup": 1563737561, + "light_mode_status": "auto" + } + ], + "smokedetectors": [] + }, + { + "id": "91763b24c43d3e344f424e8d", + "persons": [], + "place": { + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin" + }, + "cameras": [], + "smokedetectors": [] + } + ], + "user": { + "reg_locale": "de-DE", + "lang": "de-DE", + "country": "DE", + "mail": "john@doe.com" + }, + "global_info": { + "show_tags": true + } + }, + "status": "ok", + "time_exec": 0.03621506690979, + "time_server": 1560626960 +} \ No newline at end of file diff --git a/tests/fixtures/netatmo/getstationsdata.json b/tests/fixtures/netatmo/getstationsdata.json new file mode 100644 index 000000000000..2a18c7bd2807 --- /dev/null +++ b/tests/fixtures/netatmo/getstationsdata.json @@ -0,0 +1,600 @@ +{ + "body": { + "devices": [ + { + "_id": "12:34:56:37:11:ca", + "cipher_id": "enc:16:zjiZF/q8jTScXVdDa/kvhUAIUPGeYszaD1ClEf8byAJkRjxc5oth7cAocrMUIApX", + "date_setup": 1544558432, + "last_setup": 1544558432, + "type": "NAMain", + "last_status_store": 1559413181, + "module_name": "NetatmoIndoor", + "firmware": 137, + "last_upgrade": 1544558433, + "wifi_status": 45, + "reachable": true, + "co2_calibrating": false, + "station_name": "MyStation", + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 664, + "city": "Frankfurt", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [ + 52.516263, + 13.377726 + ] + }, + "dashboard_data": { + "time_utc": 1559413171, + "Temperature": 24.6, + "CO2": 749, + "Humidity": 36, + "Noise": 37, + "Pressure": 1017.3, + "AbsolutePressure": 939.7, + "min_temp": 23.4, + "max_temp": 25.6, + "date_min_temp": 1559371924, + "date_max_temp": 1559411964, + "temp_trend": "stable", + "pressure_trend": "down" + }, + "modules": [ + { + "_id": "12:34:56:36:fc:de", + "type": "NAModule1", + "module_name": "NetatmoOutdoor", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1544558433, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413157, + "Temperature": 28.6, + "Humidity": 24, + "min_temp": 16.9, + "max_temp": 30.3, + "date_min_temp": 1559365579, + "date_max_temp": 1559404698, + "temp_trend": "down" + }, + "firmware": 46, + "last_message": 1559413177, + "last_seen": 1559413157, + "rf_status": 65, + "battery_vp": 5738, + "battery_percent": 87 + }, + { + "_id": "12:34:56:07:bb:3e", + "type": "NAModule4", + "module_name": "Kitchen", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548956696, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413125, + "Temperature": 28, + "CO2": 503, + "Humidity": 26, + "min_temp": 25, + "max_temp": 28, + "date_min_temp": 1559371577, + "date_max_temp": 1559412561, + "temp_trend": "up" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 73, + "battery_vp": 5687, + "battery_percent": 83 + }, + { + "_id": "12:34:56:07:bb:0e", + "type": "NAModule4", + "module_name": "Livingroom", + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "last_setup": 1548957209, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413093, + "Temperature": 26.4, + "CO2": 451, + "Humidity": 31, + "min_temp": 25.1, + "max_temp": 26.4, + "date_min_temp": 1559365290, + "date_max_temp": 1559413093, + "temp_trend": "stable" + }, + "firmware": 44, + "last_message": 1559413177, + "last_seen": 1559413093, + "rf_status": 84, + "battery_vp": 5626, + "battery_percent": 79 + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": [ + "Wind" + ], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + }, + { + "_id": "12:34:56:05:51:20", + "type": "NAModule3", + "module_name": "Yard", + "data_type": [ + "Rain" + ], + "last_setup": 1549194580, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "Rain": 0, + "sum_rain_24": 0, + "sum_rain_1": 0 + }, + "firmware": 8, + "last_message": 1559413177, + "last_seen": 1559413170, + "rf_status": 67, + "battery_vp": 5860, + "battery_percent": 93 + } + ] + }, + { + "_id": "12 :34: 56:36:fd:3c", + "station_name": "Valley Road", + "date_setup": 1545897146, + "last_setup": 1545897146, + "type": "NAMain", + "last_status_store": 1581835369, + "firmware": 137, + "last_upgrade": 1545897125, + "wifi_status": 53, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 69, + "city": "Valley", + "country": "AU", + "timezone": "Australia/Hobart", + "location": [ + 148.444226, + -41.721282 + ] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1581835330, + "Temperature": 22.4, + "CO2": 471, + "Humidity": 46, + "Noise": 47, + "Pressure": 1011.5, + "AbsolutePressure": 1002.8, + "min_temp": 18.1, + "max_temp": 22.5, + "date_max_temp": 1581829891, + "date_min_temp": 1581794878, + "temp_trend": "stable", + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "12 :34: 56:36:e6:c0", + "type": "NAModule1", + "module_name": "Module", + "data_type": [ + "Temperature", + "Humidity" + ], + "last_setup": 1545897146, + "battery_percent": 22, + "reachable": false, + "firmware": 46, + "last_message": 1572497781, + "last_seen": 1572497742, + "rf_status": 88, + "battery_vp": 4118 + }, + { + "_id": "12:34:56:05:25:6e", + "type": "NAModule3", + "module_name": "Rain Gauge", + "data_type": [ + "Rain" + ], + "last_setup": 1553997427, + "battery_percent": 82, + "reachable": true, + "firmware": 8, + "last_message": 1581835362, + "last_seen": 1581835354, + "rf_status": 78, + "battery_vp": 5594, + "dashboard_data": { + "time_utc": 1581835329, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0 + } + } + ] + }, + { + "_id": "12:34:56:32:a7:60", + "home_name": "Ateljen", + "date_setup": 1566714693, + "last_setup": 1566714693, + "type": "NAMain", + "last_status_store": 1588481079, + "module_name": "Indoor", + "firmware": 177, + "last_upgrade": 1566714694, + "wifi_status": 50, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 11.6136629, + 57.7006827 + ] + }, + "dashboard_data": { + "time_utc": 1588481073, + "Temperature": 18.2, + "CO2": 542, + "Humidity": 45, + "Noise": 45, + "Pressure": 1013, + "AbsolutePressure": 1001.9, + "min_temp": 18.2, + "max_temp": 19.5, + "date_max_temp": 1588456861, + "date_min_temp": 1588479561, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "last_setup": 1587635819, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 100, + "reachable": false, + "firmware": 255, + "last_message": 0, + "last_seen": 0, + "rf_status": 255, + "battery_vp": 65535 + } + ] + }, + { + "_id": "12:34:56:1c:68:2e", + "station_name": "Bol\u00e5s", + "date_setup": 1470935400, + "last_setup": 1470935400, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Inne - Nere", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 11.6136629, + 57.7006827 + ] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:02:b3:da", + "type": "NAModule3", + "module_name": "Regnm\u00e4tare", + "last_setup": 1470937706, + "data_type": [ + "Rain" + ], + "battery_percent": 81, + "reachable": true, + "firmware": 12, + "last_message": 1588481393, + "last_seen": 1588481386, + "rf_status": 67, + "battery_vp": 5582, + "dashboard_data": { + "time_utc": 1588481386, + "Rain": 0, + "sum_rain_1": 0, + "sum_rain_24": 0.1 + } + }, + { + "_id": "12:34:56:03:76:60", + "type": "NAModule4", + "module_name": "Inne - Uppe", + "last_setup": 1470938089, + "data_type": [ + "Temperature", + "CO2", + "Humidity" + ], + "battery_percent": 14, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481374, + "rf_status": 70, + "battery_vp": 4448, + "dashboard_data": { + "time_utc": 1588481374, + "Temperature": 19.6, + "CO2": 696, + "Humidity": 41, + "min_temp": 19.6, + "max_temp": 20.5, + "date_max_temp": 1588456817, + "date_min_temp": 1588481374, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:32:db:06", + "type": "NAModule1", + "module_name": "Ute", + "last_setup": 1566326027, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 81, + "reachable": true, + "firmware": 50, + "last_message": 1588481393, + "last_seen": 1588481380, + "rf_status": 61, + "battery_vp": 5544, + "dashboard_data": { + "time_utc": 1588481380, + "Temperature": 6.4, + "Humidity": 91, + "min_temp": 3.6, + "max_temp": 6.4, + "date_max_temp": 1588481380, + "date_min_temp": 1588471383, + "temp_trend": "up" + } + } + ] + }, + { + "_id": "12:34:56:1d:68:2e", + "date_setup": 1470935500, + "last_setup": 1470935500, + "type": "NAMain", + "last_status_store": 1588481399, + "module_name": "Basisstation", + "firmware": 177, + "last_upgrade": 1470935401, + "wifi_status": 13, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 93, + "city": "Gothenburg", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 11.6136629, + 57.7006827 + ] + }, + "dashboard_data": { + "time_utc": 1588481387, + "Temperature": 20.8, + "CO2": 674, + "Humidity": 41, + "Noise": 34, + "Pressure": 1012.1, + "AbsolutePressure": 1001, + "min_temp": 20.8, + "max_temp": 22.2, + "date_max_temp": 1588456859, + "date_min_temp": 1588480176, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [] + }, + { + "_id": "12:34:56:58:c8:54", + "date_setup": 1605594014, + "last_setup": 1605594014, + "type": "NAMain", + "last_status_store": 1605878352, + "firmware": 178, + "wifi_status": 47, + "reachable": true, + "co2_calibrating": false, + "data_type": [ + "Temperature", + "CO2", + "Humidity", + "Noise", + "Pressure" + ], + "place": { + "altitude": 65, + "city": "Njurunda District", + "country": "SE", + "timezone": "Europe/Stockholm", + "location": [ + 17.123456, + 62.123456 + ] + }, + "station_name": "Njurunda (Indoor)", + "home_id": "5fb36b9ec68fd10c6467ca65", + "home_name": "Njurunda", + "dashboard_data": { + "time_utc": 1605878349, + "Temperature": 19.7, + "CO2": 993, + "Humidity": 40, + "Noise": 40, + "Pressure": 1015.6, + "AbsolutePressure": 1007.8, + "min_temp": 19.7, + "max_temp": 20.4, + "date_max_temp": 1605826917, + "date_min_temp": 1605873207, + "temp_trend": "stable", + "pressure_trend": "up" + }, + "modules": [ + { + "_id": "12:34:56:58:e6:38", + "type": "NAModule1", + "last_setup": 1605594034, + "data_type": [ + "Temperature", + "Humidity" + ], + "battery_percent": 100, + "reachable": true, + "firmware": 50, + "last_message": 1605878347, + "last_seen": 1605878328, + "rf_status": 62, + "battery_vp": 6198, + "dashboard_data": { + "time_utc": 1605878328, + "Temperature": 0.6, + "Humidity": 77, + "min_temp": -2.1, + "max_temp": 1.5, + "date_max_temp": 1605865920, + "date_min_temp": 1605826904, + "temp_trend": "down" + } + } + ] + } + ], + "user": { + "mail": "john@doe.com", + "administrative": { + "lang": "de-DE", + "reg_locale": "de-DE", + "country": "DE", + "unit": 0, + "windunit": 0, + "pressureunit": 0, + "feel_like_algo": 0 + } + } + }, + "status": "ok", + "time_exec": 0.91107702255249, + "time_server": 1559413602 +} \ No newline at end of file diff --git a/tests/fixtures/netatmo/homesdata.json b/tests/fixtures/netatmo/homesdata.json new file mode 100644 index 000000000000..aecab91550cc --- /dev/null +++ b/tests/fixtures/netatmo/homesdata.json @@ -0,0 +1,595 @@ +{ + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "module_ids": [ + "12:34:56:00:01:ae" + ] + }, + { + "id": "3688132631", + "name": "Hall", + "type": "custom", + "module_ids": [ + "12:34:56:00:f1:62" + ] + }, + { + "id": "2833524037", + "name": "Entrada", + "type": "lobby", + "module_ids": [ + "12:34:56:03:a5:54" + ] + }, + { + "id": "2940411577", + "name": "Cocina", + "type": "kitchen", + "module_ids": [ + "12:34:56:03:a0:ac" + ] + } + ], + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Thermostat", + "setup_date": 1494963356, + "modules_bridged": [ + "12:34:56:00:01:ae", + "12:34:56:03:a0:ac", + "12:34:56:03:a5:54" + ] + }, + { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a5:54", + "type": "NRV", + "name": "Valve1", + "setup_date": 1554549767, + "room_id": "2833524037", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a0:ac", + "type": "NRV", + "name": "Valve2", + "setup_date": 1554554444, + "room_id": "2940411577", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631" + } + ], + "therm_schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "selected": true, + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + }, + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Winter", + "id": "b1b54a2f45795764f59d50d8", + "type": "therm" + } + ], + "therm_setpoint_default_duration": 120, + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "pseudo": "John Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "pseudo": "Jane Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "pseudo": "Richard Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + } + ], + "schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Komfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 21 + } + ] + }, + { + "type": 1, + "name": "Nacht", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4, + "rooms": [ + { + "id": "2746182631", + "therm_setpoint_temperature": 17 + } + ] + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "id": "591b54a2764ff4d50d8b5795", + "selected": true, + "type": "therm" + } + ], + "therm_mode": "schedule" + }, + { + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [ + 52.516263, + 13.377726 + ], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} \ No newline at end of file diff --git a/tests/fixtures/netatmo/homestatus.json b/tests/fixtures/netatmo/homestatus.json new file mode 100644 index 000000000000..5d508ea03b0f --- /dev/null +++ b/tests/fixtures/netatmo/homestatus.json @@ -0,0 +1,113 @@ +{ + "status": "ok", + "time_server": 1559292039, + "body": { + "home": { + "modules": [ + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "monitoring": "on", + "sd_status": 4, + "alim_status": 2, + "locked": false, + "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", + "is_local": true + }, + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "firmware_revision": 174, + "rf_strength": 107, + "wifi_strength": 42 + }, + { + "id": "12:34:56:00:01:ae", + "reachable": true, + "type": "NATherm1", + "firmware_revision": 65, + "rf_strength": 58, + "battery_level": 3793, + "boiler_valve_comfort_boost": false, + "boiler_status": false, + "anticipating": false, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "high" + }, + { + "id": "12:34:56:03:a5:54", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 51, + "battery_level": 3025, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + }, + { + "id": "12:34:56:03:a0:ac", + "reachable": true, + "type": "NRV", + "firmware_revision": 79, + "rf_strength": 59, + "battery_level": 2329, + "bridge": "12:34:56:00:fa:d0", + "battery_state": "full" + } + ], + "rooms": [ + { + "id": "2746182631", + "reachable": true, + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "schedule", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 + }, + { + "id": "2940411577", + "reachable": true, + "therm_measured_temperature": 5, + "heating_power_request": 1, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + }, + { + "id": "2833524037", + "reachable": true, + "therm_measured_temperature": 24.5, + "heating_power_request": 0, + "therm_setpoint_temperature": 7, + "therm_setpoint_mode": "hg", + "therm_setpoint_start_time": 0, + "therm_setpoint_end_time": 0, + "anticipating": false, + "open_window": false + } + ], + "id": "91763b24c43d3e344f424e8b", + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "last_seen": 1557071156, + "out_of_sight": true + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "last_seen": 1559282761, + "out_of_sight": false + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "last_seen": 1559224132, + "out_of_sight": true + } + ] + } + } +} \ No newline at end of file