"""Asynchronous Python client for the P1 Monitor API.""" from __future__ import annotations import asyncio import socket from dataclasses import dataclass from importlib import metadata from typing import Any, Self from aiohttp import ClientError, ClientSession from aiohttp.hdrs import METH_GET from yarl import URL from .exceptions import P1MonitorConnectionError, P1MonitorError, P1MonitorNoDataError from .models import Phases, Settings, SmartMeter, WaterMeter VERSION = metadata.version(__package__) @dataclass class P1Monitor: """Main class for handling connections with the P1 Monitor API.""" host: str port: int = 80 request_timeout: float = 10.0 session: ClientSession | None = None _close_session: bool = False async def _request( self, uri: str, *, method: str = METH_GET, params: dict[str, Any] | None = None, ) -> Any: """Handle a request to a P1 Monitor device. Args: ---- uri: Request URI, without '/api/', for example, 'status' method: HTTP Method to use. params: Extra options to improve or limit the response. Returns: ------- A Python dictionary (JSON decoded) with the response from the P1 Monitor API. Raises: ------ P1MonitorConnectionError: An error occurred while communicating with the P1 Monitor. P1MonitorError: Received an unexpected response from the P1 Monitor API. """ url = URL.build( scheme="http", host=self.host, port=int(self.port), path="/api/", ).join(URL(uri)) headers = { "User-Agent": f"PythonP1Monitor/{VERSION}", "Accept": "application/json, text/plain, */*", } if self.session is None: self.session = ClientSession() self._close_session = True try: async with asyncio.timeout(self.request_timeout): response = await self.session.request( method, url, params=params, headers=headers, ) response.raise_for_status() except TimeoutError as exception: msg = "Timeout occurred while connecting to P1 Monitor device" raise P1MonitorConnectionError( msg, ) from exception except (ClientError, socket.gaierror) as exception: if "watermeter" in uri and response.status == 404: msg = "No water meter is connected to P1 Monitor device" raise P1MonitorConnectionError(msg) from exception msg = "Error occurred while communicating with P1 Monitor device" raise P1MonitorConnectionError(msg) from exception content_type = response.headers.get("Content-Type", "") if "application/json" not in content_type: text = await response.text() msg = "Unexpected response from the P1 Monitor device" raise P1MonitorError( msg, {"Content-Type": content_type, "response": text}, ) return await response.json() async def smartmeter(self) -> SmartMeter: """Get the latest values from you smart meter. Returns ------- A SmartMeter data object from the P1 Monitor API. """ data = await self._request( "v1/smartmeter", params={"json": "object", "limit": 1}, ) return SmartMeter.from_dict(data) async def settings(self) -> Settings: """Receive the set price values for energy and gas. Returns ------- A Settings data object from the P1 Monitor API. """ data = await self._request("v1/configuration", params={"json": "object"}) return Settings.from_dict(data) async def phases(self) -> Phases: """Receive data from all phases on your smart meter. Returns ------- A Phases data object from the P1 Monitor API. """ data = await self._request("v1/status", params={"json": "object"}) return Phases.from_dict(data) async def watermeter(self) -> WaterMeter: """Get the latest values from you water meter. Returns ------- A WaterMeter data object from the P1 Monitor API. Raises ------ P1MonitorNoDataError: No data was received from the P1 Monitor API. """ data = await self._request( "v2/watermeter/day", params={"json": "object", "limit": 1}, ) if data == []: msg = "No data received from P1 Monitor" raise P1MonitorNoDataError(msg) return WaterMeter.from_dict(data) async def close(self) -> None: """Close open client session.""" if self.session and self._close_session: await self.session.close() async def __aenter__(self) -> Self: """Async enter. Returns ------- The P1 Monitor object. """ return self async def __aexit__(self, *_exc_info: object) -> None: """Async exit. Args: ---- _exc_info: Exec type. """ await self.close()