import asyncio from dataclasses import dataclass from enum import Enum, auto from importlib.resources import files import json import logging from os import PathLike from typing import Dict, Optional, Union from nibe.coil import Coil, CoilData from nibe.event_server import EventServer from nibe.exceptions import CoilNotFoundException, ModelIdentificationFailed logger = logging.getLogger("nibe").getChild(__name__) class Series(Enum): """Series enum class""" CUSTOM = auto() F = auto() S = auto() class Model(Enum): """Model enum class""" F1155 = "f1155_f1255", Series.F F1255 = "f1155_f1255", Series.F S1155 = "s1155_s1255", Series.S S1255 = "s1155_s1255", Series.S S1156 = "s1156_s1256", Series.S S1256 = "s1156_s1256", Series.S F1145 = "f1145_f1245", Series.F F1245 = "f1145_f1245", Series.F F1345 = "f1345", Series.F F1355 = "f1355", Series.F F730 = "f730", Series.F F750 = "f750", Series.F F370 = "f370_f470", Series.F F470 = "f370_f470", Series.F S320 = "s320_s325", Series.S S325 = "s320_s325", Series.S S330 = "s330_s332", Series.S S332 = "s330_s332", Series.S S735 = "s735", Series.S S2125 = "s2125", Series.S SMO20 = "smo20", Series.F SMO40 = "smo40", Series.F SMOS40 = "smos40", Series.S VVM225 = "vvm225_vvm320_vvm325", Series.F VVM320 = "vvm225_vvm320_vvm325", Series.F VVM325 = "vvm225_vvm320_vvm325", Series.F VVM310 = "vvm310_vvm500", Series.F VVM500 = "vvm310_vvm500", Series.F VVMS325 = "vvms325", Series.S CUSTOM = "custom", Series.CUSTOM data_file: Union[str, bytes, PathLike[str], PathLike[bytes]] series: Series def __new__(cls, data_file: str, series: Series): value = len(cls.__members__) + 1 obj = object.__new__(cls) obj._value_ = value obj.data_file = data_file obj.series = series return obj def get_coil_data(self): """Get coil data for model""" if self == Model.CUSTOM: with open(self.data_file) as fh: return json.load(fh) else: with files("nibe.data").joinpath(f"{self.data_file}.json").open("r") as fh: return json.load(fh) @classmethod def keys(cls): """Get all keys of the enum class""" return cls.__members__.keys() @dataclass class ProductInfo: """Product info class""" model: str firmware_version: int def identify_model(self) -> Model: """Identify model from product info :raises ModelIdentificationFailed: When model cannot be identified""" for key in Model.keys(): if key in self.model.upper(): return getattr(Model, key) raise ModelIdentificationFailed(f'Unable to identify model from "{self.model}"') class HeatPump(EventServer): """Heat pump class""" COIL_UPDATE_EVENT = "coil_update" _address_to_coil: Dict[str, Coil] _name_to_coil: Dict[str, Coil] word_swap: Optional[bool] = None _product_info: Union[ProductInfo, None] = None _model: Optional[Model] = None def __init__(self, model: Optional[Model] = None): super().__init__() self._address_to_coil = {} self._name_to_coil = {} if model is not None: self.model = model @property def model(self) -> Union[Model, None]: """Returns the model of the heat pump""" return self._model @model.setter def model(self, model: Model): """Sets the model of the heat pump""" assert isinstance(model, Model), "Passed argument is not of a Model type" self._model = model @property def series(self) -> Series: """Returns the series of the heat pump""" assert self._model return self._model.series @property def product_info(self) -> Union[ProductInfo, None]: """Returns the product info of the heat pump""" return self._product_info @product_info.setter def product_info(self, product_info: ProductInfo): """Sets the product info of the heat pump""" assert isinstance( product_info, ProductInfo ), "Passed argument is not of a ProductInfo type" self._product_info = product_info async def _load_coils(self): assert isinstance(self._model, Model), "Model is not set" data = await asyncio.get_running_loop().run_in_executor( None, self._model.get_coil_data ) self._address_to_coil = {} for k, v in data.items(): try: self._address_to_coil[k] = Coil(address=int(k), **v) except (AssertionError, TypeError) as e: logger.warning(f"Failed to register coil {k}: {e}") self._name_to_coil = {c.name: c for _, c in self._address_to_coil.items()} async def initialize(self): """Initialize the heat pump""" if not isinstance(self._model, Model) and isinstance( self._product_info, ProductInfo ): self.model = self._product_info.identify_model() assert isinstance( self._model, Model ), "Model is not set and product info is not available" await self._load_coils() def get_coils(self) -> list[Coil]: """Returns a list of all coils""" return list(self._address_to_coil.values()) def get_coil_by_address(self, address: Union[int, str]) -> Coil: """Returns a coil by address :raises CoilNotFoundException: if coil is not found """ try: return self._address_to_coil[str(address)] except KeyError: raise CoilNotFoundException(f"Coil with address {address} not found") def get_coil_by_name(self, name: str) -> Coil: """Returns a coil by name :raises CoilNotFoundException: if coil is not found """ try: return self._name_to_coil[str(name)] except KeyError: raise CoilNotFoundException(f"Coil with name '{name}' not found") def notify_coil_update(self, coil_data: CoilData): """Notifies listeners about coil update""" self.notify_event_listeners(self.COIL_UPDATE_EVENT, coil_data)