diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 892dfcb4..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,68 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# ******** NOTE ******** - -name: "CodeQL" - -on: - push: - branches: [ development, master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ development ] - schedule: - - cron: '21 22 * * 0' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/src/pyatmo/__init__.py b/src/pyatmo/__init__.py index 55a6be8d..2bba15ea 100644 --- a/src/pyatmo/__init__.py +++ b/src/pyatmo/__init__.py @@ -1,44 +1,25 @@ """Expose submodules.""" from pyatmo import const, modules from pyatmo.account import AsyncAccount -from pyatmo.auth import AbstractAsyncAuth, ClientAuth, NetatmoOAuth2 -from pyatmo.camera import AsyncCameraData, CameraData +from pyatmo.auth import AbstractAsyncAuth from pyatmo.exceptions import ApiError, InvalidHome, InvalidRoom, NoDevice, NoSchedule from pyatmo.home import Home -from pyatmo.home_coach import AsyncHomeCoachData, HomeCoachData from pyatmo.modules import Module from pyatmo.modules.device_types import DeviceType -from pyatmo.public_data import AsyncPublicData, PublicData from pyatmo.room import Room -from pyatmo.thermostat import AsyncHomeData, AsyncHomeStatus, HomeData, HomeStatus -from pyatmo.weather_station import AsyncWeatherStationData, WeatherStationData __all__ = [ "AbstractAsyncAuth", "ApiError", "AsyncAccount", - "AsyncCameraData", - "AsyncHomeCoachData", - "AsyncHomeData", - "AsyncHomeStatus", - "AsyncPublicData", - "AsyncWeatherStationData", - "CameraData", - "ClientAuth", - "HomeCoachData", - "HomeData", - "HomeStatus", "InvalidHome", "InvalidRoom", "Home", "Module", "Room", "DeviceType", - "NetatmoOAuth2", "NoDevice", "NoSchedule", - "PublicData", - "WeatherStationData", "const", "modules", ] diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index 8d20840c..970e52ba 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -3,21 +3,13 @@ from abc import ABC, abstractmethod import asyncio -from collections.abc import Callable from json import JSONDecodeError import logging -from time import sleep from typing import Any from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError -from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError -import requests -from requests_oauthlib import OAuth2Session from pyatmo.const import ( - ALL_SCOPES, - AUTH_REQ_ENDPOINT, - AUTH_URL_ENDPOINT, AUTHORIZATION_HEADER, DEFAULT_BASE_URL, ERRORS, @@ -29,274 +21,6 @@ LOG = logging.getLogger(__name__) -class NetatmoOAuth2: - """Handle authentication with OAuth2.""" - - def __init__( - self, - client_id: str, - client_secret: str, - redirect_uri: str | None = None, - token: dict[str, str] | None = None, - token_updater: Callable[[str], None] | None = None, - scope: str | None = "read_station", - user_prefix: str | None = None, - base_url: str = DEFAULT_BASE_URL, - ) -> None: - """Initialize self.""" - - # Keyword Arguments: - # client_id {str} -- Application client ID delivered by Netatmo on dev.netatmo.com (default: {None}) - # client_secret {str} -- Application client secret delivered by Netatmo on dev.netatmo.com (default: {None}) - # redirect_uri {Optional[str]} -- Redirect URI where to the authorization server will redirect with an authorization code (default: {None}) - # token {Optional[Dict[str, str]]} -- Authorization token (default: {None}) - # token_updater {Optional[Callable[[str], None]]} -- Callback when the token is updated (default: {None}) - # scope {Optional[str]} -- List of scopes (default: {"read_station"}) - # read_station: to retrieve weather station data (Getstationsdata, Getmeasure) - # read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - # access_camera: to access the camera, the videos and the live stream - # write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) - # read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) - # write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) - # read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - # access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status - # read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - # read_smokedetector: to retrieve the smoke detector status (Gethomedata) - # Several values can be used at the same time, ie: 'read_station read_camera' - # user_prefix {Optional[str]} -- API prefix for the Netatmo customer - # base_url {str} -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}) - - self.client_id = client_id - self.client_secret = client_secret - self.redirect_uri = redirect_uri - self.token_updater = token_updater - self.user_prefix = user_prefix - self.base_url = base_url - - if token: - self.scope = " ".join(token["scope"]) - - else: - self.scope = scope or " ".join(ALL_SCOPES) - - self.extra = {"client_id": self.client_id, "client_secret": self.client_secret} - - self._oauth = OAuth2Session( - client_id=self.client_id, - token=token, - token_updater=self.token_updater, - redirect_uri=self.redirect_uri, - scope=self.scope, - ) - - def refresh_tokens(self) -> Any: - """Refresh and return new tokens.""" - - token = self._oauth.refresh_token( - self.base_url + AUTH_REQ_ENDPOINT, - **self.extra, - ) - - if self.token_updater is not None: - self.token_updater(token) - - return token - - def post_api_request( - self, - endpoint: str, - params: dict[str, Any] | None = None, - timeout: int = 5, - ) -> requests.Response: - """Wrap post requests.""" - - return self.post_request( - url=self.base_url + endpoint, - params=params, - timeout=timeout, - ) - - def post_request( - self, - url: str, - params: dict[str, Any] | None = None, - timeout: int = 5, - ) -> requests.Response: - """Wrap post requests.""" - - resp = requests.Response() - req_args = {"data": params if params is not None else {}} - - if "json" in req_args["data"]: - req_args["json"] = req_args["data"]["json"] - req_args.pop("data") - - if "https://" not in url: - try: - resp = requests.post(url, data=params, timeout=timeout) - except requests.exceptions.ChunkedEncodingError: - LOG.debug("Encoding error when connecting to '%s'", url) - except requests.exceptions.ConnectTimeout: - LOG.debug("Connection to %s timed out", url) - except requests.exceptions.ConnectionError: - LOG.debug("Remote end closed connection without response (%s)", url) - - else: - - def query( - url: str, - params: dict[str, Any], - timeout: int, - retries: int, - ) -> Any: - if retries == 0: - LOG.error("Too many retries") - return requests.Response() - - try: - return self._oauth.post(url=url, timeout=timeout, **params) - - except ( - TokenExpiredError, - requests.exceptions.ReadTimeout, - requests.exceptions.ConnectionError, - ): - self._oauth.token = self.refresh_tokens() - # Sleep for 1 sec to prevent authentication related - # timeouts after a token refresh. - sleep(1) - return query(url, params, timeout * 2, retries - 1) - - resp = query(url, req_args, timeout, 3) - - if resp.status_code is None: - LOG.debug("Resp is None - %s", resp) - return requests.Response() - - if not resp.ok: - LOG.debug( - "The Netatmo API returned %s (%s)", - resp.content, - resp.status_code, - ) - try: - raise ApiError( - f"{resp.status_code} - " - f"{ERRORS.get(resp.status_code, '')} - " - f"{resp.json()['error']['message']} " - f"({resp.json()['error']['code']}) " - f"when accessing '{url}'", - ) - - except JSONDecodeError as exc: - raise ApiError( - f"{resp.status_code} - " - f"{ERRORS.get(resp.status_code, '')} - " - f"when accessing '{url}'", - ) from exc - - if "application/json" in resp.headers.get( - "content-type", - [], - ) or resp.content not in [b"", b"None"]: - return resp - - return requests.Response() - - def get_authorization_url(self, state: str | None = None) -> Any: - """Return the authorization URL.""" - - return self._oauth.authorization_url(self.base_url + AUTH_URL_ENDPOINT, state) - - def request_token( - self, - authorization_response: str | None = None, - code: str | None = None, - ) -> Any: - """Request token.""" - - return self._oauth.fetch_token( - self.base_url + AUTH_REQ_ENDPOINT, - authorization_response=authorization_response, - code=code, - client_secret=self.client_secret, - include_client_id=True, - user_prefix=self.user_prefix, - ) - - def addwebhook(self, webhook_url: str) -> None: - """Register webhook.""" - - post_params = {"url": webhook_url} - resp = self.post_api_request(WEBHOOK_URL_ADD_ENDPOINT, post_params) - LOG.debug("addwebhook: %s", resp) - - def dropwebhook(self) -> None: - """Unregister webhook.""" - - post_params = {"app_types": "app_security"} - resp = self.post_api_request(WEBHOOK_URL_DROP_ENDPOINT, post_params) - LOG.debug("dropwebhook: %s", resp) - - -class ClientAuth(NetatmoOAuth2): - """Request authentication and keep access token available through token method.""" - - # Renew it automatically if necessary - # Args: - # clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com - # clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com - # username (str) - # password (str) - # scope (Optional[str]): - # read_station: to retrieve weather station data (Getstationsdata, Getmeasure) - # read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - # access_camera: to access the camera, the videos and the live stream - # write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) - # read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) - # write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) - # read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - # access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status - # read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - # read_smokedetector: to retrieve the smoke detector status (Gethomedata) - # Several value can be used at the same time, ie: 'read_station read_camera' - # user_prefix (Optional[str]) -- API prefix for the Netatmo customer - # base_url (str) -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}). - - def __init__( - self, - client_id: str, - client_secret: str, - username: str, - password: str, - scope: str = "read_station", - user_prefix: str | None = None, - base_url: str = DEFAULT_BASE_URL, - ) -> None: - """Initialize self.""" - - super().__init__( - client_id=client_id, - client_secret=client_secret, - scope=scope, - user_prefix=user_prefix, - base_url=base_url, - ) - - self._oauth = OAuth2Session( - client=LegacyApplicationClient(client_id=self.client_id), - ) - self._oauth.fetch_token( - token_url=self.base_url + AUTH_REQ_ENDPOINT, - username=username, - password=password, - client_id=self.client_id, - client_secret=self.client_secret, - scope=self.scope, - user_prefix=self.user_prefix, - ) - - class AbstractAsyncAuth(ABC): """Abstract class to make authenticated requests.""" @@ -338,14 +62,13 @@ async def async_get_image( headers=headers, timeout=timeout, ) as resp: - resp_status = resp.status resp_content = await resp.read() if resp.headers.get("content-type") == "image/jpeg": return resp_content raise ApiError( - f"{resp_status} - " + f"{resp.status} - " f"invalid content-type in response" f"when accessing '{url}'", ) @@ -373,12 +96,28 @@ async def async_post_request( ) -> ClientResponse: """Wrap async post requests.""" + access_token = await self.get_access_token() + headers = {AUTHORIZATION_HEADER: f"Bearer {access_token}"} + + req_args = self.prepare_request_arguments(params) + + async with self.websession.post( + url, + **req_args, + headers=headers, + timeout=timeout, + ) as resp: + return await self.process_response(resp, url) + + async def get_access_token(self): + """Get access token.""" try: - access_token = await self.async_get_access_token() + return await self.async_get_access_token() except ClientError as err: raise ApiError(f"Access token failure: {err}") from err - headers = {AUTHORIZATION_HEADER: f"Bearer {access_token}"} + def prepare_request_arguments(self, params): + """Prepare request arguments.""" req_args = {"data": params if params is not None else {}} if "params" in req_args["data"]: @@ -389,43 +128,49 @@ async def async_post_request( req_args["json"] = req_args["data"]["json"] req_args.pop("data") - async with self.websession.post( - url, - **req_args, - headers=headers, - timeout=timeout, - ) as resp: - resp_status = resp.status - resp_content = await resp.read() + return req_args + + async def process_response(self, resp, url): + """Process response.""" + resp_status = resp.status + resp_content = await resp.read() + + if not resp.ok: + LOG.debug("The Netatmo API returned %s (%s)", resp_content, resp_status) + await self.handle_error_response(resp, resp_status, url) + + return await self.handle_success_response(resp, resp_content) + + async def handle_error_response(self, resp, resp_status, url): + """Handle error response.""" + try: + resp_json = await resp.json() + raise ApiError( + f"{resp_status} - " + f"{ERRORS.get(resp_status, '')} - " + f"{resp_json['error']['message']} " + f"({resp_json['error']['code']}) " + f"when accessing '{url}'", + ) + + except (JSONDecodeError, ContentTypeError) as exc: + raise ApiError( + f"{resp_status} - " + f"{ERRORS.get(resp_status, '')} - " + f"when accessing '{url}'", + ) from exc + + async def handle_success_response(self, resp, resp_content): + """Handle success response.""" + try: + if "application/json" in resp.headers.get("content-type", []): + return resp + + if resp_content not in [b"", b"None"]: + return resp - if not resp.ok: - LOG.debug("The Netatmo API returned %s (%s)", resp_content, resp_status) - try: - resp_json = await resp.json() - raise ApiError( - f"{resp_status} - " - f"{ERRORS.get(resp_status, '')} - " - f"{resp_json['error']['message']} " - f"({resp_json['error']['code']}) " - f"when accessing '{url}'", - ) - - except (JSONDecodeError, ContentTypeError) as exc: - raise ApiError( - f"{resp_status} - " - f"{ERRORS.get(resp_status, '')} - " - f"when accessing '{url}'", - ) from exc - - try: - if "application/json" in resp.headers.get("content-type", []): - return resp - - if resp_content not in [b"", b"None"]: - return resp - - except (TypeError, AttributeError): - LOG.debug("Invalid response %s", resp) + except (TypeError, AttributeError): + LOG.debug("Invalid response %s", resp) return resp diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py deleted file mode 100644 index 7b1f5d5d..00000000 --- a/src/pyatmo/camera.py +++ /dev/null @@ -1,837 +0,0 @@ -"""Support for Netatmo security devices (cameras, smoke detectors, sirens, window sensors, events and persons).""" -from __future__ import annotations - -from abc import ABC -from collections import defaultdict -import imghdr # pylint: disable=deprecated-module -import time -from typing import Any -from warnings import warn - -import aiohttp -from requests.exceptions import ReadTimeout - -from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 -from pyatmo.const import ( - GETCAMERAPICTURE_ENDPOINT, - GETEVENTSUNTIL_ENDPOINT, - GETHOMEDATA_ENDPOINT, - SETPERSONSAWAY_ENDPOINT, - SETPERSONSHOME_ENDPOINT, - SETSTATE_ENDPOINT, -) -from pyatmo.exceptions import ApiError, NoDevice -from pyatmo.helpers import LOG, extract_raw_data - -warn(f"The module {__name__} is deprecated.", DeprecationWarning, stacklevel=2) - - -class AbstractCameraData(ABC): - """Abstract class of Netatmo camera data.""" - - raw_data: dict = defaultdict(dict) - homes: dict = defaultdict(dict) - persons: dict = defaultdict(dict) - events: dict = defaultdict(dict) - outdoor_events: dict = defaultdict(dict) - cameras: dict = defaultdict(dict) - smoke_detectors: dict = defaultdict(dict) - modules: dict = {} - last_event: dict = {} - outdoor_last_event: dict = {} - types: dict = defaultdict(dict) - - def process(self) -> None: - """Process data from API.""" - self.homes = {d["id"]: d for d in self.raw_data} - - for item in self.raw_data: - home_id: str = item.get("id", "") - - if not item.get("name"): - self.homes[home_id]["name"] = "Unknown" - - self._store_events(events=item.get("events", [])) - self._store_cameras(cameras=item.get("cameras", []), home_id=home_id) - self._store_smoke_detectors( - smoke_detectors=item.get("smokedetectors", []), - home_id=home_id, - ) - for person in item.get("persons", []): - self.persons[home_id][person["id"]] = person - - def _store_persons(self, persons: list) -> None: - for person in persons: - self.persons[person["id"]] = person - - def _store_smoke_detectors(self, smoke_detectors: list, home_id: str) -> None: - for smoke_detector in smoke_detectors: - self.smoke_detectors[home_id][smoke_detector["id"]] = smoke_detector - self.types[home_id][smoke_detector["type"]] = smoke_detector - - def _store_cameras(self, cameras: list, home_id: str) -> None: - for camera in cameras: - self.cameras[home_id][camera["id"]] = camera - self.types[home_id][camera["type"]] = camera - - if camera.get("name") is None: - self.cameras[home_id][camera["id"]]["name"] = camera["type"] - - self.cameras[home_id][camera["id"]]["home_id"] = home_id - if camera["type"] == "NACamera": - for module in camera.get("modules", []): - self.modules[module["id"]] = module - self.modules[module["id"]]["cam_id"] = camera["id"] - - def _store_events(self, events: list) -> None: - """Store all events.""" - for event in events: - if event["type"] == "outdoor": - self.outdoor_events[event["camera_id"]][event["time"]] = event - - else: - self.events[event["camera_id"]][event["time"]] = event - - def _store_last_event(self) -> None: - """Store last event for fast access.""" - for camera in self.events: - self.last_event[camera] = self.events[camera][ - sorted(self.events[camera])[-1] - ] - - for camera in self.outdoor_events: - self.outdoor_last_event[camera] = self.outdoor_events[camera][ - sorted(self.outdoor_events[camera])[-1] - ] - - def get_camera(self, camera_id: str) -> dict[str, str]: - """Get camera data.""" - return next( - ( - self.cameras[home_id][camera_id] - for home_id in self.cameras - if camera_id in self.cameras[home_id] - ), - {}, - ) - - def get_camera_home_id(self, camera_id: str) -> str | None: - """Get camera data.""" - return next( - (home_id for home_id in self.cameras if camera_id in self.cameras[home_id]), - None, - ) - - def get_module(self, module_id: str) -> dict | None: - """Get module data.""" - return None if module_id not in self.modules else self.modules[module_id] - - def get_smokedetector(self, smoke_id: str) -> dict | None: - """Get smoke detector.""" - return next( - ( - self.smoke_detectors[home_id][smoke_id] - for home_id in self.smoke_detectors - if smoke_id in self.smoke_detectors[home_id] - ), - None, - ) - - def camera_urls(self, camera_id: str) -> tuple[str | None, str | None]: - """Return the vpn_url and the local_url (if available) of a given camera.""" - - camera_data = self.get_camera(camera_id) - return camera_data.get("vpn_url", None), camera_data.get("local_url", None) - - def get_light_state(self, camera_id: str) -> str | None: - """Return the current mode of the floodlight of a presence camera.""" - camera_data = self.get_camera(camera_id) - if camera_data is None: - raise ValueError("Invalid Camera ID") - - return camera_data.get("light_mode_status") - - def persons_at_home(self, home_id: str | None = None) -> list: - """Return a list of known persons who are currently at home.""" - home_data = self.homes.get(home_id, {}) - return [ - person["pseudo"] - for person in home_data.get("persons", []) - if "pseudo" in person and not person["out_of_sight"] - ] - - def get_person_id(self, name: str, home_id: str) -> str | None: - """Retrieve the ID of a person.""" - return next( - ( - pid - for pid, data in self.persons[home_id].items() - if name == data.get("pseudo") - ), - None, - ) - - def build_event_id(self, event_id: str | None, device_type: str | None): - """Build event id.""" - - def get_event_id(data: dict): - events = {e["time"]: e for e in data.values()} - return min(events.items())[1].get("id") - - if not event_id: - # If no event is provided we need to retrieve the oldest of - # the last event seen by each camera - if device_type == "NACamera": - # for the Welcome camera - event_id = get_event_id(self.last_event) - - elif device_type in {"NOC", "NSD"}: - # for the Presence camera and for the smoke detector - event_id = get_event_id(self.outdoor_last_event) - - return event_id - - def person_seen_by_camera( - self, - name: str, - camera_id: str, - exclude: int = 0, - ) -> bool: - """Evaluate if a specific person has been seen.""" - home_id = self.get_camera_home_id(camera_id) - - if home_id is None: - raise NoDevice - - def _person_in_event(home_id: str, curr_event: dict, person_name: str) -> bool: - person_id = curr_event.get("person_id") - return ( - curr_event["type"] == "person" - and self.persons[home_id][person_id].get("pseudo") == person_name - ) - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events[camera_id], reverse=True) - - for time_ev in array_time_event: - if time_ev < limit: - return False - - current_event = self.events[camera_id][time_ev] - if _person_in_event(home_id, current_event, name): - return True - - return False - - current_event = self.last_event[camera_id] - return _person_in_event(home_id, current_event, name) - - def _known_persons(self, home_id: str) -> dict[str, dict]: - """Return all known persons.""" - return {pid: p for pid, p in self.persons[home_id].items() if "pseudo" in p} - - def known_persons(self, home_id: str) -> dict[str, str]: - """Return a dictionary of known person names.""" - return {pid: p["pseudo"] for pid, p in self._known_persons(home_id).items()} - - def known_persons_names(self, home_id: str) -> list[str]: - """Return a list of known person names.""" - return [person["pseudo"] for person in self._known_persons(home_id).values()] - - def someone_known_seen(self, camera_id: str, exclude: int = 0) -> bool: - """Evaluate if someone known has been seen.""" - if camera_id not in self.events: - raise NoDevice - - if (home_id := self.get_camera_home_id(camera_id)) is None: - raise NoDevice - - def _someone_known_seen(event: dict, home_id: str) -> bool: - return event["type"] == "person" and event[ - "person_id" - ] in self._known_persons(home_id) - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events[camera_id], reverse=True) - seen = False - - for time_ev in array_time_event: - if time_ev < limit: - continue - if seen := _someone_known_seen( - self.events[camera_id][time_ev], - home_id, - ): - break - - return seen - - return _someone_known_seen(self.last_event[camera_id], home_id) - - def someone_unknown_seen(self, camera_id: str, exclude: int = 0) -> bool: - """Evaluate if someone known has been seen.""" - if camera_id not in self.events: - raise NoDevice - - if (home_id := self.get_camera_home_id(camera_id)) is None: - raise NoDevice - - def _someone_unknown_seen(event: dict, home_id: str) -> bool: - return event["type"] == "person" and event[ - "person_id" - ] not in self._known_persons(home_id) - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events[camera_id], reverse=True) - seen = False - - for time_ev in array_time_event: - if time_ev < limit: - continue - - if seen := _someone_unknown_seen( - self.events[camera_id][time_ev], - home_id, - ): - break - - return seen - - return _someone_unknown_seen(self.last_event[camera_id], home_id) - - def motion_detected(self, camera_id: str, exclude: int = 0) -> bool: - """Evaluate if movement has been detected.""" - if camera_id not in self.events: - raise NoDevice - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events[camera_id], reverse=True) - - for time_ev in array_time_event: - if time_ev < limit: - return False - - if self.events[camera_id][time_ev]["type"] == "movement": - return True - - elif self.last_event[camera_id]["type"] == "movement": - return True - - return False - - def outdoor_motion_detected(self, camera_id: str, offset: int = 0) -> bool: - """Evaluate if outdoor movement has been detected.""" - if camera_id not in self.last_event: - return False - - last_event = self.last_event[camera_id] - return ( - last_event["type"] == "movement" - and last_event["video_status"] == "recording" - and last_event["time"] + offset > int(time.time()) - ) - - def _object_detected(self, object_name: str, camera_id: str, offset: int) -> bool: - """Evaluate if an object has been detected.""" - if self.outdoor_last_event[camera_id]["video_status"] == "recording": - for event in self.outdoor_last_event[camera_id]["event_list"]: - if event["type"] == object_name and ( - event["time"] + offset > int(time.time()) - ): - return True - - return False - - def human_detected(self, camera_id: str, offset: int = 0) -> bool: - """Evaluate if a human has been detected.""" - return self._object_detected("human", camera_id, offset) - - def animal_detected(self, camera_id: str, offset: int = 0) -> bool: - """Evaluate if an animal has been detected.""" - return self._object_detected("animal", camera_id, offset) - - def car_detected(self, camera_id: str, offset: int = 0) -> bool: - """Evaluate if a car has been detected.""" - return self._object_detected("vehicle", camera_id, offset) - - def module_motion_detected( - self, - module_id: str, - camera_id: str, - exclude: int = 0, - ) -> bool: - """Evaluate if movement has been detected.""" - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events.get(camera_id, []), reverse=True) - - for time_ev in array_time_event: - if time_ev < limit: - return False - - curr_event = self.events[camera_id][time_ev] - if ( - curr_event["type"] in {"tag_big_move", "tag_small_move"} - and curr_event["module_id"] == module_id - ): - return True - - else: - if camera_id not in self.last_event: - return False - - curr_event = self.last_event[camera_id] - if ( - curr_event["type"] in {"tag_big_move", "tag_small_move"} - and curr_event["module_id"] == module_id - ): - return True - - return False - - def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> bool: - """Evaluate if module status is open.""" - - if exclude: - limit = time.time() - exclude - array_time_event = sorted(self.events.get(camera_id, []), reverse=True) - - for time_ev in array_time_event: - if time_ev < limit: - return False - - curr_event = self.events[camera_id][time_ev] - if ( - curr_event["type"] == "tag_open" - and curr_event["module_id"] == module_id - ): - return True - - else: - if camera_id not in self.last_event: - return False - - curr_event = self.last_event[camera_id] - if ( - curr_event["type"] == "tag_open" - and curr_event["module_id"] == module_id - ): - return True - - return False - - def build_state_params( - self, - camera_id: str, - home_id: str | None, - floodlight: str | None, - monitoring: str | None, - ): - """Build camera state parameters.""" - - if home_id is None: - home_id = self.get_camera(camera_id)["home_id"] - - module = {"id": camera_id} - - if floodlight: - param, val = "floodlight", floodlight.lower() - if val not in {"on", "off", "auto"}: - LOG.error("Invalid value for floodlight") - else: - module[param] = val - - if monitoring: - param, val = "monitoring", monitoring.lower() - if val not in {"on", "off"}: - LOG.error("Invalid value for monitoring") - else: - module[param] = val - - return {"id": home_id, "modules": [module]} - - -class CameraData(AbstractCameraData): - """Class of Netatmo camera data.""" - - def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize the Netatmo camera data.""" - - self.auth = auth - - def update(self, events: int = 30) -> None: - """Fetch and process data from API.""" - resp = self.auth.post_api_request( - endpoint=GETHOMEDATA_ENDPOINT, - params={"size": events}, - ) - - self.raw_data = extract_raw_data(resp.json(), "homes") - self.process() - self._update_all_camera_urls() - self._store_last_event() - - def _update_all_camera_urls(self) -> None: - """Update all camera urls.""" - - for home_id in self.homes: - for camera_id in self.cameras[home_id]: - self.update_camera_urls(camera_id) - - def update_camera_urls(self, camera_id: str) -> None: - """Update and validate the camera urls.""" - - camera_data = self.get_camera(camera_id) - home_id = camera_data["home_id"] - - if not camera_data or camera_data.get("status") == "disconnected": - self.cameras[home_id][camera_id]["local_url"] = None - self.cameras[home_id][camera_id]["vpn_url"] = None - return - - if (vpn_url := camera_data.get("vpn_url")) and camera_data.get("is_local"): - if temp_local_url := self._check_url(vpn_url): - if local_url := self._check_url(temp_local_url): - self.cameras[home_id][camera_id]["local_url"] = local_url - else: - LOG.warning( - "Invalid IP for camera %s (%s)", - self.cameras[home_id][camera_id]["name"], - temp_local_url, - ) - self.cameras[home_id][camera_id]["is_local"] = False - - def _check_url(self, url: str) -> str | None: - """Check if the url is valid.""" - - if url.startswith("http://169.254"): - return None - resp_json = {} - try: - resp = self.auth.post_request(url=f"{url}/command/ping") - if resp.status_code: - resp_json = resp.json() - else: - raise ReadTimeout - except ReadTimeout: - LOG.debug("Timeout validation of camera url %s", url) - return None - except ApiError: - LOG.debug("Api error for camera url %s", url) - return None - - return resp_json.get("local_url") if resp_json else None - - def set_state( - self, - camera_id: str, - home_id: str | None = None, - floodlight: str | None = None, - monitoring: str | None = None, - ) -> bool: - """Turn camera (light) on/off.""" - - # Arguments: - # camera_id {str} -- ID of a camera - # home_id {str} -- ID of a home - # floodlight {str} -- Mode for floodlight (on/off/auto) - # monitoring {str} -- Mode for monitoring (on/off) - - # Returns: - # Boolean -- Success of the request - - post_params = { - "json": { - "home": self.build_state_params( - camera_id, - home_id, - floodlight, - monitoring, - ), - }, - } - - try: - resp = self.auth.post_api_request( - endpoint=SETSTATE_ENDPOINT, - params=post_params, - ).json() - except ApiError as err_msg: - LOG.error("%s", err_msg) - return False - - if "error" in resp: - LOG.debug("%s", resp) - return False - - LOG.debug("%s", resp) - return True - - def set_persons_home(self, home_id: str, person_ids: list[str] | None = None): - """Mark persons as home.""" - post_params: dict[str, str | list] = {"home_id": home_id} - if person_ids: - post_params["person_ids[]"] = person_ids - return self.auth.post_api_request( - endpoint=SETPERSONSHOME_ENDPOINT, - params=post_params, - ).json() - - def set_persons_away(self, home_id: str, person_id: str | None = None): - """Mark a person as away or set the whole home to being empty.""" - post_params = {"home_id": home_id, "person_id": person_id} - return self.auth.post_api_request( - endpoint=SETPERSONSAWAY_ENDPOINT, - params=post_params, - ).json() - - def get_camera_picture( - self, - image_id: str, - key: str, - ) -> tuple[bytes, str | None]: - """Download a specific image (of an event or user face) from the camera.""" - post_params = {"image_id": image_id, "key": key} - resp = self.auth.post_api_request( - endpoint=GETCAMERAPICTURE_ENDPOINT, - params=post_params, - ).content - image_type = imghdr.what("NONE.FILE", resp) - return resp, image_type - - def get_profile_image( - self, - name: str, - home_id: str, - ) -> tuple[bytes | None, str | None]: - """Retrieve the face of a given person.""" - for person in self.persons[home_id].values(): - if name == person.get("pseudo"): - image_id = person["face"]["id"] - key = person["face"]["key"] - return self.get_camera_picture(image_id, key) - - return None, None - - def update_events( - self, - home_id: str, - event_id: str | None = None, - device_type: str | None = None, - ) -> None: - """Update the list of events.""" - if not (event_id or device_type): - raise ApiError - - post_params = { - "home_id": home_id, - "event_id": self.build_event_id(event_id, device_type), - } - - event_list: list = [] - resp: dict[str, Any] | None = None - try: - resp = self.auth.post_api_request( - endpoint=GETEVENTSUNTIL_ENDPOINT, - params=post_params, - ).json() - if resp is not None: - event_list = resp["body"]["events_list"] - except ApiError: - pass - except KeyError: - if resp is not None: - LOG.debug("event_list response: %s", resp) - LOG.debug("event_list body: %s", dict(resp)["body"]) - else: - LOG.debug("No resp received") - - self._store_events(event_list) - self._store_last_event() - - -class AsyncCameraData(AbstractCameraData): - """Class of Netatmo camera data.""" - - def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize the Netatmo camera data.""" - - self.auth = auth - - async def async_update(self, events: int = 30) -> None: - """Fetch and process data from API.""" - - resp = await self.auth.async_post_api_request( - endpoint=GETHOMEDATA_ENDPOINT, - params={"size": events}, - ) - - assert not isinstance(resp, bytes) - self.raw_data = extract_raw_data(await resp.json(), "homes") - self.process() - - try: - await self._async_update_all_camera_urls() - except (aiohttp.ContentTypeError, aiohttp.ClientConnectorError) as err: - LOG.debug("One or more camera could not be reached. (%s)", err) - - self._store_last_event() - - async def _async_update_all_camera_urls(self) -> None: - """Update all camera urls.""" - - for home_id in self.homes: - for camera_id in self.cameras[home_id]: - await self.async_update_camera_urls(camera_id) - - async def async_set_state( - self, - camera_id: str, - home_id: str | None = None, - floodlight: str | None = None, - monitoring: str | None = None, - ) -> bool: - """Turn camera (light) on/off.""" - - # Arguments: - # camera_id {str} -- ID of a camera - # home_id {str} -- ID of a home - # floodlight {str} -- Mode for floodlight (on/off/auto) - # monitoring {str} -- Mode for monitoring (on/off) - - post_params = { - "json": { - "home": self.build_state_params( - camera_id, - home_id, - floodlight, - monitoring, - ), - }, - } - - try: - resp = await self.auth.async_post_api_request( - endpoint=SETSTATE_ENDPOINT, - params=post_params, - ) - except ApiError as err_msg: - LOG.error("%s", err_msg) - return False - - assert not isinstance(resp, bytes) - resp_data = await resp.json() - - if "error" in resp_data: - LOG.debug("%s", resp_data) - return False - - LOG.debug("%s", resp_data) - return True - - async def async_update_camera_urls(self, camera_id: str) -> None: - """Update and validate the camera urls.""" - camera_data = self.get_camera(camera_id) - home_id = camera_data["home_id"] - - if not camera_data or camera_data.get("status") == "disconnected": - self.cameras[home_id][camera_id]["local_url"] = None - self.cameras[home_id][camera_id]["vpn_url"] = None - return - - if (vpn_url := camera_data.get("vpn_url")) and camera_data.get("is_local"): - temp_local_url = await self._async_check_url(vpn_url) - if temp_local_url: - self.cameras[home_id][camera_id][ - "local_url" - ] = await self._async_check_url( - temp_local_url, - ) - - async def _async_check_url(self, url: str) -> str | None: - """Validate camera url.""" - try: - resp = await self.auth.async_post_request(url=f"{url}/command/ping") - except ReadTimeout: - LOG.debug("Timeout validation of camera url %s", url) - return None - except ApiError: - LOG.debug("Api error for camera url %s", url) - return None - - assert not isinstance(resp, bytes) - resp_data = await resp.json() - return resp_data.get("local_url") if resp_data else None - - async def async_set_persons_home( - self, - home_id: str, - person_ids: list[str] | None = None, - ): - """Mark persons as home.""" - post_params: dict[str, str | list] = {"home_id": home_id} - if person_ids: - post_params["person_ids[]"] = person_ids - return await self.auth.async_post_api_request( - endpoint=SETPERSONSHOME_ENDPOINT, - params=post_params, - ) - - async def async_set_persons_away(self, home_id: str, person_id: str | None = None): - """Mark a person as away or set the whole home to being empty.""" - post_params = {"home_id": home_id} - if person_id: - post_params["person_id"] = person_id - return await self.auth.async_post_api_request( - endpoint=SETPERSONSAWAY_ENDPOINT, - params=post_params, - ) - - async def async_get_live_snapshot(self, camera_id: str) -> bytes | None: - """Retrieve live snapshot from camera.""" - local, vpn = self.camera_urls(camera_id) - if not local and not vpn: - return None - resp = await self.auth.async_get_image( - endpoint=f"{(local or vpn)}/live/snapshot_720.jpg", - timeout=10, - ) - - return resp if isinstance(resp, bytes) else None - - async def async_get_camera_picture( - self, - image_id: str, - key: str, - ) -> tuple[bytes, str | None]: - """Download a specific image (of an event or user face) from the camera.""" - post_params = {"image_id": image_id, "key": key} - resp = await self.auth.async_get_image( - endpoint=GETCAMERAPICTURE_ENDPOINT, - params=post_params, - ) - - return ( - (resp, imghdr.what("NONE.FILE", resp)) - if isinstance(resp, bytes) - else (b"", None) - ) - - async def async_get_profile_image( - self, - name: str, - home_id: str, - ) -> tuple[bytes | None, str | None]: - """Retrieve the face of a given person.""" - for person in self.persons[home_id].values(): - if name == person.get("pseudo"): - image_id = person["face"]["id"] - key = person["face"]["key"] - return await self.async_get_camera_picture(image_id, key) - - return None, None diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py deleted file mode 100644 index 020dc92d..00000000 --- a/src/pyatmo/home_coach.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Support for Netatmo air care devices.""" -from warnings import warn - -from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 -from pyatmo.const import GETHOMECOACHDATA_ENDPOINT -from pyatmo.weather_station import AsyncWeatherStationData, WeatherStationData - -warn(f"The module {__name__} is deprecated.", DeprecationWarning, stacklevel=2) - - -class HomeCoachData(WeatherStationData): - """Class of Netatmo Home Coach devices (stations and modules).""" - - def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize self.""" - super().__init__(auth, endpoint=GETHOMECOACHDATA_ENDPOINT, favorites=False) - - -class AsyncHomeCoachData(AsyncWeatherStationData): - """Class of Netatmo Home Coach devices (stations and modules).""" - - def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize self.""" - super().__init__(auth, endpoint=GETHOMECOACHDATA_ENDPOINT, favorites=False) diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py deleted file mode 100644 index 117f8628..00000000 --- a/src/pyatmo/public_data.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Support for Netatmo public weather data.""" -from __future__ import annotations - -from abc import ABC -from collections import defaultdict -import dataclasses -from typing import Any -from warnings import warn - -from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 -from pyatmo.const import ( - ACCESSORY_GUST_ANGLE_TYPE, - ACCESSORY_GUST_STRENGTH_TYPE, - ACCESSORY_RAIN_24H_TYPE, - ACCESSORY_RAIN_60MIN_TYPE, - ACCESSORY_RAIN_LIVE_TYPE, - ACCESSORY_RAIN_TIME_TYPE, - ACCESSORY_WIND_ANGLE_TYPE, - ACCESSORY_WIND_STRENGTH_TYPE, - ACCESSORY_WIND_TIME_TYPE, - GETPUBLIC_DATA_ENDPOINT, - STATION_HUMIDITY_TYPE, - STATION_PRESSURE_TYPE, - STATION_TEMPERATURE_TYPE, -) -from pyatmo.exceptions import NoDevice -from pyatmo.modules import Location - -warn(f"The module {__name__} is deprecated.", DeprecationWarning, stacklevel=2) - - -class AbstractPublicData(ABC): - """Class of Netatmo public weather data.""" - - raw_data: dict = defaultdict(dict) - status: str = "" - - def process(self, resp: dict) -> None: - """Process data from API.""" - - self.status = resp.get("status", "") - - def stations_in_area(self) -> int: - """Return number of stations in area.""" - - return len(self.raw_data) - - def get_latest_rain(self) -> dict: - """Return latest rain measures.""" - - return self.get_accessory_data(ACCESSORY_RAIN_LIVE_TYPE) - - def get_average_rain(self) -> float: - """Return average rain measures.""" - - return average(self.get_latest_rain()) - - def get_60_min_rain(self) -> dict: - """Return 60 min rain measures.""" - - return self.get_accessory_data(ACCESSORY_RAIN_60MIN_TYPE) - - def get_average_60_min_rain(self) -> float: - """Return average 60 min rain measures.""" - - return average(self.get_60_min_rain()) - - def get_24_h_rain(self) -> dict: - """Return 24 h rain measures.""" - - return self.get_accessory_data(ACCESSORY_RAIN_24H_TYPE) - - def get_average_24_h_rain(self) -> float: - """Return average 24 h rain measures.""" - - return average(self.get_24_h_rain()) - - def get_latest_pressures(self) -> dict: - """Return latest pressure measures.""" - - return self.get_latest_station_measures(STATION_PRESSURE_TYPE) - - def get_average_pressure(self) -> float: - """Return average pressure measures.""" - - return average(self.get_latest_pressures()) - - def get_latest_temperatures(self) -> dict: - """Return latest temperature measures.""" - - return self.get_latest_station_measures(STATION_TEMPERATURE_TYPE) - - def get_average_temperature(self) -> float: - """Return average temperature measures.""" - - return average(self.get_latest_temperatures()) - - def get_latest_humidities(self) -> dict: - """Return latest humidity measures.""" - - return self.get_latest_station_measures(STATION_HUMIDITY_TYPE) - - def get_average_humidity(self) -> float: - """Return average humidity measures.""" - - return average(self.get_latest_humidities()) - - def get_latest_wind_strengths(self) -> dict: - """Return latest wind strengths.""" - - return self.get_accessory_data(ACCESSORY_WIND_STRENGTH_TYPE) - - def get_average_wind_strength(self) -> float: - """Return average wind strength.""" - - return average(self.get_latest_wind_strengths()) - - def get_latest_wind_angles(self) -> dict: - """Return latest wind angles.""" - - return self.get_accessory_data(ACCESSORY_WIND_ANGLE_TYPE) - - def get_latest_gust_strengths(self) -> dict: - """Return latest gust strengths.""" - - return self.get_accessory_data(ACCESSORY_GUST_STRENGTH_TYPE) - - def get_average_gust_strength(self) -> float: - """Return average gust strength.""" - - return average(self.get_latest_gust_strengths()) - - def get_latest_gust_angles(self): - """Return latest gust angles.""" - - return self.get_accessory_data(ACCESSORY_GUST_ANGLE_TYPE) - - def get_locations(self) -> dict: - """Return locations of stations.""" - - return { - station["_id"]: station["place"]["location"] for station in self.raw_data - } - - def get_time_for_rain_measures(self) -> dict: - """Return time for rain measures.""" - - return self.get_accessory_data(ACCESSORY_RAIN_TIME_TYPE) - - def get_time_for_wind_measures(self) -> dict: - """Return time for wind measures.""" - - return self.get_accessory_data(ACCESSORY_WIND_TIME_TYPE) - - def get_latest_station_measures(self, data_type) -> dict: - """Return latest station measures of a given type.""" - - measures: dict = {} - for station in self.raw_data: - for module in station["measures"].values(): - if ( - "type" in module - and data_type in module["type"] - and "res" in module - and module["res"] - ): - measure_index = module["type"].index(data_type) - latest_timestamp = sorted(module["res"], reverse=True)[0] - measures[station["_id"]] = module["res"][latest_timestamp][ - measure_index - ] - - return measures - - def get_accessory_data(self, data_type: str) -> dict[str, Any]: - """Return all accessory data of a given type.""" - - data: dict = {} - for station in self.raw_data: - for module in station["measures"].values(): - if data_type in module: - data[station["_id"]] = module[data_type] - - return data - - -class PublicData(AbstractPublicData): - """Class of Netatmo public weather data.""" - - def __init__( - self, - auth: NetatmoOAuth2, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - ) -> None: - """Initialize self.""" - - self.auth = auth - self.required_data_type = required_data_type - self.location = Location(lat_ne, lon_ne, lat_sw, lon_sw) - self.filtering: bool = filtering - - def update(self) -> None: - """Fetch and process data from API.""" - - post_params: dict = { - **dataclasses.asdict(self.location), - "filter": self.filtering, - } - - if self.required_data_type: - post_params["required_data"] = self.required_data_type - - resp = self.auth.post_api_request( - endpoint=GETPUBLIC_DATA_ENDPOINT, - params=post_params, - ).json() - try: - self.raw_data = resp["body"] - except (KeyError, TypeError) as exc: - raise NoDevice("No public weather data returned by Netatmo server") from exc - - self.process(resp) - - -class AsyncPublicData(AbstractPublicData): - """Class of Netatmo public weather data.""" - - def __init__( - self, - auth: AbstractAsyncAuth, - lat_ne: str, - lon_ne: str, - lat_sw: str, - lon_sw: str, - required_data_type: str | None = None, - filtering: bool = False, - ) -> None: - """Initialize self.""" - - self.auth = auth - self.required_data_type = required_data_type - self.location = Location(lat_ne, lon_ne, lat_sw, lon_sw) - self.filtering: bool = filtering - - async def async_update(self) -> None: - """Fetch and process data from API.""" - - post_params: dict = { - **dataclasses.asdict(self.location), - "filter": self.filtering, - } - - if self.required_data_type: - post_params["required_data"] = self.required_data_type - - resp = await self.auth.async_post_api_request( - endpoint=GETPUBLIC_DATA_ENDPOINT, - params=post_params, - ) - assert not isinstance(resp, bytes) - resp_data = await resp.json() - try: - self.raw_data = resp_data["body"] - except (KeyError, TypeError) as exc: - raise NoDevice("No public weather data returned by Netatmo server") from exc - - self.process(resp_data) - - -def average(data: dict) -> float: - """Calculate average value of a dict.""" - - return sum(data.values()) / len(data) if data else 0.0 diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py deleted file mode 100644 index bfd59ba6..00000000 --- a/src/pyatmo/thermostat.py +++ /dev/null @@ -1,387 +0,0 @@ -"""Support for Netatmo energy devices (relays, thermostats and valves).""" -from __future__ import annotations - -from abc import ABC -from collections import defaultdict -import logging -from typing import Any -from warnings import warn - -from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 -from pyatmo.const import ( - GETHOMESDATA_ENDPOINT, - GETHOMESTATUS_ENDPOINT, - SETROOMTHERMPOINT_ENDPOINT, - SETTHERMMODE_ENDPOINT, - SWITCHHOMESCHEDULE_ENDPOINT, -) -from pyatmo.exceptions import InvalidRoom, NoSchedule -from pyatmo.helpers import extract_raw_data - -LOG = logging.getLogger(__name__) - -warn(f"The module {__name__} is deprecated.", DeprecationWarning, stacklevel=2) - - -class AbstractHomeData(ABC): - """Abstract class of Netatmo energy devices.""" - - raw_data: dict = defaultdict(dict) - homes: dict = defaultdict(dict) - modules: dict = defaultdict(dict) - rooms: dict = defaultdict(dict) - schedules: dict = defaultdict(dict) - zones: dict = defaultdict(dict) - setpoint_duration: dict = defaultdict(dict) - - def process(self) -> None: - """Process data from API.""" - - self.homes = {d["id"]: d for d in self.raw_data} - - for item in self.raw_data: - home_id = item.get("id") - - if not (home_name := item.get("name")): - home_name = "Unknown" - self.homes[home_id]["name"] = home_name - - if "modules" not in item: - continue - - for module in item["modules"]: - self.modules[home_id][module["id"]] = module - - self.setpoint_duration[home_id] = item.get( - "therm_setpoint_default_duration", - ) - - for room in item.get("rooms", []): - self.rooms[home_id][room["id"]] = room - - for schedule in item.get("schedules", []): - schedule_id = schedule["id"] - self.schedules[home_id][schedule_id] = schedule - - if schedule_id not in self.zones[home_id]: - self.zones[home_id][schedule_id] = {} - - for zone in schedule["zones"]: - self.zones[home_id][schedule_id][zone["id"]] = zone - - def _get_selected_schedule(self, home_id: str) -> dict: - """Get the selected schedule for a given home ID.""" - - return next( - ( - value - for value in self.schedules.get(home_id, {}).values() - if "selected" in value - ), - {}, - ) - - def get_hg_temp(self, home_id: str) -> float | None: - """Return frost guard temperature value.""" - - return self._get_selected_schedule(home_id).get("hg_temp") - - def get_away_temp(self, home_id: str) -> float | None: - """Return the configured away temperature value.""" - - return self._get_selected_schedule(home_id).get("away_temp") - - def get_thermostat_type(self, home_id: str, room_id: str) -> str | None: - """Return the thermostat type of the room.""" - - return next( - ( - module.get("type") - for module in self.modules.get(home_id, {}).values() - if module.get("room_id") == room_id - ), - None, - ) - - def is_valid_schedule(self, home_id: str, schedule_id: str): - """Check if valid schedule.""" - - schedules = ( - self.schedules[home_id][s]["id"] for s in self.schedules.get(home_id, {}) - ) - return schedule_id in schedules - - -class HomeData(AbstractHomeData): - """Class of Netatmo energy devices.""" - - def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize the Netatmo home data.""" - - self.auth = auth - - def update(self) -> None: - """Fetch and process data from API.""" - - resp = self.auth.post_api_request(endpoint=GETHOMESDATA_ENDPOINT) - - self.raw_data = extract_raw_data(resp.json(), "homes") - self.process() - - def switch_home_schedule(self, home_id: str, schedule_id: str) -> Any: - """Switch the schedule for a give home ID.""" - - if not self.is_valid_schedule(home_id, schedule_id): - raise NoSchedule(f"{schedule_id} is not a valid schedule id") - - post_params = {"home_id": home_id, "schedule_id": schedule_id} - resp = self.auth.post_api_request( - endpoint=SWITCHHOMESCHEDULE_ENDPOINT, - params=post_params, - ) - LOG.debug("Response: %s", resp) - - -class AsyncHomeData(AbstractHomeData): - """Class of Netatmo energy devices.""" - - def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize the Netatmo home data.""" - - self.auth = auth - - async def async_update(self): - """Fetch and process data from API.""" - - resp = await self.auth.async_post_api_request(endpoint=GETHOMESDATA_ENDPOINT) - - assert not isinstance(resp, bytes) - self.raw_data = extract_raw_data(await resp.json(), "homes") - self.process() - - async def async_switch_home_schedule(self, home_id: str, schedule_id: str) -> None: - """Switch the schedule for a give home ID.""" - - if not self.is_valid_schedule(home_id, schedule_id): - raise NoSchedule(f"{schedule_id} is not a valid schedule id") - - resp = await self.auth.async_post_api_request( - endpoint=SWITCHHOMESCHEDULE_ENDPOINT, - params={"home_id": home_id, "schedule_id": schedule_id}, - ) - LOG.debug("Response: %s", resp) - - -class AbstractHomeStatus(ABC): - """Abstract class of the Netatmo home status.""" - - raw_data: dict = defaultdict(dict) - rooms: dict = defaultdict(dict) - thermostats: dict = defaultdict(dict) - valves: dict = defaultdict(dict) - relays: dict = defaultdict(dict) - - def process(self) -> None: - """Process data from API.""" - - for room in self.raw_data.get("rooms", []): - self.rooms[room["id"]] = room - - for module in self.raw_data.get("modules", []): - if module["type"] in {"NATherm1", "OTM"}: - self.thermostats[module["id"]] = module - - elif module["type"] == "NRV": - self.valves[module["id"]] = module - - elif module["type"] in {"OTH", "NAPlug"}: - self.relays[module["id"]] = module - - def get_room(self, room_id: str) -> dict: - """Return room data for a given room id.""" - - for value in self.rooms.values(): - if value["id"] == room_id: - return value - - raise InvalidRoom(f"No room with ID {room_id}") - - def get_thermostat(self, room_id: str) -> dict: - """Return thermostat data for a given room id.""" - - for value in self.thermostats.values(): - if value["id"] == room_id: - return value - - raise InvalidRoom(f"No room with ID {room_id}") - - def get_relay(self, room_id: str) -> dict: - """Return relay data for a given room id.""" - - for value in self.relays.values(): - if value["id"] == room_id: - return value - - raise InvalidRoom(f"No room with ID {room_id}") - - def get_valve(self, room_id: str) -> dict: - """Return valve data for a given room id.""" - - for value in self.valves.values(): - if value["id"] == room_id: - return value - - raise InvalidRoom(f"No room with ID {room_id}") - - def set_point(self, room_id: str) -> float | None: - """Return the setpoint of a given room.""" - - return self.get_room(room_id).get("therm_setpoint_temperature") - - def set_point_mode(self, room_id: str) -> str | None: - """Return the setpointmode of a given room.""" - - return self.get_room(room_id).get("therm_setpoint_mode") - - def measured_temperature(self, room_id: str) -> float | None: - """Return the measured temperature of a given room.""" - - return self.get_room(room_id).get("therm_measured_temperature") - - def boiler_status(self, module_id: str) -> bool | None: - """Return the status of the boiler status.""" - - return self.get_thermostat(module_id).get("boiler_status") - - -class HomeStatus(AbstractHomeStatus): - """Class of the Netatmo home status.""" - - def __init__(self, auth: NetatmoOAuth2, home_id: str): - """Initialize the Netatmo home status.""" - - self.auth = auth - self.home_id = home_id - - def update(self) -> None: - """Fetch and process data from API.""" - - resp = self.auth.post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": self.home_id}, - ) - - self.raw_data = extract_raw_data(resp.json(), "home") - self.process() - - def set_thermmode( - self, - mode: str, - end_time: int | None = None, - schedule_id: str | None = None, - ) -> str | None: - """Set thermotat mode.""" - - post_params = {"home_id": self.home_id, "mode": mode} - if end_time is not None and mode in {"hg", "away"}: - post_params["endtime"] = str(end_time) - - if schedule_id is not None and mode == "schedule": - post_params["schedule_id"] = schedule_id - - return self.auth.post_api_request( - endpoint=SETTHERMMODE_ENDPOINT, - params=post_params, - ).json() - - def set_room_thermpoint( - self, - room_id: str, - mode: str, - temp: float | None = None, - end_time: int | None = None, - ) -> str | None: - """Set room themperature set point.""" - - post_params = {"home_id": self.home_id, "room_id": room_id, "mode": mode} - # Temp and endtime should only be sent when mode=='manual', but netatmo api can - # handle that even when mode == 'home' and these settings don't make sense - if temp is not None: - post_params["temp"] = str(temp) - - if end_time is not None: - post_params["endtime"] = str(end_time) - - return self.auth.post_api_request( - endpoint=SETROOMTHERMPOINT_ENDPOINT, - params=post_params, - ).json() - - -class AsyncHomeStatus(AbstractHomeStatus): - """Class of the Netatmo home status.""" - - def __init__(self, auth: AbstractAsyncAuth, home_id: str): - """Initialize the Netatmo home status.""" - - self.auth = auth - self.home_id = home_id - - async def async_update(self) -> None: - """Fetch and process data from API.""" - - resp = await self.auth.async_post_api_request( - endpoint=GETHOMESTATUS_ENDPOINT, - params={"home_id": self.home_id}, - ) - - assert not isinstance(resp, bytes) - self.raw_data = extract_raw_data(await resp.json(), "home") - self.process() - - async def async_set_thermmode( - self, - mode: str, - end_time: int | None = None, - schedule_id: str | None = None, - ) -> str | None: - """Set thermotat mode.""" - - post_params = {"home_id": self.home_id, "mode": mode} - if end_time is not None and mode in {"hg", "away"}: - post_params["endtime"] = str(end_time) - - if schedule_id is not None and mode == "schedule": - post_params["schedule_id"] = schedule_id - - resp = await self.auth.async_post_api_request( - endpoint=SETTHERMMODE_ENDPOINT, - params=post_params, - ) - assert not isinstance(resp, bytes) - return await resp.json() - - async def async_set_room_thermpoint( - self, - room_id: str, - mode: str, - temp: float | None = None, - end_time: int | None = None, - ) -> str | None: - """Set room themperature set point.""" - - post_params = {"home_id": self.home_id, "room_id": room_id, "mode": mode} - # Temp and endtime should only be sent when mode=='manual', but netatmo api can - # handle that even when mode == 'home' and these settings don't make sense - if temp is not None: - post_params["temp"] = str(temp) - - if end_time is not None: - post_params["endtime"] = str(end_time) - - resp = await self.auth.async_post_api_request( - endpoint=SETROOMTHERMPOINT_ENDPOINT, - params=post_params, - ) - assert not isinstance(resp, bytes) - return await resp.json() diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py deleted file mode 100644 index 14b9ca71..00000000 --- a/src/pyatmo/weather_station.py +++ /dev/null @@ -1,328 +0,0 @@ -"""Support for Netatmo weather station devices (stations and modules).""" -from __future__ import annotations - -from abc import ABC -from collections import defaultdict -import logging -import time -from warnings import warn - -from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 -from pyatmo.const import GETMEASURE_ENDPOINT, GETSTATIONDATA_ENDPOINT -from pyatmo.helpers import extract_raw_data, today_stamps - -LOG = logging.getLogger(__name__) - - -warn(f"The module {__name__} is deprecated.", DeprecationWarning, stacklevel=2) - - -class AbstractWeatherStationData(ABC): - """Abstract class of Netatmo Weather Station devices.""" - - raw_data: dict = defaultdict(dict) - stations: dict = defaultdict(dict) - modules: dict = defaultdict(dict) - - def process(self) -> None: - """Process data from API.""" - - self.stations = {d["_id"]: d for d in self.raw_data} - self.modules = {} - - for item in self.raw_data: - # The station name is sometimes not contained in the backend data - if "station_name" not in item: - item["station_name"] = item.get("home_name", item["type"]) - - if "modules" not in item: - item["modules"] = [item] - - for module in item["modules"]: - if "module_name" not in module and module["type"] == "NHC": - module["module_name"] = module["station_name"] - - self.modules[module["_id"]] = module - self.modules[module["_id"]]["main_device"] = item["_id"] - - def get_module_names(self, station_id: str) -> list: - """Return a list of all module names for a given station.""" - - if not (station_data := self.get_station(station_id)): - return [] - - res = {station_data.get("module_name", station_data.get("type"))} - for module in station_data["modules"]: - # Add module name, use module type if no name is available - res.add(module.get("module_name", module.get("type"))) - - return list(res) - - def get_modules(self, station_id: str) -> dict: - """Return a dict of modules per given station.""" - - if not (station_data := self.get_station(station_id)): - return {} - - res = {} - for station in [self.stations[station_data["_id"]]]: - station_type = station.get("type") - station_name = station.get("station_name", station_type) - res[station["_id"]] = { - "station_name": station_name, - "module_name": station.get("module_name", station_type), - "id": station["_id"], - } - - for module in station["modules"]: - res[module["_id"]] = { - "station_name": module.get("station_name", station_name), - "module_name": module.get("module_name", module.get("type")), - "id": module["_id"], - } - - return res - - def get_station(self, station_id: str) -> dict: - """Return station by id.""" - - return self.stations.get(station_id, {}) - - def get_module(self, module_id: str) -> dict: - """Return module by id.""" - - return self.modules.get(module_id, {}) - - def get_monitored_conditions(self, module_id: str) -> list: - """Return monitored conditions for given module.""" - - if not (module := (self.get_module(module_id) or self.get_station(module_id))): - return [] - - conditions = [] - for condition in module.get("data_type", []): - if condition == "Wind": - # the Wind meter actually exposes the following conditions - conditions.extend( - ["WindAngle", "WindStrength", "GustAngle", "GustStrength"], - ) - - elif condition == "Rain": - conditions.extend(["Rain", "sum_rain_24", "sum_rain_1"]) - - else: - conditions.append(condition) - - if module["type"] in ["NAMain", "NHC"]: - # the main module has wifi_status - conditions.append("wifi_status") - - else: - # assume all other modules have rf_status, battery_vp, and battery_percent - conditions.extend(["rf_status", "battery_vp", "battery_percent"]) - - if module["type"] in ["NAMain", "NAModule1", "NAModule4"]: - conditions.extend(["temp_trend"]) - - if module["type"] == "NAMain": - conditions.extend(["pressure_trend"]) - - if module["type"] in [ - "NAMain", - "NAModule1", - "NAModule2", - "NAModule3", - "NAModule4", - "NHC", - ]: - conditions.append("reachable") - - return conditions - - def get_last_data(self, station_id: str, exclude: int = 0) -> dict: - """Return data for a given station and time frame.""" - - key = "_id" - last_data: dict = {} - - if ( - not (station := self.get_station(station_id)) - or "dashboard_data" not in station - ): - LOG.debug("No dashboard data for station %s", station_id) - return last_data - - # Define oldest acceptable sensor measure event - limit = (time.time() - exclude) if exclude else 0 - - data = station["dashboard_data"] - if key in station and data["time_utc"] > limit: - last_data[station[key]] = data.copy() - last_data[station[key]]["When"] = last_data[station[key]].pop("time_utc") - last_data[station[key]]["wifi_status"] = station.get("wifi_status") - last_data[station[key]]["reachable"] = station.get("reachable") - - for module in station["modules"]: - if "dashboard_data" not in module or key not in module: - continue - - data = module["dashboard_data"] - if "time_utc" in data and data["time_utc"] > limit: - last_data[module[key]] = data.copy() - last_data[module[key]]["When"] = last_data[module[key]].pop("time_utc") - - # For potential use, add battery and radio coverage information - # to module data if present - for val in ( - "rf_status", - "battery_vp", - "battery_percent", - "reachable", - "wifi_status", - ): - if val in module: - last_data[module[key]][val] = module[val] - - return last_data - - def check_not_updated(self, station_id: str, delay: int = 3600) -> list: - """Check if a given station has not been updated.""" - - res = self.get_last_data(station_id) - return [ - key for key, value in res.items() if time.time() - value["When"] > delay - ] - - def check_updated(self, station_id: str, delay: int = 3600) -> list: - """Check if a given station has been updated.""" - - res = self.get_last_data(station_id) - return [ - key for key, value in res.items() if time.time() - value["When"] < delay - ] - - -class WeatherStationData(AbstractWeatherStationData): - """Class of Netatmo weather station devices.""" - - def __init__( - self, - auth: NetatmoOAuth2, - endpoint: str = GETSTATIONDATA_ENDPOINT, - favorites: bool = True, - ) -> None: - """Initialize the Netatmo weather station data.""" - - self.auth = auth - self.endpoint = endpoint - self.params = {"get_favorites": ("true" if favorites else "false")} - - def update(self): - """Fetch data from API.""" - - self.raw_data = extract_raw_data( - self.auth.post_api_request( - endpoint=self.endpoint, - params=self.params, - ).json(), - "devices", - ) - self.process() - - def get_data( - self, - device_id: str, - scale: str, - module_type: str, - module_id: str | None = None, - date_begin: float | None = None, - date_end: float | None = None, - limit: int | None = None, - optimize: bool = False, - real_time: bool = False, - ) -> dict | None: - """Retrieve data from a device or module.""" - - post_params = {"device_id": device_id} - if module_id: - post_params["module_id"] = module_id - - post_params["scale"] = scale - post_params["type"] = module_type - - if date_begin: - post_params["date_begin"] = f"{date_begin}" - - if date_end: - post_params["date_end"] = f"{date_end}" - - if limit: - post_params["limit"] = f"{limit}" - - post_params["optimize"] = "true" if optimize else "false" - post_params["real_time"] = "true" if real_time else "false" - - return self.auth.post_api_request( - endpoint=GETMEASURE_ENDPOINT, - params=post_params, - ).json() - - def get_min_max_t_h( - self, - station_id: str, - module_id: str | None = None, - frame: str = "last24", - ) -> tuple[float, float, float, float] | None: - """Return minimum and maximum temperature and humidity over the given timeframe.""" - - if frame == "last24": - end = time.time() - start = end - 24 * 3600 # 24 hours ago - - elif frame == "day": - start, end = today_stamps() - - else: - raise ValueError("'frame' value can only be 'last24' or 'day'") - - if resp := self.get_data( - device_id=station_id, - module_id=module_id, - scale="max", - module_type="Temperature,Humidity", - date_begin=start, - date_end=end, - ): - temperature = [temp[0] for temp in resp["body"].values()] - humidity = [hum[1] for hum in resp["body"].values()] - return min(temperature), max(temperature), min(humidity), max(humidity) - - return None - - -class AsyncWeatherStationData(AbstractWeatherStationData): - """Class of Netatmo weather station devices.""" - - def __init__( - self, - auth: AbstractAsyncAuth, - endpoint: str = GETSTATIONDATA_ENDPOINT, - favorites: bool = True, - ) -> None: - """Initialize the Netatmo weather station data.""" - - self.auth = auth - self.endpoint = endpoint - self.params = {"get_favorites": ("true" if favorites else "false")} - - async def async_update(self): - """Fetch data from API.""" - - resp = await self.auth.async_post_api_request( - endpoint=self.endpoint, - params=self.params, - ) - - assert not isinstance(resp, bytes) - self.raw_data = extract_raw_data(await resp.json(), "devices") - self.process() diff --git a/tests/conftest.py b/tests/conftest.py index a312ff0b..0b0c7703 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,12 @@ """Define shared fixtures.""" # pylint: disable=redefined-outer-name, protected-access from contextlib import contextmanager -import json from unittest.mock import AsyncMock, patch import pyatmo import pytest -from .common import MockResponse, fake_post_request +from .common import fake_post_request @contextmanager @@ -15,151 +14,6 @@ def does_not_raise(): yield -@pytest.fixture(scope="function") -def auth(requests_mock): - """Auth fixture.""" - with open("fixtures/oauth2_token.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.AUTH_REQ_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - return pyatmo.ClientAuth( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - username="USERNAME", - password="PASSWORD", - scope=" ".join(pyatmo.const.ALL_SCOPES), - ) - - -@pytest.fixture(scope="function") -def home_data(auth, requests_mock): - """HomeData fixture.""" - with open("fixtures/home_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_data = pyatmo.HomeData(auth) - home_data.update() - return home_data - - -@pytest.fixture(scope="function") -def home_status(auth, home_id, requests_mock): - """HomeStatus fixture.""" - with open("fixtures/home_status_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESTATUS_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_status = pyatmo.HomeStatus(auth, home_id) - home_status.update() - return home_status - - -@pytest.fixture(scope="function") -def public_data(auth, requests_mock): - """PublicData fixture.""" - with open("fixtures/public_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETPUBLIC_DATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - lon_ne = str(6.221652) - lat_ne = str(46.610870) - lon_sw = str(6.217828) - lat_sw = str(46.596485) - - public_data = pyatmo.PublicData(auth, lat_ne, lon_ne, lat_sw, lon_sw) - public_data.update() - return public_data - - -@pytest.fixture(scope="function") -def weather_station_data(auth, requests_mock): - """WeatherStationData fixture.""" - with open( - "fixtures/weatherstation_data_simple.json", - encoding="utf-8", - ) as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETSTATIONDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - wsd = pyatmo.WeatherStationData(auth) - wsd.update() - return wsd - - -@pytest.fixture(scope="function") -def home_coach_data(auth, requests_mock): - """HomeCoachData fixture.""" - with open("fixtures/home_coach_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMECOACHDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - hcd = pyatmo.HomeCoachData(auth) - hcd.update() - return hcd - - -@pytest.fixture(scope="function") -def camera_ping(requests_mock): - """Camera ping fixture.""" - for index in ["w", "z", "g"]: - vpn_url = ( - f"https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" - f"6d278460699e56180d47ab47169efb31/" - f"MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTT{index},," - ) - with open("fixtures/camera_ping.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - f"{vpn_url}/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - f"{local_url}/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - -@pytest.fixture(scope="function") -def camera_home_data(auth, camera_ping, requests_mock): # pylint: disable=W0613 - """CameraHomeData fixture.""" - with open("fixtures/camera_home_data.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMEDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - camera_data = pyatmo.CameraData(auth) - camera_data.update() - return camera_data - - @pytest.fixture(scope="function") async def async_auth(): """AsyncAuth fixture.""" @@ -167,108 +21,6 @@ async def async_auth(): yield auth -@pytest.fixture(scope="function") -async def async_camera_home_data(async_auth): - """AsyncCameraHomeData fixture.""" - with open("fixtures/camera_home_data.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_api_request, patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - camera_data = pyatmo.AsyncCameraData(async_auth) - await camera_data.async_update() - - mock_api_request.assert_called() - mock_request.assert_called() - yield camera_data - - -@pytest.fixture(scope="function") -async def async_home_coach_data(async_auth): - """AsyncHomeCoacheData fixture.""" - with open("fixtures/home_coach_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - hcd = pyatmo.AsyncHomeCoachData(async_auth) - await hcd.async_update() - - mock_request.assert_awaited() - yield hcd - - -@pytest.fixture(scope="function") -async def async_home_data(async_auth): - """AsyncHomeData fixture.""" - with open("fixtures/home_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - home_data = pyatmo.AsyncHomeData(async_auth) - await home_data.async_update() - - mock_request.assert_called() - return home_data - - -@pytest.fixture(scope="function") -async def async_home_status(async_auth, home_id): - """AsyncHomeStatus fixture.""" - with open("fixtures/home_status_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - home_status = pyatmo.AsyncHomeStatus(async_auth, home_id) - await home_status.async_update() - - mock_request.assert_called() - return home_status - - -@pytest.fixture(scope="function") -async def async_weather_station_data(async_auth): - """AsyncWeatherStationData fixture.""" - with open( - "fixtures/weatherstation_data_simple.json", - encoding="utf-8", - ) as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - wsd = pyatmo.AsyncWeatherStationData(async_auth) - await wsd.async_update() - - mock_request.assert_called() - return wsd - - @pytest.fixture(scope="function") async def async_account(async_auth): """AsyncAccount fixture.""" diff --git a/tests/test_async.py b/tests/test_async.py deleted file mode 100644 index cef46766..00000000 --- a/tests/test_async.py +++ /dev/null @@ -1,605 +0,0 @@ -"""Define tests for async methods.""" -# pylint: disable=protected-access -import json -from unittest.mock import AsyncMock, patch - -import pyatmo -import pytest - -from tests.conftest import MockResponse, does_not_raise - -LON_NE = "6.221652" -LAT_NE = "46.610870" -LON_SW = "6.217828" -LAT_SW = "46.596485" - - -@pytest.mark.asyncio -async def test_async_auth(async_auth, mocker): - with open("fixtures/camera_home_data.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_api_request, patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - camera_data = pyatmo.AsyncCameraData(async_auth) - await camera_data.async_update() - - mock_api_request.assert_awaited() - mock_request.assert_awaited() - assert camera_data.homes is not None - - -@pytest.mark.asyncio -async def test_async_camera_data(async_camera_home_data): - assert async_camera_home_data.homes is not None - - -@pytest.mark.asyncio -async def test_async_home_data_no_body(async_auth): - with open("fixtures/camera_data_empty.json", encoding="utf-8") as fixture_file: - json_fixture = json.load(fixture_file) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=json_fixture), - ) as mock_request: - camera_data = pyatmo.AsyncCameraData(async_auth) - - with pytest.raises(pyatmo.NoDevice): - await camera_data.async_update() - mock_request.assert_awaited() - - -@pytest.mark.asyncio -async def test_async_home_data_no_homes(async_auth): - with open( - "fixtures/camera_home_data_no_homes.json", - encoding="utf-8", - ) as fixture_file: - json_fixture = json.load(fixture_file) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=json_fixture), - ) as mock_request: - camera_data = pyatmo.AsyncCameraData(async_auth) - - with pytest.raises(pyatmo.NoDevice): - await camera_data.async_update() - mock_request.assert_awaited() - - -@pytest.mark.asyncio -async def test_async_home_coach_data(async_home_coach_data): - assert ( - async_home_coach_data.stations["12:34:56:26:69:0c"]["station_name"] == "Bedroom" - ) - - -@pytest.mark.asyncio -async def test_async_public_data(async_auth): - with open("fixtures/public_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ) as mock_request: - public_data = pyatmo.AsyncPublicData(async_auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - await public_data.async_update() - - mock_request.assert_awaited() - assert public_data.status == "ok" - - public_data = pyatmo.AsyncPublicData( - async_auth, - LAT_NE, - LON_NE, - LAT_SW, - LON_SW, - required_data_type="temperature,rain_live", - ) - await public_data.async_update() - assert public_data.status == "ok" - - -@pytest.mark.asyncio -async def test_async_public_data_error(async_auth): - with open("fixtures/public_data_error_mongo.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ): - public_data = pyatmo.AsyncPublicData(async_auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - - with pytest.raises(pyatmo.NoDevice): - await public_data.async_update() - - -@pytest.mark.asyncio -async def test_async_home_data(async_home_data): - expected = { - "12:34:56:00:fa:d0": { - "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", - ], - }, - "12:34:56:00:01:ae": { - "id": "12:34:56:00:01:ae", - "type": "NATherm1", - "name": "Livingroom", - "setup_date": 1494963356, - "room_id": "2746182631", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:03:a5:54": { - "id": "12:34:56:03:a5:54", - "type": "NRV", - "name": "Valve1", - "setup_date": 1554549767, - "room_id": "2833524037", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:03:a0:ac": { - "id": "12:34:56:03:a0:ac", - "type": "NRV", - "name": "Valve2", - "setup_date": 1554554444, - "room_id": "2940411577", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:00:f1:62": { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "name": "Hall", - "setup_date": 1544828430, - "room_id": "3688132631", - }, - } - assert async_home_data.modules["91763b24c43d3e344f424e8b"] == expected - - -@pytest.mark.asyncio -async def test_async_home_data_no_data(async_auth): - mock_resp = MockResponse(None, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ), pytest.raises(pyatmo.NoDevice): - home_data = pyatmo.AsyncHomeData(async_auth) - await home_data.async_update() - - -@pytest.mark.asyncio -async def test_async_data_no_body(async_auth): - with open("fixtures/home_data_empty.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ): - home_data = pyatmo.AsyncHomeData(async_auth) - with pytest.raises(pyatmo.NoDevice): - await home_data.async_update() - - -@pytest.mark.parametrize( - "t_home_id, t_sched_id, expected", - [ - ("91763b24c43d3e344f424e8b", "591b54a2764ff4d50d8b5795", does_not_raise()), - ( - "91763b24c43d3e344f424e8b", - "123456789abcdefg12345678", - pytest.raises(pyatmo.NoSchedule), - ), - ], -) -@pytest.mark.asyncio -async def test_async_home_data_switch_home_schedule( - async_home_data, - t_home_id, - t_sched_id, - expected, -): - with open("fixtures/status_ok.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=json_fixture), - ), expected: - await async_home_data.async_switch_home_schedule( - home_id=t_home_id, - schedule_id=t_sched_id, - ) - - -@pytest.mark.parametrize( - "home_id, room_id, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "2746182631", - { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - }, - ), - ], -) -@pytest.mark.asyncio -async def test_async_home_status(async_home_status, room_id, expected): - assert len(async_home_status.rooms) == 3 - assert async_home_status.rooms[room_id] == expected - - -@pytest.mark.parametrize( - "home_id, mode, end_time, schedule_id, json_fixture, expected", - [ - ( - None, - None, - None, - None, - "home_status_error_mode_is_missing.json", - "mode is missing", - ), - ( - "91763b24c43d3e344f424e8b", - None, - None, - None, - "home_status_error_mode_is_missing.json", - "mode is missing", - ), - ( - "invalidID", - "away", - None, - None, - "home_status_error_invalid_id.json", - "Invalid id", - ), - ("91763b24c43d3e344f424e8b", "away", None, None, "status_ok.json", "ok"), - ("91763b24c43d3e344f424e8b", "away", 1559162650, None, "status_ok.json", "ok"), - ( - "91763b24c43d3e344f424e8b", - "away", - 1559162650, - 0000000, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - None, - "591b54a2764ff4d50d8b5795", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - 1559162650, - "591b54a2764ff4d50d8b5795", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - None, - "blahblahblah", - "home_status_error_invalid_schedule_id.json", - "schedule is not therm schedule", - ), - ], -) -@pytest.mark.asyncio -async def test_async_home_status_set_thermmode( - async_home_status, - mode, - end_time, - schedule_id, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ): - res = await async_home_status.async_set_thermmode( - mode=mode, - end_time=end_time, - schedule_id=schedule_id, - ) - if "error" in res: - assert expected in res["error"]["message"] - else: - assert expected in res["status"] - - -@pytest.mark.parametrize( - "home_id, room_id, mode, temp, end_time, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - 14, - None, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - 14, - 1559162650, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - None, - None, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - None, - 1559162650, - "status_ok.json", - "ok", - ), - ], -) -@pytest.mark.asyncio -async def test_async_home_status_set_room_thermpoint( - async_home_status, - room_id, - mode, - temp, - end_time, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - - mock_resp = MockResponse(json_fixture, 200) - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=mock_resp), - ): - result = await async_home_status.async_set_room_thermpoint( - room_id=room_id, - mode=mode, - temp=temp, - end_time=end_time, - ) - assert result["status"] == expected - - -@pytest.mark.asyncio -async def test_async_camera_live_snapshot(async_camera_home_data): - _id = "12:34:56:00:f1:62" - - assert async_camera_home_data.homes is not None - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_get_image", - AsyncMock(return_value=b"0000"), - ): - result = await async_camera_home_data.async_get_live_snapshot(camera_id=_id) - - assert result == b"0000" - - -@pytest.mark.asyncio -async def test_async_camera_data_get_camera_picture(async_camera_home_data): - image_id = "5c22739723720a6e278c43bf" - key = "276751836a6d1a71447f8d975494c87bc125766a970f7e022e79e001e021d756" - with open( - "fixtures/camera_image_sample.jpg", - "rb", - ) as fixture_file: - expect = fixture_file.read() - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_get_image", - AsyncMock(return_value=expect), - ): - assert await async_camera_home_data.async_get_camera_picture(image_id, key) == ( - expect, - "jpeg", - ) - - -@pytest.mark.asyncio -async def test_async_camera_data_get_profile_image(async_camera_home_data): - with open( - "fixtures/camera_image_sample.jpg", - "rb", - ) as fixture_file: - expect = fixture_file.read() - - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_get_image", - AsyncMock(return_value=expect), - ): - assert await async_camera_home_data.async_get_profile_image( - "John Doe", - "91763b24c43d3e344f424e8b", - ) == ( - expect, - "jpeg", - ) - assert await async_camera_home_data.async_get_profile_image( - "Jack Foe", - "91763b24c43d3e344f424e8b", - ) == ( - None, - None, - ) - - -@pytest.mark.parametrize( - "home_id, person_id, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "91827374-7e04-5298-83ad-a0cb8372dff1", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "91827376-7e04-5298-83af-a0cb8372dff3", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - None, - "status_ok.json", - "ok", - ), - ], -) -@pytest.mark.asyncio -async def test_async_camera_data_set_persons_away( - async_camera_home_data, - home_id, - person_id, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=json_fixture), - ) as mock_req: - result = await async_camera_home_data.async_set_persons_away(home_id, person_id) - assert result["status"] == expected - - if person_id is not None: - mock_req.assert_awaited_once_with( - params={ - "home_id": home_id, - "person_id": person_id, - }, - endpoint="api/setpersonsaway", - ) - else: - mock_req.assert_awaited_once_with( - params={ - "home_id": home_id, - }, - endpoint="api/setpersonsaway", - ) - - -@pytest.mark.parametrize( - "home_id, person_ids, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - [ - "91827374-7e04-5298-83ad-a0cb8372dff1", - "91827376-7e04-5298-83af-a0cb8372dff3", - ], - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "91827376-7e04-5298-83af-a0cb8372dff3", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - None, - "status_ok.json", - "ok", - ), - ], -) -@pytest.mark.asyncio -async def test_async_camera_data_set_persons_home( - async_camera_home_data, - home_id, - person_ids, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - with patch( - "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", - AsyncMock(return_value=json_fixture), - ) as mock_req: - result = await async_camera_home_data.async_set_persons_home( - home_id, - person_ids, - ) - assert result["status"] == expected - - if isinstance(person_ids, list) or person_ids: - mock_req.assert_awaited_once_with( - params={ - "home_id": home_id, - "person_ids[]": person_ids, - }, - endpoint="api/setpersonshome", - ) - else: - mock_req.assert_awaited_once_with( - params={ - "home_id": home_id, - }, - endpoint="api/setpersonshome", - ) diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py deleted file mode 100644 index c4d76d76..00000000 --- a/tests/test_pyatmo.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Define tests for untility methods.""" -# pylint: disable=protected-access -import json -import time - -import oauthlib -import pyatmo -import pytest - - -def test_client_auth(auth): - assert auth._oauth.token["access_token"] == ( - "91763b24c43d3e344f424e8b|880b55a08c758e87ff8755a00c6b8a12" - ) - assert auth._oauth.token["refresh_token"] == ( - "91763b24c43d3e344f424e8b|87ff8755a00c6b8a120b55a08c758e93" - ) - - -def test_client_auth_invalid(requests_mock): - with open("fixtures/invalid_grant.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.auth.AUTH_REQ_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(oauthlib.oauth2.rfc6749.errors.InvalidGrantError): - pyatmo.ClientAuth( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - username="USERNAME", - password="PASSWORD", - ) - - -def test_post_request_json(auth, requests_mock): - """Test wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL, - json={"a": "b"}, - headers={"content-type": "application/json"}, - ) - resp = auth.post_request(pyatmo.const.DEFAULT_BASE_URL, None).json() - assert resp == {"a": "b"} - - -def test_post_request_binary(auth, requests_mock): - """Test wrapper for posting requests against the Netatmo API.""" - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL, - text="Success", - headers={"content-type": "application/text"}, - ) - resp = auth.post_request(pyatmo.const.DEFAULT_BASE_URL, None).content - assert resp == b"Success" - - -@pytest.mark.parametrize("test_input,expected", [(200, None), (404, None), (401, None)]) -def test_post_request_fail(auth, requests_mock, test_input, expected): - """Test failing requests against the Netatmo API.""" - requests_mock.post(pyatmo.const.DEFAULT_BASE_URL, status_code=test_input) - - if test_input == 200: - resp = auth.post_request(pyatmo.const.DEFAULT_BASE_URL, None).content - assert resp is expected - else: - with pytest.raises(pyatmo.ApiError): - resp = auth.post_request(pyatmo.const.DEFAULT_BASE_URL, None).content - - -@pytest.mark.parametrize( - "test_input,expected", - [ - (1, "1970-01-01_00:00:01"), - (0, "1970-01-01_00:00:00"), - (-1, "1969-12-31_23:59:59"), - (2000000000, "2033-05-18_03:33:20"), - ("1", "1970-01-01_00:00:01"), - pytest.param("A", None, marks=pytest.mark.xfail), - pytest.param([1], None, marks=pytest.mark.xfail), - pytest.param({1}, None, marks=pytest.mark.xfail), - ], -) -def test_to_time_string(test_input, expected): - """Test time to string conversion.""" - assert pyatmo.helpers.to_time_string(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ("1970-01-01_00:00:01", 1), - ("1970-01-01_00:00:00", 0), - ("1969-12-31_23:59:59", -1), - ("2033-05-18_03:33:20", 2000000000), - ], -) -def test_to_epoch(test_input, expected): - """Test time to epoch conversion.""" - assert pyatmo.helpers.to_epoch(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ("2018-06-21", (1529539200, 1529625600)), - ("2000-01-01", (946684800, 946771200)), - pytest.param("2000-04-31", None, marks=pytest.mark.xfail), - ], -) -def test_today_stamps(monkeypatch, test_input, expected): - """Test today_stamps function.""" - - def mockreturn(_): - return test_input - - monkeypatch.setattr(time, "strftime", mockreturn) - assert pyatmo.helpers.today_stamps() == expected diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py deleted file mode 100644 index d10d047e..00000000 --- a/tests/test_pyatmo_camera.py +++ /dev/null @@ -1,579 +0,0 @@ -"""Define tests for Camera module.""" -# pylint: disable=protected-access -import datetime as dt -import json - -import pyatmo -import pytest -import time_machine - -from .conftest import does_not_raise - - -def test_camera_data(camera_home_data): - assert camera_home_data.homes is not None - - -def test_home_data_no_body(auth, requests_mock): - with open("fixtures/camera_data_empty.json", encoding="utf-8") as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMEDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - camera_data = pyatmo.CameraData(auth) - camera_data.update() - - -def test_home_data_no_homes(auth, requests_mock): - with open( - "fixtures/camera_home_data_no_homes.json", - encoding="utf-8", - ) as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMEDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - camera_data = pyatmo.CameraData(auth) - camera_data.update() - - -@pytest.mark.parametrize( - "cid, expected", - [ - ("12:34:56:00:f1:62", "Hall"), - ("12:34:56:00:a5:a4", "Garden"), - ("12:34:56:00:a5:a6", "NOC"), - ("None", None), - (None, None), - ], -) -def test_camera_data_get_camera(camera_home_data, cid, expected): - camera = camera_home_data.get_camera(cid) - assert camera.get("name") == expected - - -def test_camera_data_get_module(camera_home_data): - assert camera_home_data.get_module("00:00:00:00:00:00") is None - - -def test_camera_data_camera_urls(camera_home_data, requests_mock): - cid = "12:34:56:00:f1:62" - vpn_url = ( - "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/" - "6d278460699e56180d47ab47169efb31/" - "MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTg,," - ) - local_url = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" - with open("fixtures/camera_ping.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - f"{vpn_url}/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - with open("fixtures/camera_ping.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - f"{local_url}/command/ping", - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - camera_home_data.update_camera_urls(cid) - - assert camera_home_data.camera_urls(cid) == (vpn_url, local_url) - - -def test_camera_data_update_camera_urls_empty(camera_home_data): - camera_id = "12:34:56:00:f1:62" - home_id = "91763b24c43d3e344f424e8b" - camera_home_data.cameras[home_id][camera_id]["vpn_url"] = None - camera_home_data.cameras[home_id][camera_id]["local_url"] = None - - camera_home_data.update_camera_urls(camera_id) - - assert camera_home_data.camera_urls(camera_id) == (None, None) - - -def test_camera_data_camera_urls_disconnected(auth, camera_ping, requests_mock): - with open( - "fixtures/camera_home_data_disconnected.json", - encoding="utf-8", - ) as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMEDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - camera_data = pyatmo.CameraData(auth) - camera_data.update() - cid = "12:34:56:00:f1:62" - - camera_data.update_camera_urls(cid) - - assert camera_data.camera_urls(cid) == (None, None) - - -@pytest.mark.parametrize( - "home_id, expected", - [("91763b24c43d3e344f424e8b", ["Richard Doe"])], -) -def test_camera_data_persons_at_home(camera_home_data, home_id, expected): - assert camera_home_data.persons_at_home(home_id) == expected - - -@time_machine.travel(dt.datetime(2019, 6, 16)) -@pytest.mark.parametrize( - "name, cid, exclude, expected", - [ - ("John Doe", "12:34:56:00:f1:62", None, True), - ("Richard Doe", "12:34:56:00:f1:62", None, False), - ("Unknown", "12:34:56:00:f1:62", None, False), - ("John Doe", "12:34:56:00:f1:62", 1, False), - ("John Doe", "12:34:56:00:f1:62", 50000, True), - ("Jack Doe", "12:34:56:00:f1:62", None, False), - ], -) -def test_camera_data_person_seen_by_camera( - camera_home_data, - name, - cid, - exclude, - expected, -): - assert ( - camera_home_data.person_seen_by_camera(name, cid, exclude=exclude) is expected - ) - - -def test_camera_data__known_persons(camera_home_data): - known_persons = camera_home_data._known_persons("91763b24c43d3e344f424e8b") - assert len(known_persons) == 3 - assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"]["pseudo"] == "John Doe" - - -def test_camera_data_known_persons(camera_home_data): - known_persons = camera_home_data.known_persons("91763b24c43d3e344f424e8b") - assert len(known_persons) == 3 - assert known_persons["91827374-7e04-5298-83ad-a0cb8372dff1"] == "John Doe" - - -def test_camera_data_known_persons_names(camera_home_data): - assert sorted(camera_home_data.known_persons_names("91763b24c43d3e344f424e8b")) == [ - "Jane Doe", - "John Doe", - "Richard Doe", - ] - - -@time_machine.travel(dt.datetime(2019, 6, 16)) -@pytest.mark.parametrize( - "name, home_id, expected", - [ - ( - "John Doe", - "91763b24c43d3e344f424e8b", - "91827374-7e04-5298-83ad-a0cb8372dff1", - ), - ( - "Richard Doe", - "91763b24c43d3e344f424e8b", - "91827376-7e04-5298-83af-a0cb8372dff3", - ), - ("Dexter Foe", "91763b24c43d3e344f424e8b", None), - ], -) -def test_camera_data_get_person_id(camera_home_data, name, home_id, expected): - assert camera_home_data.get_person_id(name, home_id) == expected - - -@pytest.mark.parametrize( - "home_id, person_id, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "91827374-7e04-5298-83ad-a0cb8372dff1", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "91827376-7e04-5298-83af-a0cb8372dff3", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - None, - "status_ok.json", - "ok", - ), - ], -) -def test_camera_data_set_persons_away( - camera_home_data, - requests_mock, - home_id, - person_id, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - mock_req = requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETPERSONSAWAY_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert camera_home_data.set_persons_away(home_id, person_id)["status"] == expected - if person_id is not None: - assert ( - mock_req.request_history[0].text - == f"home_id={home_id}&person_id={person_id}" - ) - else: - assert mock_req.request_history[0].text == f"home_id={home_id}" - - -@pytest.mark.parametrize( - "home_id, person_ids, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - [ - "91827374-7e04-5298-83ad-a0cb8372dff1", - "91827376-7e04-5298-83af-a0cb8372dff3", - ], - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "91827376-7e04-5298-83af-a0cb8372dff3", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - None, - "status_ok.json", - "ok", - ), - ], -) -def test_camera_data_set_persons_home( - camera_home_data, - requests_mock, - home_id, - person_ids, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - mock_req = requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETPERSONSHOME_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert camera_home_data.set_persons_home(home_id, person_ids)["status"] == expected - - if isinstance(person_ids, list): - assert ( - mock_req.request_history[0].text - == f"home_id={home_id}&person_ids%5B%5D={'&person_ids%5B%5D='.join(person_ids)}" - ) - elif person_ids: - assert ( - mock_req.request_history[0].text - == f"home_id={home_id}&person_ids%5B%5D={person_ids}" - ) - else: - assert mock_req.request_history[0].text == f"home_id={home_id}" - - -@time_machine.travel(dt.datetime(2019, 6, 16)) -@pytest.mark.parametrize( - "camera_id, exclude, expected, expectation", - [ - ("12:34:56:00:f1:62", None, True, does_not_raise()), - ("12:34:56:00:f1:62", 40000, True, does_not_raise()), - ("12:34:56:00:f1:62", 5, False, does_not_raise()), - (None, None, None, pytest.raises(pyatmo.NoDevice)), - ], -) -def test_camera_data_someone_known_seen( - camera_home_data, - camera_id, - exclude, - expected, - expectation, -): - with expectation: - assert camera_home_data.someone_known_seen(camera_id, exclude) == expected - - -@time_machine.travel(dt.datetime(2019, 6, 16)) -@pytest.mark.parametrize( - "camera_id, exclude, expected, expectation", - [ - ("12:34:56:00:f1:62", None, False, does_not_raise()), - ("12:34:56:00:f1:62", 40000, True, does_not_raise()), - ("12:34:56:00:f1:62", 100, False, does_not_raise()), - (None, None, None, pytest.raises(pyatmo.NoDevice)), - ], -) -def test_camera_data_someone_unknown_seen( - camera_home_data, - camera_id, - exclude, - expected, - expectation, -): - with expectation: - assert camera_home_data.someone_unknown_seen(camera_id, exclude) == expected - - -@time_machine.travel(dt.datetime(2019, 6, 16)) -@pytest.mark.parametrize( - "camera_id, exclude, expected, expectation", - [ - ("12:34:56:00:f1:62", None, False, does_not_raise()), - ("12:34:56:00:f1:62", 140000, True, does_not_raise()), - ("12:34:56:00:f1:62", 130000, False, does_not_raise()), - (None, None, False, pytest.raises(pyatmo.NoDevice)), - ], -) -def test_camera_data_motion_detected( - camera_home_data, - camera_id, - exclude, - expected, - expectation, -): - with expectation: - assert camera_home_data.motion_detected(camera_id, exclude) == expected - - -@pytest.mark.parametrize( - "sid, expected", - [ - ("12:34:56:00:8b:a2", "Hall"), - ("12:34:56:00:8b:ac", "Kitchen"), - ("None", None), - (None, None), - ], -) -def test_camera_data_get_smokedetector(camera_home_data, sid, expected): - if smokedetector := camera_home_data.get_smokedetector(sid): - assert smokedetector["name"] == expected - else: - assert smokedetector is expected - - -@pytest.mark.parametrize( - "home_id, camera_id, floodlight, monitoring, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "12:34:56:00:f1:ff", - "on", - None, - "camera_set_state_error.json", - False, - ), - ( - "91763b24c43d3e344f424e8b", - "12:34:56:00:f1:62", - None, - "on", - "camera_set_state_ok.json", - True, - ), - (None, "12:34:56:00:f1:62", None, "on", "camera_set_state_ok.json", True), - ( - "91763b24c43d3e344f424e8b", - "12:34:56:00:f1:62", - "auto", - "on", - "camera_set_state_ok.json", - True, - ), - ( - "91763b24c43d3e344f424e8b", - "12:34:56:00:f1:62", - None, - "on", - "camera_set_state_error_already_on.json", - True, - ), - ( - "91763b24c43d3e344f424e8b", - "12:34:56:00:f1:62", - "on", - None, - "camera_set_state_error_wrong_parameter.json", - False, - ), - ], -) -def test_camera_data_set_state( - camera_home_data, - requests_mock, - home_id, - camera_id, - floodlight, - monitoring, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETSTATE_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert ( - camera_home_data.set_state( - home_id=home_id, - camera_id=camera_id, - floodlight=floodlight, - monitoring=monitoring, - ) - == expected - ) - - -def test_camera_data_get_light_state(camera_home_data): - camera_id = "12:34:56:00:a5:a4" - expected = "auto" - assert camera_home_data.get_light_state(camera_id) == expected - - -def test_camera_data_get_camera_picture(camera_home_data, requests_mock): - image_id = "5c22739723720a6e278c43bf" - key = "276751836a6d1a71447f8d975494c87bc125766a970f7e022e79e001e021d756" - with open( - "fixtures/camera_image_sample.jpg", - "rb", - ) as fixture_file: - expect = fixture_file.read() - - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETCAMERAPICTURE_ENDPOINT, - content=expect, - ) - - assert camera_home_data.get_camera_picture(image_id, key) == (expect, "jpeg") - - -def test_camera_data_get_profile_image(camera_home_data, requests_mock): - with open( - "fixtures/camera_image_sample.jpg", - "rb", - ) as fixture_file: - expect = fixture_file.read() - - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETCAMERAPICTURE_ENDPOINT, - content=expect, - ) - assert camera_home_data.get_profile_image( - "John Doe", - "91763b24c43d3e344f424e8b", - ) == (expect, "jpeg") - assert camera_home_data.get_profile_image( - "Jack Foe", - "91763b24c43d3e344f424e8b", - ) == (None, None) - - -@pytest.mark.parametrize( - "home_id, event_id, device_type, exception", - [ - ("91763b24c43d3e344f424e8b", None, None, pytest.raises(pyatmo.ApiError)), - ( - "91763b24c43d3e344f424e8b", - "a1b2c3d4e5f6abcdef123456", - None, - does_not_raise(), - ), - ("91763b24c43d3e344f424e8b", None, "NOC", does_not_raise()), - ("91763b24c43d3e344f424e8b", None, "NACamera", does_not_raise()), - ("91763b24c43d3e344f424e8b", None, "NSD", does_not_raise()), - ], -) -def test_camera_data_update_events( - camera_home_data, - requests_mock, - home_id, - event_id, - device_type, - exception, -): - with open( - "fixtures/camera_data_events_until.json", - encoding="utf-8", - ) as fixture_file: - json_fixture = json.load(fixture_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETEVENTSUNTIL_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with exception: - assert ( - camera_home_data.update_events( - home_id=home_id, - event_id=event_id, - device_type=device_type, - ) - is None - ) - - -def test_camera_data_outdoor_motion_detected(camera_home_data): - camera_id = "12:34:56:00:a5:a4" - assert camera_home_data.outdoor_motion_detected(camera_id) is False - assert camera_home_data.outdoor_motion_detected(camera_id, 100) is False - - -def test_camera_data_human_detected(camera_home_data): - camera_id = "12:34:56:00:a5:a4" - assert camera_home_data.human_detected(camera_id) is False - assert camera_home_data.human_detected(camera_id, 100) is False - - -def test_camera_data_animal_detected(camera_home_data): - camera_id = "12:34:56:00:a5:a4" - assert camera_home_data.animal_detected(camera_id) is False - assert camera_home_data.animal_detected(camera_id, 100) is False - - -def test_camera_data_car_detected(camera_home_data): - camera_id = "12:34:56:00:a5:a4" - assert camera_home_data.car_detected(camera_id) is False - assert camera_home_data.car_detected(camera_id, 100) is False - - -def test_camera_data_module_motion_detected(camera_home_data): - camera_id = "12:34:56:00:f1:62" - module_id = "12:34:56:00:f2:f1" - assert camera_home_data.module_motion_detected(camera_id, module_id) is False - assert camera_home_data.module_motion_detected(camera_id, module_id, 100) is False - - -def test_camera_data_module_opened(camera_home_data): - camera_id = "12:34:56:00:f1:62" - module_id = "12:34:56:00:f2:f1" - assert camera_home_data.module_opened(camera_id, module_id) is False - assert camera_home_data.module_opened(camera_id, module_id, 100) is False diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py deleted file mode 100644 index 2fa475e1..00000000 --- a/tests/test_pyatmo_homecoach.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Define tests for HomeCoach module.""" -# pylint: disable=protected-access -import json - -import pyatmo -import pytest - - -def test_home_coach_data(home_coach_data): - assert home_coach_data.stations["12:34:56:26:69:0c"]["station_name"] == "Bedroom" - - -@pytest.mark.parametrize( - "station_id, expected", - [ - ("12:34:56:26:69:0c", ["Bedroom"]), - pytest.param( - "NoValidStation", - None, - marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet.", - ), - ), - ], -) -def test_home_coach_data_get_module_names(home_coach_data, station_id, expected): - assert sorted(home_coach_data.get_module_names(station_id)) == expected - - -@pytest.mark.parametrize( - "station_id, expected", - [ - (None, {}), - ( - "12:34:56:26:69:0c", - { - "12:34:56:26:69:0c": { - "station_name": "Bedroom", - "module_name": "Bedroom", - "id": "12:34:56:26:69:0c", - }, - }, - ), - pytest.param( - "NoValidStation", - None, - marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet.", - ), - ), - ], -) -def test_home_coach_data_get_modules(home_coach_data, station_id, expected): - assert home_coach_data.get_modules(station_id) == expected - - -def test_home_coach_data_no_devices(auth, requests_mock): - with open("fixtures/home_coach_no_devices.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMECOACHDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - hcd = pyatmo.home_coach.HomeCoachData(auth) - hcd.update() diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py deleted file mode 100644 index c844e8ec..00000000 --- a/tests/test_pyatmo_publicdata.py +++ /dev/null @@ -1,314 +0,0 @@ -"""Define tests for Public weather module.""" -# pylint: disable=protected-access -import json - -import pyatmo -import pytest - -LON_NE = "6.221652" -LAT_NE = "46.610870" -LON_SW = "6.217828" -LAT_SW = "46.596485" - - -def test_public_data(auth, requests_mock): - with open("fixtures/public_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETPUBLIC_DATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - - public_data = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - public_data.update() - assert public_data.status == "ok" - - public_data = pyatmo.PublicData( - auth, - LAT_NE, - LON_NE, - LAT_SW, - LON_SW, - required_data_type="temperature,rain_live", - ) - public_data.update() - assert public_data.status == "ok" - - -def test_public_data_unavailable(auth, requests_mock): - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETPUBLIC_DATA_ENDPOINT, - status_code=404, - ) - with pytest.raises(pyatmo.ApiError): - public_data = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - public_data.update() - - -def test_public_data_error(auth, requests_mock): - with open("fixtures/public_data_error_mongo.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETPUBLIC_DATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - public_data = pyatmo.PublicData(auth, LAT_NE, LON_NE, LAT_SW, LON_SW) - public_data.update() - - -def test_public_data_stations_in_area(public_data): - assert public_data.stations_in_area() == 8 - - -def test_public_data_get_latest_rain(public_data): - expected = { - "70:ee:50:1f:68:9e": 0, - "70:ee:50:27:25:b0": 0, - "70:ee:50:36:94:7c": 0.5, - "70:ee:50:36:a9:fc": 0, - } - assert public_data.get_latest_rain() == expected - - -def test_public_data_get_average_rain(public_data): - assert public_data.get_average_rain() == 0.125 - - -def test_public_data_get_60_min_rain(public_data): - expected = { - "70:ee:50:1f:68:9e": 0, - "70:ee:50:27:25:b0": 0, - "70:ee:50:36:94:7c": 0.2, - "70:ee:50:36:a9:fc": 0, - } - assert public_data.get_60_min_rain() == expected - - -def test_public_data_get_average_60_min_rain(public_data): - assert public_data.get_average_60_min_rain() == 0.05 - - -def test_public_data_get_24_h_rain(public_data): - expected = { - "70:ee:50:1f:68:9e": 9.999, - "70:ee:50:27:25:b0": 11.716000000000001, - "70:ee:50:36:94:7c": 12.322000000000001, - "70:ee:50:36:a9:fc": 11.009, - } - assert public_data.get_24_h_rain() == expected - - -def test_public_data_get_average_24_h_rain(public_data): - assert public_data.get_average_24_h_rain() == 11.261500000000002 - - -def test_public_data_get_latest_pressures(public_data): - expected = { - "70:ee:50:1f:68:9e": 1007.3, - "70:ee:50:27:25:b0": 1012.8, - "70:ee:50:36:94:7c": 1010.6, - "70:ee:50:36:a9:fc": 1010, - "70:ee:50:01:20:fa": 1014.4, - "70:ee:50:04:ed:7a": 1005.4, - "70:ee:50:27:9f:2c": 1010.6, - "70:ee:50:3c:02:78": 1011.7, - } - assert public_data.get_latest_pressures() == expected - - -def test_public_data_get_average_pressure(public_data): - assert public_data.get_average_pressure() == 1010.3499999999999 - - -def test_public_data_get_latest_temperatures(public_data): - expected = { - "70:ee:50:1f:68:9e": 21.1, - "70:ee:50:27:25:b0": 23.2, - "70:ee:50:36:94:7c": 21.4, - "70:ee:50:36:a9:fc": 20.1, - "70:ee:50:01:20:fa": 27.4, - "70:ee:50:04:ed:7a": 19.8, - "70:ee:50:27:9f:2c": 25.5, - "70:ee:50:3c:02:78": 23.3, - } - assert public_data.get_latest_temperatures() == expected - - -def test_public_data_get_average_temperature(public_data): - assert public_data.get_average_temperature() == 22.725 - - -def test_public_data_get_latest_humidities(public_data): - expected = { - "70:ee:50:1f:68:9e": 69, - "70:ee:50:27:25:b0": 60, - "70:ee:50:36:94:7c": 62, - "70:ee:50:36:a9:fc": 67, - "70:ee:50:01:20:fa": 58, - "70:ee:50:04:ed:7a": 76, - "70:ee:50:27:9f:2c": 56, - "70:ee:50:3c:02:78": 58, - } - assert public_data.get_latest_humidities() == expected - - -def test_public_data_get_average_humidity(public_data): - assert public_data.get_average_humidity() == 63.25 - - -def test_public_data_get_latest_wind_strengths(public_data): - expected = {"70:ee:50:36:a9:fc": 15} - assert public_data.get_latest_wind_strengths() == expected - - -def test_public_data_get_average_wind_strength(public_data): - assert public_data.get_average_wind_strength() == 15 - - -def test_public_data_get_latest_wind_angles(public_data): - expected = {"70:ee:50:36:a9:fc": 17} - assert public_data.get_latest_wind_angles() == expected - - -def test_public_data_get_latest_gust_strengths(public_data): - expected = {"70:ee:50:36:a9:fc": 31} - assert public_data.get_latest_gust_strengths() == expected - - -def test_public_data_get_average_gust_strength(public_data): - assert public_data.get_average_gust_strength() == 31 - - -def test_public_data_get_latest_gust_angles(public_data): - expected = {"70:ee:50:36:a9:fc": 217} - assert public_data.get_latest_gust_angles() == expected - - -def test_public_data_get_locations(public_data): - expected = { - "70:ee:50:1f:68:9e": [8.795445200000017, 50.2130169], - "70:ee:50:27:25:b0": [8.7807159, 50.1946167], - "70:ee:50:36:94:7c": [8.791382999999996, 50.2136394], - "70:ee:50:36:a9:fc": [8.801164269110814, 50.19596181704958], - "70:ee:50:01:20:fa": [8.7953, 50.195241], - "70:ee:50:04:ed:7a": [8.785034, 50.192169], - "70:ee:50:27:9f:2c": [8.785342, 50.193573], - "70:ee:50:3c:02:78": [8.795953681700666, 50.19530139868166], - } - assert public_data.get_locations() == expected - - -def test_public_data_get_time_for_rain_measures(public_data): - expected = { - "70:ee:50:36:a9:fc": 1560248184, - "70:ee:50:1f:68:9e": 1560248344, - "70:ee:50:27:25:b0": 1560247896, - "70:ee:50:36:94:7c": 1560248022, - } - assert public_data.get_time_for_rain_measures() == expected - - -def test_public_data_get_time_for_wind_measures(public_data): - expected = {"70:ee:50:36:a9:fc": 1560248190} - assert public_data.get_time_for_wind_measures() == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ( - "pressure", - { - "70:ee:50:01:20:fa": 1014.4, - "70:ee:50:04:ed:7a": 1005.4, - "70:ee:50:1f:68:9e": 1007.3, - "70:ee:50:27:25:b0": 1012.8, - "70:ee:50:27:9f:2c": 1010.6, - "70:ee:50:36:94:7c": 1010.6, - "70:ee:50:36:a9:fc": 1010, - "70:ee:50:3c:02:78": 1011.7, - }, - ), - ( - "temperature", - { - "70:ee:50:01:20:fa": 27.4, - "70:ee:50:04:ed:7a": 19.8, - "70:ee:50:1f:68:9e": 21.1, - "70:ee:50:27:25:b0": 23.2, - "70:ee:50:27:9f:2c": 25.5, - "70:ee:50:36:94:7c": 21.4, - "70:ee:50:36:a9:fc": 20.1, - "70:ee:50:3c:02:78": 23.3, - }, - ), - ( - "humidity", - { - "70:ee:50:01:20:fa": 58, - "70:ee:50:04:ed:7a": 76, - "70:ee:50:1f:68:9e": 69, - "70:ee:50:27:25:b0": 60, - "70:ee:50:27:9f:2c": 56, - "70:ee:50:36:94:7c": 62, - "70:ee:50:36:a9:fc": 67, - "70:ee:50:3c:02:78": 58, - }, - ), - ], -) -def test_public_data_get_latest_station_measures(public_data, test_input, expected): - assert public_data.get_latest_station_measures(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ("wind_strength", {"70:ee:50:36:a9:fc": 15}), - ("wind_angle", {"70:ee:50:36:a9:fc": 17}), - ("gust_strength", {"70:ee:50:36:a9:fc": 31}), - ("gust_angle", {"70:ee:50:36:a9:fc": 217}), - ("wind_timeutc", {"70:ee:50:36:a9:fc": 1560248190}), - ], -) -def test_public_data_get_accessory_data(public_data, test_input, expected): - assert public_data.get_accessory_data(test_input) == expected - - -@pytest.mark.parametrize( - "test_input,expected", - [ - ( - { - "70:ee:50:01:20:fa": 1014.4, - "70:ee:50:04:ed:7a": 1005.4, - "70:ee:50:1f:68:9e": 1007.3, - "70:ee:50:27:25:b0": 1012.8, - "70:ee:50:27:9f:2c": 1010.6, - "70:ee:50:36:94:7c": 1010.6, - "70:ee:50:36:a9:fc": 1010, - "70:ee:50:3c:02:78": 1011.7, - }, - 1010.35, - ), - ( - { - "70:ee:50:01:20:fa": 27.4, - "70:ee:50:04:ed:7a": 19.8, - "70:ee:50:1f:68:9e": 21.1, - "70:ee:50:27:25:b0": 23.2, - "70:ee:50:27:9f:2c": 25.5, - "70:ee:50:36:94:7c": 21.4, - "70:ee:50:36:a9:fc": 20.1, - "70:ee:50:3c:02:78": 23.3, - }, - 22.725, - ), - ({}, 0), - ], -) -def test_public_data_average(test_input, expected): - assert pyatmo.public_data.average(test_input) == expected diff --git a/tests/test_pyatmo_refactor.py b/tests/test_pyatmo_refactor.py index 956f5cb7..2231b89e 100644 --- a/tests/test_pyatmo_refactor.py +++ b/tests/test_pyatmo_refactor.py @@ -11,8 +11,8 @@ import pytest import time_machine -from tests.common import fake_post_request -from tests.conftest import MockResponse, does_not_raise +from tests.common import MockResponse, fake_post_request +from tests.conftest import does_not_raise # pylint: disable=F6401 diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py deleted file mode 100644 index 4d9e2651..00000000 --- a/tests/test_pyatmo_thermostat.py +++ /dev/null @@ -1,590 +0,0 @@ -"""Define tests for Thermostat module.""" -# pylint: disable=protected-access -import json - -import pyatmo -import pytest - -from tests.conftest import does_not_raise - - -def test_home_data(home_data): - expected = { - "12:34:56:00:fa:d0": { - "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", - ], - }, - "12:34:56:00:01:ae": { - "id": "12:34:56:00:01:ae", - "type": "NATherm1", - "name": "Livingroom", - "setup_date": 1494963356, - "room_id": "2746182631", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:03:a5:54": { - "id": "12:34:56:03:a5:54", - "type": "NRV", - "name": "Valve1", - "setup_date": 1554549767, - "room_id": "2833524037", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:03:a0:ac": { - "id": "12:34:56:03:a0:ac", - "type": "NRV", - "name": "Valve2", - "setup_date": 1554554444, - "room_id": "2940411577", - "bridge": "12:34:56:00:fa:d0", - }, - "12:34:56:00:f1:62": { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "name": "Hall", - "setup_date": 1544828430, - "room_id": "3688132631", - }, - } - assert home_data.modules["91763b24c43d3e344f424e8b"] == expected - - -def test_home_data_no_data(auth, requests_mock): - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json={}, - headers={"content-type": "application/json"}, - ) - home_data = pyatmo.HomeData(auth) - with pytest.raises(pyatmo.NoDevice): - home_data.update() - - -def test_home_data_no_body(auth, requests_mock): - with open("fixtures/home_data_empty.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_data = pyatmo.HomeData(auth) - with pytest.raises(pyatmo.NoDevice): - home_data.update() - - -def test_home_data_no_homes(auth, requests_mock): - with open("fixtures/home_data_no_homes.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_data = pyatmo.HomeData(auth) - with pytest.raises(pyatmo.NoDevice): - home_data.update() - - -def test_home_data_no_home_name(auth, requests_mock): - with open("fixtures/home_data_nohomename.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_data = pyatmo.HomeData(auth) - home_data.update() - home_id = "91763b24c43d3e344f424e8b" - assert home_data.homes[home_id]["name"] == "Unknown" - - -@pytest.mark.parametrize( - "home_id, expected", - [("91763b24c43d3e344f424e8b", "MYHOME"), ("91763b24c43d3e344f424e8c", "Unknown")], -) -def test_home_data_homes_by_id(home_data, home_id, expected): - assert home_data.homes[home_id]["name"] == expected - - -def test_home_data_get_selected_schedule(home_data): - assert ( - home_data._get_selected_schedule("91763b24c43d3e344f424e8b")["name"] - == "Default" - ) - assert home_data._get_selected_schedule("Unknown") == {} - - -@pytest.mark.parametrize( - "t_home_id, t_sched_id, expected", - [ - ("91763b24c43d3e344f424e8b", "591b54a2764ff4d50d8b5795", does_not_raise()), - ( - "91763b24c43d3e344f424e8b", - "123456789abcdefg12345678", - pytest.raises(pyatmo.NoSchedule), - ), - ], -) -def test_home_data_switch_home_schedule( - home_data, - requests_mock, - t_home_id, - t_sched_id, - expected, -): - with open("fixtures/status_ok.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SWITCHHOMESCHEDULE_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with expected: - home_data.switch_home_schedule(home_id=t_home_id, schedule_id=t_sched_id) - - -@pytest.mark.parametrize( - "home_id, expected", - [("91763b24c43d3e344f424e8b", 14), ("00000000000000000000000", None)], -) -def test_home_data_get_away_temp(home_data, home_id, expected): - assert home_data.get_away_temp(home_id) == expected - - -@pytest.mark.parametrize( - "home_id, expected", - [("91763b24c43d3e344f424e8b", 7), ("00000000000000000000000", None)], -) -def test_home_data_get_hg_temp(home_data, home_id, expected): - assert home_data.get_hg_temp(home_id) == expected - - -@pytest.mark.parametrize( - "home_id, module_id, expected", - [ - ("91763b24c43d3e344f424e8b", "2746182631", "NATherm1"), - ("91763b24c43d3e344f424e8b", "2833524037", "NRV"), - ("91763b24c43d3e344f424e8b", "0000000000", None), - ], -) -def test_home_data_thermostat_type(home_data, home_id, module_id, expected): - assert home_data.get_thermostat_type(home_id, module_id) == expected - - -@pytest.mark.parametrize( - "home_id, room_id, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "2746182631", - { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - }, - ), - ], -) -def test_home_status(home_status, room_id, expected): - assert len(home_status.rooms) == 3 - assert home_status.rooms[room_id] == expected - - -def test_home_status_error_and_data(auth, requests_mock): - with open( - "fixtures/home_status_error_and_data.json", - encoding="utf-8", - ) as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESTATUS_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - home_status = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") - home_status.update() - assert len(home_status.rooms) == 3 - - expexted = { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - } - assert home_status.rooms["2746182631"] == expexted - - -def test_home_status_error(auth, requests_mock): - with open("fixtures/home_status_empty.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESTATUS_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with open("fixtures/home_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - home_status = pyatmo.HomeStatus(auth, home_id="91763b24c43d3e344f424e8b") - home_status.update() - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_get_room(home_status): - expexted = { - "id": "2746182631", - "reachable": True, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0, - } - assert home_status.get_room("2746182631") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.get_room("0000000000") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_get_thermostat(home_status): - expexted = { - "id": "12:34:56:00:01:ae", - "reachable": True, - "type": "NATherm1", - "firmware_revision": 65, - "rf_strength": 58, - "battery_level": 3780, - "boiler_valve_comfort_boost": False, - "boiler_status": True, - "anticipating": False, - "bridge": "12:34:56:00:fa:d0", - "battery_state": "high", - } - assert home_status.get_thermostat("12:34:56:00:01:ae") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.get_thermostat("00:00:00:00:00:00") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_get_relay(home_status): - expexted = { - "id": "12:34:56:00:fa:d0", - "type": "NAPlug", - "firmware_revision": 174, - "rf_strength": 107, - "wifi_strength": 42, - } - assert home_status.get_relay("12:34:56:00:fa:d0") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.get_relay("00:00:00:00:00:00") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_get_valve(home_status): - expexted = { - "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", - } - assert home_status.get_valve("12:34:56:03:a5:54") == expexted - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.get_valve("00:00:00:00:00:00") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_set_point(home_status): - assert home_status.set_point("2746182631") == 12 - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.set_point("0000000000") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_set_point_mode(home_status): - assert home_status.set_point_mode("2746182631") == "away" - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.set_point_mode("0000000000") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_measured_temperature(home_status): - assert home_status.measured_temperature("2746182631") == 19.8 - with pytest.raises(pyatmo.InvalidRoom): - assert home_status.measured_temperature("0000000000") - - -@pytest.mark.parametrize("home_id", ["91763b24c43d3e344f424e8b"]) -def test_home_status_boiler_status(home_status): - assert home_status.boiler_status("12:34:56:00:01:ae") is True - - -@pytest.mark.parametrize( - "home_id, mode, end_time, schedule_id, json_fixture, expected", - [ - ( - None, - None, - None, - None, - "home_status_error_mode_is_missing.json", - "mode is missing", - ), - ( - "91763b24c43d3e344f424e8b", - None, - None, - None, - "home_status_error_mode_is_missing.json", - "mode is missing", - ), - ( - "invalidID", - "away", - None, - None, - "home_status_error_invalid_id.json", - "Invalid id", - ), - ("91763b24c43d3e344f424e8b", "away", None, None, "status_ok.json", "ok"), - ("91763b24c43d3e344f424e8b", "away", 1559162650, None, "status_ok.json", "ok"), - ( - "91763b24c43d3e344f424e8b", - "away", - 1559162650, - 0000000, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - None, - "591b54a2764ff4d50d8b5795", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - 1559162650, - "591b54a2764ff4d50d8b5795", - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "schedule", - None, - "blahblahblah", - "home_status_error_invalid_schedule_id.json", - "schedule is not therm schedule", - ), - ], -) -def test_home_status_set_thermmode( - home_status, - requests_mock, - mode, - end_time, - schedule_id, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETTHERMMODE_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - res = home_status.set_thermmode( - mode=mode, - end_time=end_time, - schedule_id=schedule_id, - ) - if "error" in res: - assert expected in res["error"]["message"] - else: - assert expected in res["status"] - - -@pytest.mark.parametrize( - "home_id, room_id, mode, temp, end_time, json_fixture, expected", - [ - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - 14, - None, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - 14, - 1559162650, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - None, - None, - "status_ok.json", - "ok", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - None, - 1559162650, - "status_ok.json", - "ok", - ), - ], -) -def test_home_status_set_room_thermpoint( - home_status, - requests_mock, - room_id, - mode, - temp, - end_time, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETROOMTHERMPOINT_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert ( - home_status.set_room_thermpoint( - room_id=room_id, - mode=mode, - temp=temp, - end_time=end_time, - )["status"] - == expected - ) - - -@pytest.mark.parametrize( - "home_id, room_id, mode, temp, json_fixture, expected", - [ - ( - None, - None, - None, - None, - "home_status_error_missing_home_id.json", - "Missing home_id", - ), - ( - None, - None, - "home", - None, - "home_status_error_missing_home_id.json", - "Missing home_id", - ), - ( - "91763b24c43d3e344f424e8b", - None, - "home", - None, - "home_status_error_missing_parameters.json", - "Missing parameters", - ), - ( - "91763b24c43d3e344f424e8b", - "2746182631", - "home", - None, - "home_status_error_missing_parameters.json", - "Missing parameters", - ), - ], -) -def test_home_status_set_room_thermpoint_error( - home_status, - requests_mock, - room_id, - mode, - temp, - json_fixture, - expected, -): - with open(f"fixtures/{json_fixture}", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.SETROOMTHERMPOINT_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert ( - home_status.set_room_thermpoint(room_id=room_id, mode=mode, temp=temp)["error"][ - "message" - ] - == expected - ) - - -def test_home_status_error_disconnected( - auth, - requests_mock, - home_id="91763b24c43d3e344f424e8b", -): - with open( - "fixtures/home_status_error_disconnected.json", - encoding="utf-8", - ) as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETHOMESTATUS_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with open("fixtures/home_data_simple.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.GETHOMESDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - home_status = pyatmo.HomeStatus(auth, home_id) - home_status.update() diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py deleted file mode 100644 index 5019a126..00000000 --- a/tests/test_pyatmo_weatherstation.py +++ /dev/null @@ -1,496 +0,0 @@ -"""Define tests for WeatherStation module.""" -# pylint: disable=protected-access -import datetime as dt -import json - -import pyatmo -import pytest -import time_machine - - -def test_weather_station_data(weather_station_data): - assert ( - weather_station_data.stations["12:34:56:37:11:ca"]["station_name"] - == "MyStation" - ) - - -def test_weather_station_data_no_response(auth, requests_mock): - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETSTATIONDATA_ENDPOINT, - json={}, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - wsd = pyatmo.WeatherStationData(auth) - wsd.update() - - -def test_weather_station_data_no_body(auth, requests_mock): - with open("fixtures/status_ok.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETSTATIONDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - wsd = pyatmo.WeatherStationData(auth) - wsd.update() - - -def test_weather_station_data_no_data(auth, requests_mock): - with open("fixtures/home_data_empty.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETSTATIONDATA_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - with pytest.raises(pyatmo.NoDevice): - wsd = pyatmo.WeatherStationData(auth) - wsd.update() - - -@pytest.mark.parametrize( - "station_id, expected", - [ - ( - "12:34:56:37:11:ca", - [ - "Garden", - "Kitchen", - "Livingroom", - "NetatmoIndoor", - "NetatmoOutdoor", - "Yard", - ], - ), - ("12:34:56:36:fd:3c", ["Module", "NAMain", "Rain Gauge"]), - pytest.param( - "NoValidStation", - None, - marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet.", - ), - ), - ], -) -def test_weather_station_get_module_names(weather_station_data, station_id, expected): - assert sorted(weather_station_data.get_module_names(station_id)) == expected - - -@pytest.mark.parametrize( - "station_id, expected", - [ - (None, {}), - ( - "12:34:56:37:11:ca", - { - "12:34:56:03:1b:e4": { - "id": "12:34:56:03:1b:e4", - "module_name": "Garden", - "station_name": "MyStation", - }, - "12:34:56:05:51:20": { - "id": "12:34:56:05:51:20", - "module_name": "Yard", - "station_name": "MyStation", - }, - "12:34:56:07:bb:0e": { - "id": "12:34:56:07:bb:0e", - "module_name": "Livingroom", - "station_name": "MyStation", - }, - "12:34:56:07:bb:3e": { - "id": "12:34:56:07:bb:3e", - "module_name": "Kitchen", - "station_name": "MyStation", - }, - "12:34:56:36:fc:de": { - "id": "12:34:56:36:fc:de", - "module_name": "NetatmoOutdoor", - "station_name": "MyStation", - }, - "12:34:56:37:11:ca": { - "id": "12:34:56:37:11:ca", - "module_name": "NetatmoIndoor", - "station_name": "MyStation", - }, - }, - ), - ( - "12:34:56:1d:68:2e", - { - "12:34:56:1d:68:2e": { - "id": "12:34:56:1d:68:2e", - "module_name": "Basisstation", - "station_name": "NAMain", - }, - }, - ), - ( - "12:34:56:58:c8:54", - { - "12:34:56:58:c8:54": { - "id": "12:34:56:58:c8:54", - "module_name": "NAMain", - "station_name": "Njurunda (Indoor)", - }, - "12:34:56:58:e6:38": { - "id": "12:34:56:58:e6:38", - "module_name": "NAModule1", - "station_name": "Njurunda (Indoor)", - }, - }, - ), - pytest.param( - "NoValidStation", - None, - marks=pytest.mark.xfail( - reason="Invalid station names are not handled yet.", - ), - ), - ], -) -def test_weather_station_get_modules(weather_station_data, station_id, expected): - assert weather_station_data.get_modules(station_id) == expected - - -def test_weather_station_get_station(weather_station_data): - result = weather_station_data.get_station("12:34:56:37:11:ca") - - assert result["_id"] == "12:34:56:37:11:ca" - assert result["station_name"] == "MyStation" - assert result["module_name"] == "NetatmoIndoor" - assert result["type"] == "NAMain" - assert result["data_type"] == [ - "Temperature", - "CO2", - "Humidity", - "Noise", - "Pressure", - ] - - assert weather_station_data.get_station("NoValidStation") == {} - - -@pytest.mark.parametrize( - "mid, expected", - [ - ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"), - ("12:34:56:07:bb:3e", "12:34:56:07:bb:3e"), - ("", {}), - (None, {}), - ], -) -def test_weather_station_get_module(weather_station_data, mid, expected): - mod = weather_station_data.get_module(mid) - - assert isinstance(mod, dict) - assert mod.get("_id", mod) == expected - - -@pytest.mark.parametrize( - "module_id, expected", - [ - ( - "12:34:56:07:bb:3e", - [ - "CO2", - "Humidity", - "Temperature", - "battery_percent", - "battery_vp", - "reachable", - "rf_status", - "temp_trend", - ], - ), - ( - "12:34:56:07:bb:3e", - [ - "CO2", - "Humidity", - "Temperature", - "battery_percent", - "battery_vp", - "reachable", - "rf_status", - "temp_trend", - ], - ), - ( - "12:34:56:03:1b:e4", - [ - "GustAngle", - "GustStrength", - "WindAngle", - "WindStrength", - "battery_percent", - "battery_vp", - "reachable", - "rf_status", - ], - ), - ( - "12:34:56:05:51:20", - [ - "Rain", - "battery_percent", - "battery_vp", - "reachable", - "rf_status", - "sum_rain_1", - "sum_rain_24", - ], - ), - ( - "12:34:56:37:11:ca", - [ - "CO2", - "Humidity", - "Noise", - "Pressure", - "Temperature", - "pressure_trend", - "reachable", - "temp_trend", - "wifi_status", - ], - ), - ( - "12:34:56:58:c8:54", - [ - "CO2", - "Humidity", - "Noise", - "Pressure", - "Temperature", - "pressure_trend", - "reachable", - "temp_trend", - "wifi_status", - ], - ), - ( - "12:34:56:58:e6:38", - [ - "Humidity", - "Temperature", - "battery_percent", - "battery_vp", - "reachable", - "rf_status", - "temp_trend", - ], - ), - pytest.param( - None, - None, - marks=pytest.mark.xfail(reason="Invalid module names are not handled yet."), - ), - ], -) -def test_weather_station_get_monitored_conditions( - weather_station_data, - module_id, - expected, -): - assert sorted(weather_station_data.get_monitored_conditions(module_id)) == expected - - -@time_machine.travel(dt.datetime(2019, 6, 11)) -@pytest.mark.parametrize( - "station_id, exclude, expected", - [ - ("12:34:56:05:51:20", None, {}), - ( - "12:34:56:37:11:ca", - None, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ("", None, {}), - ("NoValidStation", None, {}), - ( - "12:34:56:37:11:ca", - 1000000, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ( - "12:34:56:37:11:ca", - 798103, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ], -) -def test_weather_station_get_last_data( - weather_station_data, - station_id, - exclude, - expected, -): - if mod := weather_station_data.get_last_data(station_id, exclude=exclude): - assert sorted(mod) == expected - else: - assert mod == expected - - -@time_machine.travel(dt.datetime(2019, 6, 11)) -@pytest.mark.parametrize( - "station_id, delay, expected", - [ - ( - "12:34:56:37:11:ca", - 3600, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ("12:34:56:37:11:ca", 798500, []), - pytest.param( - "NoValidStation", - 3600, - None, - marks=pytest.mark.xfail(reason="Invalid station name not handled yet"), - ), - ], -) -def test_weather_station_check_not_updated( - weather_station_data, - station_id, - delay, - expected, -): - mod = weather_station_data.check_not_updated(station_id, delay) - assert sorted(mod) == expected - - -@time_machine.travel(dt.datetime(2019, 6, 11)) -@pytest.mark.parametrize( - "station_id, delay, expected", - [ - ( - "12:34:56:37:11:ca", - 798500, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - ("12:34:56:37:11:ca", 100, []), - ], -) -def test_weather_station_check_updated( - weather_station_data, - station_id, - delay, - expected, -): - if mod := weather_station_data.check_updated(station_id, delay): - assert sorted(mod) == expected - else: - assert mod == expected - - -@time_machine.travel(dt.datetime(2019, 6, 11)) -@pytest.mark.parametrize( - "device_id, scale, module_type, expected", - [("MyStation", "scale", "type", [28.1])], -) -def test_weather_station_get_data( - weather_station_data, - requests_mock, - device_id, - scale, - module_type, - expected, -): - with open("fixtures/weatherstation_measure.json", encoding="utf-8") as json_file: - json_fixture = json.load(json_file) - requests_mock.post( - pyatmo.const.DEFAULT_BASE_URL + pyatmo.const.GETMEASURE_ENDPOINT, - json=json_fixture, - headers={"content-type": "application/json"}, - ) - assert ( - weather_station_data.get_data(device_id, scale, module_type)["body"][ - "1544558433" - ] - == expected - ) - - -def test_weather_station_get_last_data_measurements(weather_station_data): - station_id = "12:34:56:37:11:ca" - module_id = "12:34:56:03:1b:e4" - - mod = weather_station_data.get_last_data(station_id, None) - - assert mod[station_id]["Temperature"] == 24.6 - assert mod[station_id]["Pressure"] == 1017.3 - assert mod[module_id]["WindAngle"] == 217 - assert mod[module_id]["WindStrength"] == 4 - assert mod[module_id]["GustAngle"] == 206 - assert mod[module_id]["GustStrength"] == 9 - - -@time_machine.travel(dt.datetime(2019, 6, 11)) -@pytest.mark.parametrize( - "station_id, exclude, expected", - [ - ( - "12:34:56:37:11:ca", - None, - [ - "12:34:56:03:1b:e4", - "12:34:56:05:51:20", - "12:34:56:07:bb:0e", - "12:34:56:07:bb:3e", - "12:34:56:36:fc:de", - "12:34:56:37:11:ca", - ], - ), - (None, None, {}), - ("12:34:56:00:aa:01", None, {}), - ], -) -def test_weather_station_get_last_data_bug_97( - weather_station_data, - station_id, - exclude, - expected, -): - if mod := weather_station_data.get_last_data(station_id, exclude): - assert sorted(mod) == expected - else: - assert mod == expected