diff --git a/aioairzone/localapi.py b/aioairzone/localapi.py index 11188e4..76384c5 100644 --- a/aioairzone/localapi.py +++ b/aioairzone/localapi.py @@ -1,4 +1,4 @@ -"""Airzone Local API based device.""" +"""Airzone Local API.""" from __future__ import annotations import json @@ -8,120 +8,28 @@ from aiohttp import ClientResponseError, ClientSession from aiohttp.client_reqrep import ClientResponse -from .common import ( - AirzoneStages, - ConnectionOptions, - OperationMode, - SystemType, - TemperatureUnit, - ThermostatType, - WebServerInterface, -) +from .common import ConnectionOptions from .const import ( - API_AIR_DEMAND, - API_COLD_STAGE, - API_COLD_STAGES, - API_COOL_MAX_TEMP, - API_COOL_MIN_TEMP, - API_COOL_SET_POINT, API_DATA, - API_DOUBLE_SET_POINT, - API_ERROR_LOW_BATTERY, API_ERROR_SYSTEM_ID_OUT_RANGE, API_ERROR_ZONE_ID_NOT_AVAILABLE, API_ERROR_ZONE_ID_OUT_RANGE, API_ERRORS, - API_FLOOR_DEMAND, - API_HEAT_MAX_TEMP, - API_HEAT_MIN_TEMP, - API_HEAT_SET_POINT, - API_HEAT_STAGE, - API_HEAT_STAGES, - API_HUMIDITY, API_HVAC, - API_INTERFACE, API_MAC, - API_MAX_TEMP, - API_MIN_TEMP, - API_MODE, - API_MODES, - API_NAME, - API_ON, - API_POWER, - API_ROOM_TEMP, - API_SET_POINT, - API_SPEED, - API_SPEEDS, - API_SYSTEM_FIRMWARE, API_SYSTEM_ID, API_SYSTEM_PARAMS, - API_SYSTEM_TYPE, API_SYSTEMS, - API_THERMOS_FIRMWARE, - API_THERMOS_RADIO, - API_THERMOS_TYPE, - API_UNITS, API_V1, API_WEBSERVER, - API_WIFI, - API_WIFI_CHANNEL, - API_WIFI_QUALITY, - API_WIFI_SIGNAL, API_ZONE_ID, API_ZONE_PARAMS, - AZD_AIR_DEMAND, - AZD_BATTERY_LOW, - AZD_COLD_STAGE, - AZD_COLD_STAGES, - AZD_COOL_TEMP_MAX, - AZD_COOL_TEMP_MIN, - AZD_COOL_TEMP_SET, - AZD_DEMAND, - AZD_DOUBLE_SET_POINT, - AZD_ERRORS, - AZD_FIRMWARE, - AZD_FLOOR_DEMAND, - AZD_HEAT_STAGE, - AZD_HEAT_STAGES, - AZD_HEAT_TEMP_MAX, - AZD_HEAT_TEMP_MIN, - AZD_HEAT_TEMP_SET, - AZD_HUMIDITY, - AZD_ID, - AZD_INTERFACE, - AZD_MAC, - AZD_MASTER, - AZD_MODE, - AZD_MODEL, - AZD_MODES, - AZD_NAME, - AZD_ON, - AZD_POWER, - AZD_PROBLEMS, - AZD_SPEED, - AZD_SPEEDS, - AZD_SYSTEM, AZD_SYSTEMS, AZD_SYSTEMS_NUM, - AZD_TEMP, - AZD_TEMP_MAX, - AZD_TEMP_MIN, - AZD_TEMP_SET, - AZD_TEMP_UNIT, - AZD_THERMOSTAT_FW, - AZD_THERMOSTAT_MODEL, - AZD_THERMOSTAT_RADIO, AZD_WEBSERVER, - AZD_WIFI_CHANNEL, - AZD_WIFI_QUALITY, - AZD_WIFI_SIGNAL, AZD_ZONES, AZD_ZONES_NUM, - ERROR_SYSTEM, - ERROR_ZONE, HTTP_CALL_TIMEOUT, - THERMOSTAT_RADIO, - THERMOSTAT_WIRED, ) from .exceptions import ( APIError, @@ -131,6 +39,9 @@ InvalidZone, ParamUpdateFailure, ) +from .system import System +from .webserver import WebServer +from .zone import Zone _LOGGER = logging.getLogger(__name__) @@ -344,547 +255,3 @@ def num_zones(self) -> int: for system in self.systems.values(): count += system.num_zones() return count - - -class System: - """Airzone System.""" - - def __init__(self, airzone_system): - """System init.""" - self.errors: list[str] = [] - self.id = None - self.firmware: str | None = None - self.modes: list[OperationMode] = [] - self.power: bool | None = None - self.type: SystemType | None = None - self.zones: dict[int, Zone] = {} - - for airzone_zone in airzone_system: - zone = Zone(self, airzone_zone) - if zone: - _id = int(airzone_zone[API_SYSTEM_ID]) - if not self.id: - self.id = _id - elif self.id != _id: - _LOGGER.error("System ID mismatch across zones") - - self.zones[zone.get_id()] = zone - - def data(self) -> dict[str, Any]: - """Return Airzone system data.""" - data: dict[str, Any] = { - AZD_ID: self.get_id(), - AZD_PROBLEMS: self.get_problems(), - AZD_ZONES_NUM: self.num_zones(), - } - - if len(self.errors) > 0: - data[AZD_ERRORS] = self.get_errors() - - if self.firmware: - data[AZD_FIRMWARE] = self.get_firmware() - - if self.type: - data[AZD_MODEL] = self.get_model() - - if self.power is not None: - data[AZD_POWER] = self.get_power() - - return data - - def add_error(self, val: str) -> None: - """Add system error.""" - if val not in self.errors: - self.errors.append(val) - - def get_errors(self) -> list[str]: - """Return system errors.""" - return self.errors - - def get_id(self) -> int: - """Return system ID.""" - return self.id - - def get_firmware(self) -> str | None: - """Return system firmware.""" - if self.firmware and "." not in self.firmware and len(self.firmware) > 2: - return f"{self.firmware[0:1]}.{self.firmware[1:]}" - return self.firmware - - def get_model(self) -> str | None: - """Return system model.""" - if self.type: - return str(self.type) - return None - - def get_modes(self) -> list[OperationMode]: - """Return system modes.""" - return self.modes - - def get_power(self) -> bool | None: - """Return system power.""" - return self.power - - def get_problems(self) -> bool: - """Return system problems.""" - return bool(self.errors) - - def get_zone(self, zone_id: int) -> Zone: - """Return Airzone zone.""" - for zone in self.zones.values(): - if zone.get_id() == zone_id: - return zone - raise InvalidZone - - def num_zones(self) -> int: - """Return number of system zones.""" - return len(self.zones) - - def set_modes(self, modes: list[OperationMode]) -> None: - """Set system modes.""" - self.modes = modes - - def set_param(self, key: str, value: Any) -> None: - """Update zones parameters by key and value.""" - for zone in self.zones.values(): - zone.set_param(key, value) - - def update_data(self, data: dict[str, Any]) -> None: - """Update system parameters by dict.""" - if API_SYSTEM_FIRMWARE in data: - self.firmware = str(data[API_SYSTEM_FIRMWARE]) - - if API_POWER in data: - self.power = bool(data[API_POWER]) - - if API_SYSTEM_TYPE in data: - self.type = SystemType(data[API_SYSTEM_TYPE]) - - -class Thermostat: - """Airzone Thermostat.""" - - def __init__(self, data: dict[str, Any]): - self.firmware: str | None = None - self.radio: bool | None = None - self.type: ThermostatType | None = None - - if API_THERMOS_FIRMWARE in data: - self.firmware = str(data[API_THERMOS_FIRMWARE]) - if API_THERMOS_RADIO in data: - self.radio = bool(data[API_THERMOS_RADIO]) - if API_THERMOS_TYPE in data: - self.type = ThermostatType(data[API_THERMOS_TYPE]) - - def get_firmware(self) -> str | None: - """Return Airzone Thermostat firmware.""" - if self.firmware and "." not in self.firmware and len(self.firmware) > 2: - return f"{self.firmware[0:1]}.{self.firmware[1:]}" - return self.firmware - - def get_model(self) -> str | None: - """Return Airzone Thermostat model.""" - if self.type: - name = str(self.type) - if self.type.exists_radio(): - sfx = f" ({THERMOSTAT_RADIO if self.radio else THERMOSTAT_WIRED})" - else: - sfx = "" - return f"{name}{sfx}" - return None - - def get_radio(self) -> bool | None: - """Return Airzone Thermostat radio.""" - return self.radio - - -class WebServer: - """Airzone WebServer.""" - - def __init__(self, data: dict[str, Any]): - """WebServer init.""" - self.interface: WebServerInterface | None = None - self.mac: str | None = None - self.wifi_channel: int | None = None - self.wifi_quality: int | None = None - self.wifi_signal: int | None = None - - if API_INTERFACE in data: - if data[API_INTERFACE] == API_WIFI: - self.interface = WebServerInterface.WIFI - else: - self.interface = WebServerInterface.ETHERNET - - if API_MAC in data: - self.mac = str(data[API_MAC]) - - if API_WIFI_CHANNEL in data: - self.wifi_channel = int(data[API_WIFI_CHANNEL]) - if API_WIFI_QUALITY in data: - self.wifi_quality = int(data[API_WIFI_QUALITY]) - if API_WIFI_SIGNAL in data: - self.wifi_signal = int(data[API_WIFI_SIGNAL]) - - def data(self) -> dict[str, Any]: - """Return Airzone system data.""" - data: dict[str, Any] = {} - - if self.interface is not None: - data[AZD_INTERFACE] = self.get_interface() - - if self.mac is not None: - data[AZD_MAC] = self.get_mac() - - if self.wifi_channel is not None: - data[AZD_WIFI_CHANNEL] = self.get_wifi_channel() - if self.wifi_quality is not None: - data[AZD_WIFI_QUALITY] = self.get_wifi_quality() - if self.wifi_signal is not None: - data[AZD_WIFI_SIGNAL] = self.get_wifi_signal() - - return data - - def get_interface(self) -> WebServerInterface | None: - """Return WebServer network interface.""" - return self.interface - - def get_mac(self) -> str | None: - """Return WebServer MAC address.""" - return self.mac - - def get_wifi_channel(self) -> int | None: - """Return WebServer wifi channel.""" - return self.wifi_channel - - def get_wifi_quality(self) -> int | None: - """Return WebServer wifi quality.""" - return self.wifi_quality - - def get_wifi_signal(self) -> int | None: - """Return WebServer wifi signal.""" - return self.wifi_signal - - -class Zone: - """Airzone Zone.""" - - def __init__(self, system: System, zone: dict[str, Any]): - """Zone init.""" - self.air_demand = bool(zone[API_AIR_DEMAND]) - self.cold_stage = AirzoneStages(zone[API_COLD_STAGE]) - self.cold_stages: list[AirzoneStages] = [] - self.cool_temp_max: float | None = None - self.cool_temp_min: float | None = None - self.cool_temp_set: float | None = None - self.double_set_point: bool = False - self.errors: list[str] = [] - self.floor_demand = bool(zone[API_FLOOR_DEMAND]) - self.heat_temp_max: float | None = None - self.heat_temp_min: float | None = None - self.heat_temp_set: float | None = None - self.heat_stage = AirzoneStages(zone[API_HEAT_STAGE]) - self.heat_stages: list[AirzoneStages] = [] - self.humidity = int(zone[API_HUMIDITY]) - self.id = int(zone[API_ZONE_ID]) - self.master = bool(API_MODES in zone) - self.mode = OperationMode(zone[API_MODE]) - self.modes: list[OperationMode] = [] - self.name = str(zone[API_NAME]) - self.on = bool(zone[API_ON]) - self.speed: int | None = None - self.speeds: int | None = None - self.temp = float(zone[API_ROOM_TEMP]) - self.temp_max = float(zone[API_MAX_TEMP]) - self.temp_min = float(zone[API_MIN_TEMP]) - self.temp_set = float(zone[API_SET_POINT]) - self.temp_unit = TemperatureUnit(zone[API_UNITS]) - self.thermostat = Thermostat(zone) - self.system = system - - if API_COLD_STAGES in zone: - cold_stages = AirzoneStages(zone[API_COLD_STAGES]) - self.cold_stages = cold_stages.to_list() - elif self.cold_stage: - self.cold_stages = [self.cold_stage] - if API_HEAT_STAGES in zone: - heat_stages = AirzoneStages(zone[API_HEAT_STAGES]) - self.heat_stages = heat_stages.to_list() - elif self.heat_stage: - self.heat_stages = [self.heat_stage] - - if API_COOL_MAX_TEMP in zone: - self.cool_temp_max = float(zone[API_COOL_MAX_TEMP]) - if API_COOL_MIN_TEMP in zone: - self.cool_temp_min = float(zone[API_COOL_MIN_TEMP]) - if API_COOL_SET_POINT in zone: - self.cool_temp_set = float(zone[API_COOL_SET_POINT]) - if API_DOUBLE_SET_POINT in zone: - self.double_set_point = bool(zone[API_DOUBLE_SET_POINT]) - if API_HEAT_MAX_TEMP in zone: - self.heat_temp_max = float(zone[API_HEAT_MAX_TEMP]) - if API_HEAT_MIN_TEMP in zone: - self.heat_temp_min = float(zone[API_HEAT_MIN_TEMP]) - if API_HEAT_SET_POINT in zone: - self.heat_temp_set = float(zone[API_HEAT_SET_POINT]) - - if API_ERRORS in zone: - errors: list[dict[str, str]] = zone[API_ERRORS] - for error in errors: - for key, val in error.items(): - self.add_error(key, val) - - if API_SPEED in zone: - self.speed = int(zone[API_SPEED]) - if API_SPEEDS in zone: - self.speeds = int(zone[API_SPEEDS]) - - if self.master: - for mode in zone[API_MODES]: - self.modes.append(OperationMode(mode)) - if OperationMode.STOP not in self.modes: - self.modes.append(OperationMode.STOP) - self.system.set_modes(self.modes) - - def data(self) -> dict[str, Any]: - """Return Airzone zone data.""" - data = { - AZD_AIR_DEMAND: self.get_air_demand(), - AZD_COLD_STAGE: self.get_cold_stage(), - AZD_DEMAND: self.get_demand(), - AZD_DOUBLE_SET_POINT: self.get_double_set_point(), - AZD_FLOOR_DEMAND: self.get_floor_demand(), - AZD_HEAT_STAGE: self.get_heat_stage(), - AZD_HUMIDITY: self.get_humidity(), - AZD_ID: self.get_id(), - AZD_MASTER: self.get_master(), - AZD_MODE: self.get_mode(), - AZD_NAME: self.get_name(), - AZD_ON: self.get_on(), - AZD_PROBLEMS: self.get_problems(), - AZD_SYSTEM: self.get_system_id(), - AZD_TEMP: self.get_temp(), - AZD_TEMP_MAX: self.get_temp_max(), - AZD_TEMP_MIN: self.get_temp_min(), - AZD_TEMP_SET: self.get_temp_set(), - AZD_TEMP_UNIT: self.get_temp_unit(), - } - - if data[AZD_DOUBLE_SET_POINT]: - if self.cool_temp_max: - data[AZD_COOL_TEMP_MAX] = self.get_cool_temp_max() - if self.cool_temp_min: - data[AZD_COOL_TEMP_MIN] = self.get_cool_temp_min() - if self.cool_temp_set: - data[AZD_COOL_TEMP_SET] = self.get_cool_temp_set() - if self.heat_temp_max: - data[AZD_HEAT_TEMP_MAX] = self.get_heat_temp_max() - if self.heat_temp_min: - data[AZD_HEAT_TEMP_MIN] = self.get_heat_temp_min() - if self.heat_temp_set: - data[AZD_HEAT_TEMP_SET] = self.get_heat_temp_set() - - if self.cold_stages: - data[AZD_COLD_STAGES] = self.get_cold_stages() - if self.heat_stages: - data[AZD_HEAT_STAGES] = self.get_heat_stages() - - if self.speed: - data[AZD_SPEED] = self.speed - if self.speeds: - data[AZD_SPEEDS] = self.speeds - - if len(self.errors) > 0: - data[AZD_ERRORS] = self.get_errors() - - modes = self.get_modes() - if modes: - data[AZD_MODES] = modes - - if self.thermostat.firmware: - data[AZD_THERMOSTAT_FW] = self.thermostat.get_firmware() - if self.thermostat.type: - data[AZD_THERMOSTAT_MODEL] = self.thermostat.get_model() - if self.thermostat.radio: - data[AZD_THERMOSTAT_RADIO] = self.thermostat.get_radio() - - battery_low = self.get_battery_low() - if battery_low is not None: - data[AZD_BATTERY_LOW] = battery_low - - return data - - def add_error(self, key: str, val: str) -> None: - """Add system or zone error.""" - _key = key.casefold() - if _key == ERROR_SYSTEM: - self.system.add_error(val) - elif _key == ERROR_ZONE: - if val not in self.errors: - self.errors.append(val) - - def get_air_demand(self) -> bool: - """Return zone air demand.""" - return self.air_demand - - def get_battery_low(self) -> bool | None: - """Return battery status.""" - if self.thermostat.get_radio(): - return API_ERROR_LOW_BATTERY in self.errors - return None - - def get_cold_stage(self) -> AirzoneStages: - """Return zone cold stage.""" - return self.cold_stage - - def get_cold_stages(self) -> list[AirzoneStages]: - """Return zone cold stages.""" - return self.cold_stages - - def get_cool_temp_max(self) -> float | None: - """Return zone maximum cool temperature.""" - if self.cool_temp_max: - return round(self.cool_temp_max, 1) - return None - - def get_cool_temp_min(self) -> float | None: - """Return zone minimum cool temperature.""" - if self.cool_temp_min: - return round(self.cool_temp_min, 1) - return None - - def get_cool_temp_set(self) -> float | None: - """Return zone set cool temperature.""" - if self.cool_temp_set: - return round(self.cool_temp_set, 1) - return None - - def get_demand(self) -> bool: - """Return zone demand.""" - return self.get_air_demand() or self.get_floor_demand() - - def get_double_set_point(self) -> bool: - """Return zone double set point.""" - return self.double_set_point - - def get_heat_temp_max(self) -> float | None: - """Return zone maximum heat temperature.""" - if self.heat_temp_max: - return round(self.heat_temp_max, 1) - return None - - def get_heat_temp_min(self) -> float | None: - """Return zone minimum heat temperature.""" - if self.heat_temp_min: - return round(self.heat_temp_min, 1) - return None - - def get_heat_temp_set(self) -> float | None: - """Return zone set heat temperature.""" - if self.heat_temp_set: - return round(self.heat_temp_set, 1) - return None - - def get_errors(self) -> list[str]: - """Return zone errors.""" - return self.errors - - def get_floor_demand(self) -> bool: - """Return zone floor demand.""" - return self.floor_demand - - def get_id(self) -> int: - """Return zone ID.""" - return self.id - - def get_heat_stage(self) -> AirzoneStages: - """Return zone heat stage.""" - return self.heat_stage - - def get_heat_stages(self) -> list[AirzoneStages]: - """Return zone heat stages.""" - return self.heat_stages - - def get_humidity(self) -> int: - """Return zone humidity.""" - return self.humidity - - def get_master(self) -> bool: - """Return zone master/slave.""" - return self.master - - def get_mode(self) -> OperationMode: - """Return zone mode.""" - return self.mode - - def get_modes(self) -> list[OperationMode]: - """Return zone modes.""" - if self.master: - return self.modes - return self.system.get_modes() - - def get_name(self) -> str: - """Return zone name.""" - return self.name - - def get_on(self) -> bool: - """Return zone on/off.""" - return self.on - - def get_problems(self) -> bool: - """Return zone problems.""" - return bool(self.errors) - - def get_speed(self) -> int | None: - """Return zone speed.""" - return self.speed - - def get_speeds(self) -> int | None: - """Return zone speedS.""" - return self.speeds - - def get_system_id(self) -> int: - """Return system ID.""" - return self.system.get_id() - - def get_system_zone_id(self) -> str: - """Combine System and Zone IDs into a single ID.""" - return f"{self.get_system_id()}:{self.get_id()}" - - def get_temp(self) -> float: - """Return zone temperature.""" - return round(self.temp, 2) - - def get_temp_max(self) -> float: - """Return zone maximum temperature.""" - return round(self.temp_max, 1) - - def get_temp_min(self) -> float: - """Return zone minimum temperature.""" - return round(self.temp_min, 1) - - def get_temp_set(self) -> float: - """Return zone set temperature.""" - return round(self.temp_set, 1) - - def get_temp_unit(self) -> TemperatureUnit: - """Return zone temperature unit.""" - return self.temp_unit - - def set_param(self, key: str, value: Any) -> None: - """Update zone parameter by key and value.""" - if key == API_COOL_SET_POINT: - self.cool_temp_set = float(value) - elif key == API_COLD_STAGE: - self.cold_stage = AirzoneStages(value) - elif key == API_HEAT_SET_POINT: - self.heat_temp_set = float(value) - elif key == API_HEAT_STAGE: - self.heat_stage = AirzoneStages(value) - elif key == API_MODE: - self.mode = OperationMode(value) - elif key == API_NAME: - self.name = str(value) - elif key == API_ON: - self.on = bool(value) - elif key == API_SET_POINT: - self.temp_set = float(value) diff --git a/aioairzone/system.py b/aioairzone/system.py new file mode 100644 index 0000000..064874b --- /dev/null +++ b/aioairzone/system.py @@ -0,0 +1,139 @@ +"""Airzone Local API System.""" +from __future__ import annotations + +import logging +from typing import Any + +from .common import OperationMode, SystemType +from .const import ( + API_POWER, + API_SYSTEM_FIRMWARE, + API_SYSTEM_ID, + API_SYSTEM_TYPE, + AZD_ERRORS, + AZD_FIRMWARE, + AZD_ID, + AZD_MODEL, + AZD_POWER, + AZD_PROBLEMS, + AZD_ZONES_NUM, +) +from .exceptions import InvalidZone +from .zone import Zone + +_LOGGER = logging.getLogger(__name__) + + +class System: + """Airzone System.""" + + def __init__(self, airzone_system): + """System init.""" + self.errors: list[str] = [] + self.id = None + self.firmware: str | None = None + self.modes: list[OperationMode] = [] + self.power: bool | None = None + self.type: SystemType | None = None + self.zones: dict[int, Zone] = {} + + for airzone_zone in airzone_system: + zone = Zone(self, airzone_zone) + if zone: + _id = int(airzone_zone[API_SYSTEM_ID]) + if not self.id: + self.id = _id + elif self.id != _id: + _LOGGER.error("System ID mismatch across zones") + + self.zones[zone.get_id()] = zone + + def data(self) -> dict[str, Any]: + """Return Airzone system data.""" + data: dict[str, Any] = { + AZD_ID: self.get_id(), + AZD_PROBLEMS: self.get_problems(), + AZD_ZONES_NUM: self.num_zones(), + } + + if len(self.errors) > 0: + data[AZD_ERRORS] = self.get_errors() + + if self.firmware: + data[AZD_FIRMWARE] = self.get_firmware() + + if self.type: + data[AZD_MODEL] = self.get_model() + + if self.power is not None: + data[AZD_POWER] = self.get_power() + + return data + + def add_error(self, val: str) -> None: + """Add system error.""" + if val not in self.errors: + self.errors.append(val) + + def get_errors(self) -> list[str]: + """Return system errors.""" + return self.errors + + def get_id(self) -> int: + """Return system ID.""" + return self.id + + def get_firmware(self) -> str | None: + """Return system firmware.""" + if self.firmware and "." not in self.firmware and len(self.firmware) > 2: + return f"{self.firmware[0:1]}.{self.firmware[1:]}" + return self.firmware + + def get_model(self) -> str | None: + """Return system model.""" + if self.type: + return str(self.type) + return None + + def get_modes(self) -> list[OperationMode]: + """Return system modes.""" + return self.modes + + def get_power(self) -> bool | None: + """Return system power.""" + return self.power + + def get_problems(self) -> bool: + """Return system problems.""" + return bool(self.errors) + + def get_zone(self, zone_id: int) -> Zone: + """Return Airzone zone.""" + for zone in self.zones.values(): + if zone.get_id() == zone_id: + return zone + raise InvalidZone + + def num_zones(self) -> int: + """Return number of system zones.""" + return len(self.zones) + + def set_modes(self, modes: list[OperationMode]) -> None: + """Set system modes.""" + self.modes = modes + + def set_param(self, key: str, value: Any) -> None: + """Update zones parameters by key and value.""" + for zone in self.zones.values(): + zone.set_param(key, value) + + def update_data(self, data: dict[str, Any]) -> None: + """Update system parameters by dict.""" + if API_SYSTEM_FIRMWARE in data: + self.firmware = str(data[API_SYSTEM_FIRMWARE]) + + if API_POWER in data: + self.power = bool(data[API_POWER]) + + if API_SYSTEM_TYPE in data: + self.type = SystemType(data[API_SYSTEM_TYPE]) diff --git a/aioairzone/thermostat.py b/aioairzone/thermostat.py new file mode 100644 index 0000000..808b7ff --- /dev/null +++ b/aioairzone/thermostat.py @@ -0,0 +1,50 @@ +"""Airzone Local API Thermostat.""" +from __future__ import annotations + +from typing import Any + +from .common import ThermostatType +from .const import ( + API_THERMOS_FIRMWARE, + API_THERMOS_RADIO, + API_THERMOS_TYPE, + THERMOSTAT_RADIO, + THERMOSTAT_WIRED, +) + + +class Thermostat: + """Airzone Thermostat.""" + + def __init__(self, data: dict[str, Any]): + self.firmware: str | None = None + self.radio: bool | None = None + self.type: ThermostatType | None = None + + if API_THERMOS_FIRMWARE in data: + self.firmware = str(data[API_THERMOS_FIRMWARE]) + if API_THERMOS_RADIO in data: + self.radio = bool(data[API_THERMOS_RADIO]) + if API_THERMOS_TYPE in data: + self.type = ThermostatType(data[API_THERMOS_TYPE]) + + def get_firmware(self) -> str | None: + """Return Airzone Thermostat firmware.""" + if self.firmware and "." not in self.firmware and len(self.firmware) > 2: + return f"{self.firmware[0:1]}.{self.firmware[1:]}" + return self.firmware + + def get_model(self) -> str | None: + """Return Airzone Thermostat model.""" + if self.type: + name = str(self.type) + if self.type.exists_radio(): + sfx = f" ({THERMOSTAT_RADIO if self.radio else THERMOSTAT_WIRED})" + else: + sfx = "" + return f"{name}{sfx}" + return None + + def get_radio(self) -> bool | None: + """Return Airzone Thermostat radio.""" + return self.radio diff --git a/aioairzone/webserver.py b/aioairzone/webserver.py new file mode 100644 index 0000000..7a0510a --- /dev/null +++ b/aioairzone/webserver.py @@ -0,0 +1,86 @@ +"""Airzone Local API WebServer.""" +from __future__ import annotations + +from typing import Any + +from .common import WebServerInterface +from .const import ( + API_INTERFACE, + API_MAC, + API_WIFI, + API_WIFI_CHANNEL, + API_WIFI_QUALITY, + API_WIFI_SIGNAL, + AZD_INTERFACE, + AZD_MAC, + AZD_WIFI_CHANNEL, + AZD_WIFI_QUALITY, + AZD_WIFI_SIGNAL, +) + + +class WebServer: + """Airzone WebServer.""" + + def __init__(self, data: dict[str, Any]): + """WebServer init.""" + self.interface: WebServerInterface | None = None + self.mac: str | None = None + self.wifi_channel: int | None = None + self.wifi_quality: int | None = None + self.wifi_signal: int | None = None + + if API_INTERFACE in data: + if data[API_INTERFACE] == API_WIFI: + self.interface = WebServerInterface.WIFI + else: + self.interface = WebServerInterface.ETHERNET + + if API_MAC in data: + self.mac = str(data[API_MAC]) + + if API_WIFI_CHANNEL in data: + self.wifi_channel = int(data[API_WIFI_CHANNEL]) + if API_WIFI_QUALITY in data: + self.wifi_quality = int(data[API_WIFI_QUALITY]) + if API_WIFI_SIGNAL in data: + self.wifi_signal = int(data[API_WIFI_SIGNAL]) + + def data(self) -> dict[str, Any]: + """Return Airzone system data.""" + data: dict[str, Any] = {} + + if self.interface is not None: + data[AZD_INTERFACE] = self.get_interface() + + if self.mac is not None: + data[AZD_MAC] = self.get_mac() + + if self.wifi_channel is not None: + data[AZD_WIFI_CHANNEL] = self.get_wifi_channel() + if self.wifi_quality is not None: + data[AZD_WIFI_QUALITY] = self.get_wifi_quality() + if self.wifi_signal is not None: + data[AZD_WIFI_SIGNAL] = self.get_wifi_signal() + + return data + + def get_interface(self) -> WebServerInterface | None: + """Return WebServer network interface.""" + return self.interface + + def get_mac(self) -> str | None: + """Return WebServer MAC address.""" + return self.mac + + def get_wifi_channel(self) -> int | None: + """Return WebServer wifi channel.""" + return self.wifi_channel + + def get_wifi_quality(self) -> int | None: + """Return WebServer wifi quality.""" + return self.wifi_quality + + def get_wifi_signal(self) -> int | None: + """Return WebServer wifi signal.""" + return self.wifi_signal diff --git a/aioairzone/zone.py b/aioairzone/zone.py new file mode 100644 index 0000000..90f0ef0 --- /dev/null +++ b/aioairzone/zone.py @@ -0,0 +1,400 @@ +"""Airzone Local API Zone.""" +from __future__ import annotations + +from typing import Any + +from .common import AirzoneStages, OperationMode, TemperatureUnit +from .const import ( + API_AIR_DEMAND, + API_COLD_STAGE, + API_COLD_STAGES, + API_COOL_MAX_TEMP, + API_COOL_MIN_TEMP, + API_COOL_SET_POINT, + API_DOUBLE_SET_POINT, + API_ERROR_LOW_BATTERY, + API_ERRORS, + API_FLOOR_DEMAND, + API_HEAT_MAX_TEMP, + API_HEAT_MIN_TEMP, + API_HEAT_SET_POINT, + API_HEAT_STAGE, + API_HEAT_STAGES, + API_HUMIDITY, + API_MAX_TEMP, + API_MIN_TEMP, + API_MODE, + API_MODES, + API_NAME, + API_ON, + API_ROOM_TEMP, + API_SET_POINT, + API_SPEED, + API_SPEEDS, + API_UNITS, + API_ZONE_ID, + AZD_AIR_DEMAND, + AZD_BATTERY_LOW, + AZD_COLD_STAGE, + AZD_COLD_STAGES, + AZD_COOL_TEMP_MAX, + AZD_COOL_TEMP_MIN, + AZD_COOL_TEMP_SET, + AZD_DEMAND, + AZD_DOUBLE_SET_POINT, + AZD_ERRORS, + AZD_FLOOR_DEMAND, + AZD_HEAT_STAGE, + AZD_HEAT_STAGES, + AZD_HEAT_TEMP_MAX, + AZD_HEAT_TEMP_MIN, + AZD_HEAT_TEMP_SET, + AZD_HUMIDITY, + AZD_ID, + AZD_MASTER, + AZD_MODE, + AZD_MODES, + AZD_NAME, + AZD_ON, + AZD_PROBLEMS, + AZD_SPEED, + AZD_SPEEDS, + AZD_SYSTEM, + AZD_TEMP, + AZD_TEMP_MAX, + AZD_TEMP_MIN, + AZD_TEMP_SET, + AZD_TEMP_UNIT, + AZD_THERMOSTAT_FW, + AZD_THERMOSTAT_MODEL, + AZD_THERMOSTAT_RADIO, + ERROR_SYSTEM, + ERROR_ZONE, +) +from .system import System +from .thermostat import Thermostat + + +class Zone: + """Airzone Zone.""" + + def __init__(self, system: System, zone: dict[str, Any]): + """Zone init.""" + self.air_demand = bool(zone[API_AIR_DEMAND]) + self.cold_stage = AirzoneStages(zone[API_COLD_STAGE]) + self.cold_stages: list[AirzoneStages] = [] + self.cool_temp_max: float | None = None + self.cool_temp_min: float | None = None + self.cool_temp_set: float | None = None + self.double_set_point: bool = False + self.errors: list[str] = [] + self.floor_demand = bool(zone[API_FLOOR_DEMAND]) + self.heat_temp_max: float | None = None + self.heat_temp_min: float | None = None + self.heat_temp_set: float | None = None + self.heat_stage = AirzoneStages(zone[API_HEAT_STAGE]) + self.heat_stages: list[AirzoneStages] = [] + self.humidity = int(zone[API_HUMIDITY]) + self.id = int(zone[API_ZONE_ID]) + self.master = bool(API_MODES in zone) + self.mode = OperationMode(zone[API_MODE]) + self.modes: list[OperationMode] = [] + self.name = str(zone[API_NAME]) + self.on = bool(zone[API_ON]) + self.speed: int | None = None + self.speeds: int | None = None + self.temp = float(zone[API_ROOM_TEMP]) + self.temp_max = float(zone[API_MAX_TEMP]) + self.temp_min = float(zone[API_MIN_TEMP]) + self.temp_set = float(zone[API_SET_POINT]) + self.temp_unit = TemperatureUnit(zone[API_UNITS]) + self.thermostat = Thermostat(zone) + self.system = system + + if API_COLD_STAGES in zone: + cold_stages = AirzoneStages(zone[API_COLD_STAGES]) + self.cold_stages = cold_stages.to_list() + elif self.cold_stage: + self.cold_stages = [self.cold_stage] + if API_HEAT_STAGES in zone: + heat_stages = AirzoneStages(zone[API_HEAT_STAGES]) + self.heat_stages = heat_stages.to_list() + elif self.heat_stage: + self.heat_stages = [self.heat_stage] + + if API_COOL_MAX_TEMP in zone: + self.cool_temp_max = float(zone[API_COOL_MAX_TEMP]) + if API_COOL_MIN_TEMP in zone: + self.cool_temp_min = float(zone[API_COOL_MIN_TEMP]) + if API_COOL_SET_POINT in zone: + self.cool_temp_set = float(zone[API_COOL_SET_POINT]) + if API_DOUBLE_SET_POINT in zone: + self.double_set_point = bool(zone[API_DOUBLE_SET_POINT]) + if API_HEAT_MAX_TEMP in zone: + self.heat_temp_max = float(zone[API_HEAT_MAX_TEMP]) + if API_HEAT_MIN_TEMP in zone: + self.heat_temp_min = float(zone[API_HEAT_MIN_TEMP]) + if API_HEAT_SET_POINT in zone: + self.heat_temp_set = float(zone[API_HEAT_SET_POINT]) + + if API_ERRORS in zone: + errors: list[dict[str, str]] = zone[API_ERRORS] + for error in errors: + for key, val in error.items(): + self.add_error(key, val) + + if API_SPEED in zone: + self.speed = int(zone[API_SPEED]) + if API_SPEEDS in zone: + self.speeds = int(zone[API_SPEEDS]) + + if self.master: + for mode in zone[API_MODES]: + self.modes.append(OperationMode(mode)) + if OperationMode.STOP not in self.modes: + self.modes.append(OperationMode.STOP) + self.system.set_modes(self.modes) + + def data(self) -> dict[str, Any]: + """Return Airzone zone data.""" + data = { + AZD_AIR_DEMAND: self.get_air_demand(), + AZD_COLD_STAGE: self.get_cold_stage(), + AZD_DEMAND: self.get_demand(), + AZD_DOUBLE_SET_POINT: self.get_double_set_point(), + AZD_FLOOR_DEMAND: self.get_floor_demand(), + AZD_HEAT_STAGE: self.get_heat_stage(), + AZD_HUMIDITY: self.get_humidity(), + AZD_ID: self.get_id(), + AZD_MASTER: self.get_master(), + AZD_MODE: self.get_mode(), + AZD_NAME: self.get_name(), + AZD_ON: self.get_on(), + AZD_PROBLEMS: self.get_problems(), + AZD_SYSTEM: self.get_system_id(), + AZD_TEMP: self.get_temp(), + AZD_TEMP_MAX: self.get_temp_max(), + AZD_TEMP_MIN: self.get_temp_min(), + AZD_TEMP_SET: self.get_temp_set(), + AZD_TEMP_UNIT: self.get_temp_unit(), + } + + if data[AZD_DOUBLE_SET_POINT]: + if self.cool_temp_max: + data[AZD_COOL_TEMP_MAX] = self.get_cool_temp_max() + if self.cool_temp_min: + data[AZD_COOL_TEMP_MIN] = self.get_cool_temp_min() + if self.cool_temp_set: + data[AZD_COOL_TEMP_SET] = self.get_cool_temp_set() + if self.heat_temp_max: + data[AZD_HEAT_TEMP_MAX] = self.get_heat_temp_max() + if self.heat_temp_min: + data[AZD_HEAT_TEMP_MIN] = self.get_heat_temp_min() + if self.heat_temp_set: + data[AZD_HEAT_TEMP_SET] = self.get_heat_temp_set() + + if self.cold_stages: + data[AZD_COLD_STAGES] = self.get_cold_stages() + if self.heat_stages: + data[AZD_HEAT_STAGES] = self.get_heat_stages() + + if self.speed: + data[AZD_SPEED] = self.speed + if self.speeds: + data[AZD_SPEEDS] = self.speeds + + if len(self.errors) > 0: + data[AZD_ERRORS] = self.get_errors() + + modes = self.get_modes() + if modes: + data[AZD_MODES] = modes + + if self.thermostat.firmware: + data[AZD_THERMOSTAT_FW] = self.thermostat.get_firmware() + if self.thermostat.type: + data[AZD_THERMOSTAT_MODEL] = self.thermostat.get_model() + if self.thermostat.radio: + data[AZD_THERMOSTAT_RADIO] = self.thermostat.get_radio() + + battery_low = self.get_battery_low() + if battery_low is not None: + data[AZD_BATTERY_LOW] = battery_low + + return data + + def add_error(self, key: str, val: str) -> None: + """Add system or zone error.""" + _key = key.casefold() + if _key == ERROR_SYSTEM: + self.system.add_error(val) + elif _key == ERROR_ZONE: + if val not in self.errors: + self.errors.append(val) + + def get_air_demand(self) -> bool: + """Return zone air demand.""" + return self.air_demand + + def get_battery_low(self) -> bool | None: + """Return battery status.""" + if self.thermostat.get_radio(): + return API_ERROR_LOW_BATTERY in self.errors + return None + + def get_cold_stage(self) -> AirzoneStages: + """Return zone cold stage.""" + return self.cold_stage + + def get_cold_stages(self) -> list[AirzoneStages]: + """Return zone cold stages.""" + return self.cold_stages + + def get_cool_temp_max(self) -> float | None: + """Return zone maximum cool temperature.""" + if self.cool_temp_max: + return round(self.cool_temp_max, 1) + return None + + def get_cool_temp_min(self) -> float | None: + """Return zone minimum cool temperature.""" + if self.cool_temp_min: + return round(self.cool_temp_min, 1) + return None + + def get_cool_temp_set(self) -> float | None: + """Return zone set cool temperature.""" + if self.cool_temp_set: + return round(self.cool_temp_set, 1) + return None + + def get_demand(self) -> bool: + """Return zone demand.""" + return self.get_air_demand() or self.get_floor_demand() + + def get_double_set_point(self) -> bool: + """Return zone double set point.""" + return self.double_set_point + + def get_heat_temp_max(self) -> float | None: + """Return zone maximum heat temperature.""" + if self.heat_temp_max: + return round(self.heat_temp_max, 1) + return None + + def get_heat_temp_min(self) -> float | None: + """Return zone minimum heat temperature.""" + if self.heat_temp_min: + return round(self.heat_temp_min, 1) + return None + + def get_heat_temp_set(self) -> float | None: + """Return zone set heat temperature.""" + if self.heat_temp_set: + return round(self.heat_temp_set, 1) + return None + + def get_errors(self) -> list[str]: + """Return zone errors.""" + return self.errors + + def get_floor_demand(self) -> bool: + """Return zone floor demand.""" + return self.floor_demand + + def get_id(self) -> int: + """Return zone ID.""" + return self.id + + def get_heat_stage(self) -> AirzoneStages: + """Return zone heat stage.""" + return self.heat_stage + + def get_heat_stages(self) -> list[AirzoneStages]: + """Return zone heat stages.""" + return self.heat_stages + + def get_humidity(self) -> int: + """Return zone humidity.""" + return self.humidity + + def get_master(self) -> bool: + """Return zone master/slave.""" + return self.master + + def get_mode(self) -> OperationMode: + """Return zone mode.""" + return self.mode + + def get_modes(self) -> list[OperationMode]: + """Return zone modes.""" + if self.master: + return self.modes + return self.system.get_modes() + + def get_name(self) -> str: + """Return zone name.""" + return self.name + + def get_on(self) -> bool: + """Return zone on/off.""" + return self.on + + def get_problems(self) -> bool: + """Return zone problems.""" + return bool(self.errors) + + def get_speed(self) -> int | None: + """Return zone speed.""" + return self.speed + + def get_speeds(self) -> int | None: + """Return zone speedS.""" + return self.speeds + + def get_system_id(self) -> int: + """Return system ID.""" + return self.system.get_id() + + def get_system_zone_id(self) -> str: + """Combine System and Zone IDs into a single ID.""" + return f"{self.get_system_id()}:{self.get_id()}" + + def get_temp(self) -> float: + """Return zone temperature.""" + return round(self.temp, 2) + + def get_temp_max(self) -> float: + """Return zone maximum temperature.""" + return round(self.temp_max, 1) + + def get_temp_min(self) -> float: + """Return zone minimum temperature.""" + return round(self.temp_min, 1) + + def get_temp_set(self) -> float: + """Return zone set temperature.""" + return round(self.temp_set, 1) + + def get_temp_unit(self) -> TemperatureUnit: + """Return zone temperature unit.""" + return self.temp_unit + + def set_param(self, key: str, value: Any) -> None: + """Update zone parameter by key and value.""" + if key == API_COOL_SET_POINT: + self.cool_temp_set = float(value) + elif key == API_COLD_STAGE: + self.cold_stage = AirzoneStages(value) + elif key == API_HEAT_SET_POINT: + self.heat_temp_set = float(value) + elif key == API_HEAT_STAGE: + self.heat_stage = AirzoneStages(value) + elif key == API_MODE: + self.mode = OperationMode(value) + elif key == API_NAME: + self.name = str(value) + elif key == API_ON: + self.on = bool(value) + elif key == API_SET_POINT: + self.temp_set = float(value) diff --git a/pylintrc b/pylintrc index 147931b..101cc46 100644 --- a/pylintrc +++ b/pylintrc @@ -20,7 +20,8 @@ disable= too-many-instance-attributes, too-many-branches, too-many-statements, - C0103 + C0103, + R0801 [REPORTS] score=no