1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
|
"""Common code for Shelly library."""
from __future__ import annotations
import asyncio
import ipaddress
import logging
from dataclasses import dataclass
from socket import gethostbyname
from typing import Any
from aiohttp import BasicAuth, ClientSession, ClientTimeout
from yarl import URL
from .const import CONNECT_ERRORS, DEFAULT_HTTP_PORT, DEVICE_IO_TIMEOUT
from .exceptions import (
DeviceConnectionError,
MacAddressMismatchError,
)
_LOGGER = logging.getLogger(__name__)
DEVICE_IO_TIMEOUT_CLIENT_TIMEOUT = ClientTimeout(total=DEVICE_IO_TIMEOUT)
@dataclass
class ConnectionOptions:
"""Shelly options for connection."""
ip_address: str
username: str | None = None
password: str | None = None
temperature_unit: str = "C"
auth: BasicAuth | None = None
device_mac: str | None = None
port: int = DEFAULT_HTTP_PORT
def __post_init__(self) -> None:
"""Call after initialization."""
if self.username is not None:
if self.password is None:
raise ValueError("Supply both username and password")
object.__setattr__(self, "auth", BasicAuth(self.username, self.password))
IpOrOptionsType = str | ConnectionOptions
async def process_ip_or_options(ip_or_options: IpOrOptionsType) -> ConnectionOptions:
"""Return ConnectionOptions class from ip str or ConnectionOptions."""
if isinstance(ip_or_options, str):
options = ConnectionOptions(ip_or_options)
else:
options = ip_or_options
try:
ipaddress.ip_address(options.ip_address)
except ValueError:
loop = asyncio.get_running_loop()
options.ip_address = await loop.run_in_executor(
None, gethostbyname, options.ip_address
)
return options
async def get_info(
aiohttp_session: ClientSession,
ip_address: str,
device_mac: str | None = None,
port: int = DEFAULT_HTTP_PORT,
) -> dict[str, Any]:
"""Get info from device through REST call."""
try:
async with aiohttp_session.get(
URL.build(scheme="http", host=ip_address, port=port, path="/shelly"),
raise_for_status=True,
timeout=DEVICE_IO_TIMEOUT_CLIENT_TIMEOUT,
) as resp:
result: dict[str, Any] = await resp.json()
except CONNECT_ERRORS as err:
error = DeviceConnectionError(err)
_LOGGER.debug("host %s:%s: error: %r", ip_address, port, error)
raise error from err
mac = result["mac"]
if device_mac and device_mac != mac:
mac_err = MacAddressMismatchError(f"Input MAC: {device_mac}, Shelly MAC: {mac}")
_LOGGER.debug("host %s:%s: error: %r", ip_address, port, mac_err)
raise mac_err
return result
|