add sources to custom_components
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
"""The koolnova BMS modbus integration.""" # pylint: disable=invalid-name
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME, CONF_DEVICE_ID
|
||||
|
||||
from .const import DOMAIN
|
||||
#from .wfrac.device import Device
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ["sensor", "climate", "select"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
""" Set up koolnova from a config entry. """
|
||||
|
||||
_LOGGER.info('async_setup_entry')
|
||||
# if DOMAIN not in hass.data:
|
||||
# hass.data[DOMAIN] = []
|
||||
#
|
||||
# device: str = entry.data[CONF_HOST]
|
||||
# name: str = entry.data[CONF_NAME]
|
||||
# device_id: str = entry.data[CONF_DEVICE_ID]
|
||||
# operator_id: str = entry.data[CONF_OPERATOR_ID]
|
||||
# port: int = entry.data[CONF_PORT]
|
||||
# airco_id: str = entry.data[CONF_AIRCO_ID]
|
||||
|
||||
# try:
|
||||
# api = Device(hass, name, device, port, device_id, operator_id, airco_id)
|
||||
# await api.update() # initial update to get fresh values
|
||||
# hass.data[DOMAIN].append(api)
|
||||
# except Exception as ex: # pylint: disable=broad-except
|
||||
# _LOGGER.warning("Something whent wrong setting up device [%s] %s", device, ex)
|
||||
#
|
||||
# for component in COMPONENT_TYPES:
|
||||
# hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, component))
|
||||
#
|
||||
# return True
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
_LOGGER.info('async_unload_entry')
|
||||
# if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
# hass.data[DOMAIN].pop(entry.entry_id)
|
||||
# return unloaded
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload config entry."""
|
||||
_LOGGER.info('async_reload_entry')
|
||||
# await async_unload_entry(hass, entry)
|
||||
# await async_setup_entry(hass, entry)
|
||||
|
||||
async def async_remove_entry(hass, entry: ConfigEntry) -> None:
|
||||
"""Handle removal of an entry."""
|
||||
_LOGGER.info('async_remove_entry')
|
||||
# for device in hass.data[DOMAIN]:
|
||||
# temp_device: Device = device
|
||||
# if temp_device.host == entry.data[CONF_HOST]:
|
||||
# try:
|
||||
# await temp_device.delete_account()
|
||||
# _LOGGER.info(
|
||||
# "Deleted operator ID [%s] from airco [%s]",
|
||||
# temp_device.operator_id,
|
||||
# temp_device.airco_id,
|
||||
# )
|
||||
# hass.data[DOMAIN].remove(temp_device)
|
||||
# except Exception as ex: # pylint: disable=broad-except
|
||||
# _LOGGER.warning(
|
||||
# "Something whent wrong deleting account from airco [%s] %s",
|
||||
# temp_device.name,
|
||||
# ex,
|
||||
# )
|
||||
@@ -1,153 +0,0 @@
|
||||
""" for Climate integration."""
|
||||
from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateEntity,
|
||||
ConfigEntry,
|
||||
)
|
||||
from homeassistant.components.climate.const import HVACMode, FAN_AUTO
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
|
||||
from .koolnova.device import MIN_TIME_BETWEEN_UPDATES, Koolnova
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FAN_MODE_TRANSLATION,
|
||||
HVAC_TRANSLATION,
|
||||
SERVICE_SET_HORIZONTAL_SWING_MODE,
|
||||
SERVICE_SET_VERTICAL_SWING_MODE,
|
||||
SUPPORT_FLAGS,
|
||||
SWING_HORIZONTAL_AUTO,
|
||||
SWING_VERTICAL_AUTO,
|
||||
SUPPORT_SWING_MODES,
|
||||
SUPPORTED_FAN_MODES,
|
||||
SUPPORTED_HVAC_MODES,
|
||||
SWING_3D_AUTO,
|
||||
SWING_MODE_TRANSLATION,
|
||||
HORIZONTAL_SWING_MODE_TRANSLATION,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
|
||||
|
||||
async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities):
|
||||
"""Setup climate entities"""
|
||||
for device in hass.data[DOMAIN]:
|
||||
if device.host == entry.data[CONF_HOST]:
|
||||
_LOGGER.info("Setup climate for: %s, %s", device.name, device.airco_id)
|
||||
async_add_entities([KoolnovaZoneClimate(device)])
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
|
||||
class KoolnovaZoneClimate(ClimateEntity):
|
||||
"""Representation of a climate entity"""
|
||||
|
||||
_attr_supported_features: int = SUPPORT_FLAGS
|
||||
_attr_temperature_unit: str = TEMP_CELSIUS
|
||||
_attr_hvac_modes: list[HVACMode] = SUPPORTED_HVAC_MODES
|
||||
_attr_fan_modes: list[str] = SUPPORTED_FAN_MODES
|
||||
_attr_hvac_mode: HVACMode = HVACMode.OFF
|
||||
_attr_fan_mode: str = FAN_AUTO
|
||||
_attr_min_temp: float = 16
|
||||
_attr_max_temp: float = 30
|
||||
|
||||
def __init__(self, device: Device) -> None:
|
||||
self._device = device
|
||||
self._attr_name = device.name
|
||||
self._attr_device_info = device.device_info
|
||||
self._attr_unique_id = f"{DOMAIN}-{self._device.airco_id}-climate"
|
||||
self._update_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
opts = {AirconCommands.PresetTemp: kwargs.get(ATTR_TEMPERATURE)}
|
||||
|
||||
if "hvac_mode" in kwargs:
|
||||
hvac_mode = kwargs.get("hvac_mode")
|
||||
opts.update(
|
||||
{
|
||||
AirconCommands.OperationMode: self._device.airco.OperationMode
|
||||
if hvac_mode == HVACMode.OFF
|
||||
else HVAC_TRANSLATION[hvac_mode],
|
||||
AirconCommands.Operation: hvac_mode != HVACMode.OFF,
|
||||
}
|
||||
)
|
||||
|
||||
await self._device.set_airco(opts)
|
||||
self._update_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
await self._device.set_airco(
|
||||
{AirconCommands.AirFlow: FAN_MODE_TRANSLATION[fan_mode]}
|
||||
)
|
||||
self._update_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._device.set_airco({AirconCommands.Operation: True})
|
||||
self._update_state()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
await self._device.set_airco(
|
||||
{
|
||||
AirconCommands.OperationMode: self._device.airco.OperationMode
|
||||
if hvac_mode == HVACMode.OFF
|
||||
else HVAC_TRANSLATION[hvac_mode],
|
||||
AirconCommands.Operation: hvac_mode != HVACMode.OFF,
|
||||
}
|
||||
)
|
||||
self._update_state()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self._device.set_airco({AirconCommands.Operation: False})
|
||||
self._update_state()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
"""Private update attributes"""
|
||||
airco = self._device.airco
|
||||
|
||||
self._attr_target_temperature = airco.PresetTemp
|
||||
self._attr_current_temperature = airco.IndoorTemp
|
||||
self._attr_fan_mode = list(FAN_MODE_TRANSLATION.keys())[airco.AirFlow]
|
||||
self._attr_swing_mode = (
|
||||
SWING_3D_AUTO
|
||||
if airco.Entrust
|
||||
else list(SWING_MODE_TRANSLATION.keys())[airco.WindDirectionUD]
|
||||
)
|
||||
# self._attr_horizontal_swing_mode = list(
|
||||
# HORIZONTAL_SWING_MODE_TRANSLATION.keys()
|
||||
# )[airco.WindDirectionLR]
|
||||
self._attr_hvac_mode = list(HVAC_TRANSLATION.keys())[airco.OperationMode]
|
||||
|
||||
if airco.Operation is False:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
else:
|
||||
_new_mode: HVACMode = None
|
||||
_mode = airco.OperationMode
|
||||
if _mode == 0:
|
||||
_new_mode = HVACMode.AUTO
|
||||
elif _mode == 1:
|
||||
_new_mode = HVACMode.COOL
|
||||
elif _mode == 2:
|
||||
_new_mode = HVACMode.HEAT
|
||||
elif _mode == 3:
|
||||
_new_mode = HVACMode.FAN_ONLY
|
||||
elif _mode == 4:
|
||||
_new_mode = HVACMode.DRY
|
||||
self._attr_hvac_mode = _new_mode
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
await self._device.update()
|
||||
self._update_state()
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
"""Config flow Koolnova"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_NAME,
|
||||
CONF_BASE,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_FORCE_UPDATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
#from .const import CONF_OPERATOR_ID, CONF_AIRCO_ID, DOMAIN
|
||||
#from .wfrac.repository import Repository
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class KoolnovaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle adding device manually."""
|
||||
|
||||
field = partial(self._field, user_input)
|
||||
data_schema = vol.Schema({
|
||||
field(CONF_NAME, vol.Required, "Airco unknown") : cv.string,
|
||||
field(CONF_HOST, vol.Required) : cv.string,
|
||||
field(CONF_PORT, vol.Optional, 51443): cv.port,
|
||||
field(CONF_FORCE_UPDATE, vol.Optional, False): cv.boolean,
|
||||
})
|
||||
|
||||
return await self._async_create_common(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
user_input=user_input
|
||||
)
|
||||
|
||||
@property
|
||||
def _name(self) -> str | None:
|
||||
return self.context.get(CONF_NAME)
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
class KnownError(exceptions.HomeAssistantError):
|
||||
"""Base class for errors known to this config flow.
|
||||
|
||||
[error_name] is the value passed to [errors] in async_show_form, which should match a key
|
||||
under "errors" in strings.json
|
||||
|
||||
[applies_to_field] is the name of the field name that contains the error (for
|
||||
async_show_form); if the field doesn't exist in the form CONF_BASE will be used instead.
|
||||
"""
|
||||
error_name = "unknown_error"
|
||||
applies_to_field = CONF_BASE
|
||||
|
||||
def __init__(self, *args: object, **kwargs: dict[str, str]) -> None:
|
||||
super().__init__(*args)
|
||||
self._extra_info = kwargs
|
||||
|
||||
def get_errors_and_placeholders(self, schema):
|
||||
"""Return dicts of errors and description_placeholders, for adding to async_show_form"""
|
||||
key = self.applies_to_field
|
||||
# Errors will only be displayed to the user if the key is actually in the form (or
|
||||
# CONF_BASE for a general error), so we'll check the schema (seems weird there
|
||||
# isn't a more efficient way to do this...)
|
||||
if key not in {k.schema for k in schema}:
|
||||
key = CONF_BASE
|
||||
return ({key : self.error_name}, self._extra_info or {})
|
||||
|
||||
class CannotConnect(KnownError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
error_name = "cannot_connect"
|
||||
|
||||
class InvalidHost(KnownError):
|
||||
"""Error to indicate there is an invalid hostname."""
|
||||
error_name = "cannot_connect"
|
||||
applies_to_field = CONF_HOST
|
||||
|
||||
class HostAlreadyConfigured(KnownError):
|
||||
"""Error to indicate there is an duplicate hostname."""
|
||||
error_name = "host_already_configured"
|
||||
applies_to_field = CONF_HOST
|
||||
|
||||
class InvalidName(KnownError):
|
||||
"""Error to indicate there is an invalid hostname."""
|
||||
error_name = "name_invalid"
|
||||
applies_to_field = CONF_NAME
|
||||
|
||||
class TooManyDevicesRegistered(KnownError):
|
||||
"""Error to indicate that there are too many devices registered"""
|
||||
error_name = "too_many_devices_registered"
|
||||
applies_to_field = CONF_BASE
|
||||
@@ -1,85 +0,0 @@
|
||||
"""Constants used by the koolnova-bms component."""
|
||||
|
||||
from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.components.climate.const import (
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_HEAT,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_AUTO,
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
FAN_AUTO,
|
||||
)
|
||||
|
||||
DOMAIN = "koolnova_bms"
|
||||
DEVICES = "wf-rac-devices"
|
||||
|
||||
CONF_OPERATOR_ID = "operator_id"
|
||||
CONF_AIRCO_ID = "airco_id"
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_CONNECTED_ACCOUNTS = "connected_accounts"
|
||||
|
||||
ATTR_INSIDE_TEMPERATURE = "inside_temperature"
|
||||
|
||||
SENSOR_TYPE_TEMPERATURE = "temperature"
|
||||
|
||||
SENSOR_TYPES = {
|
||||
ATTR_INSIDE_TEMPERATURE: {
|
||||
CONF_NAME: "Inside Temperature",
|
||||
CONF_ICON: "mdi:thermometer",
|
||||
CONF_TYPE: SENSOR_TYPE_TEMPERATURE,
|
||||
},
|
||||
}
|
||||
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
|
||||
SUPPORTED_HVAC_MODES = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
HVACMode.DRY,
|
||||
HVACMode.HEAT,
|
||||
HVACMode.FAN_ONLY,
|
||||
]
|
||||
|
||||
HVAC_TRANSLATION = {
|
||||
HVAC_MODE_AUTO: 0,
|
||||
HVAC_MODE_COOL: 1,
|
||||
HVAC_MODE_HEAT: 2,
|
||||
HVAC_MODE_FAN_ONLY: 3,
|
||||
HVAC_MODE_DRY: 4,
|
||||
}
|
||||
|
||||
FAN_MODE_1 = "1 Lowest"
|
||||
FAN_MODE_2 = "2 Low"
|
||||
FAN_MODE_3 = "3 High"
|
||||
FAN_MODE_4 = "4 Highest"
|
||||
|
||||
FAN_MODE_TRANSLATION = {
|
||||
FAN_AUTO: 0,
|
||||
FAN_MODE_1: 1,
|
||||
FAN_MODE_2: 2,
|
||||
FAN_MODE_3: 3,
|
||||
FAN_MODE_4: 4,
|
||||
}
|
||||
|
||||
SUPPORTED_FAN_MODES = [
|
||||
FAN_AUTO,
|
||||
FAN_MODE_1,
|
||||
FAN_MODE_2,
|
||||
FAN_MODE_3,
|
||||
FAN_MODE_4,
|
||||
]
|
||||
|
||||
OPERATION_LIST = {
|
||||
# HVAC_MODE_OFF: "Off",
|
||||
HVAC_MODE_HEAT: "Heat",
|
||||
HVAC_MODE_COOL: "Cool",
|
||||
HVAC_MODE_AUTO: "Auto",
|
||||
HVAC_MODE_DRY: "Dry",
|
||||
HVAC_MODE_FAN_ONLY: "Fan",
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,115 +0,0 @@
|
||||
# Constants for koolnova Modbus RTU BMS
|
||||
# d'après la documentation officielle A52102 - Registre contrôle Modbus
|
||||
|
||||
from enum import Enum
|
||||
|
||||
# La couche physique est Modbus RTU sur RS485 à 9600, avec 8 bits de données, sans parité
|
||||
# ou même parité et un bit d'arrêt. Par défaut: 9600 8E1
|
||||
# L'adresse Modbus par défaut est 49
|
||||
DEFAULT_ADDR = 49
|
||||
DEFAULT_BAUDRATE = 9600
|
||||
DEFAULT_PARITY = 'E'
|
||||
DEFAULT_STOPBITS = 1
|
||||
DEFAULT_BYTESIZE = 8
|
||||
|
||||
# Nombre de machines (AC1, AC2, AC3 et AC4)
|
||||
NUM_OF_ENGINES = 4
|
||||
# Nombre de registres par zone
|
||||
NUM_REG_PER_ZONE = 4
|
||||
# Nombre max de zone pour un systeme
|
||||
NB_ZONE_MAX = 16
|
||||
|
||||
# Chaque zone climatique est définie par 4 registres et il y a 16 zones possibles,
|
||||
# donc le climat est défini par 64 registres
|
||||
REG_START_ZONE = 0
|
||||
|
||||
# Commandes Modbus prises en charge sont le Read Holding Register (0x03)
|
||||
# et le Write Single Register (0x06)
|
||||
|
||||
REG_LOCK_ZONE = 0 # 40001, 40005, 40009, etc ...
|
||||
REG_STATE_AND_FLOW = 1 # 40002, 40006, 40010, etc ...
|
||||
|
||||
class ZoneState(Enum):
|
||||
STATE_OFF = 0
|
||||
STATE_ON = 1
|
||||
|
||||
class ZoneRegister(Enum):
|
||||
REGISTER_OFF = 0
|
||||
REGISTER_ON = 1
|
||||
|
||||
class ZoneFanMode(Enum):
|
||||
FAN_OFF = 0
|
||||
FAN_LOW = 1
|
||||
FAN_MEDIUM = 2
|
||||
FAN_HIGH = 3
|
||||
FAN_AUTO = 4
|
||||
|
||||
class ZoneClimMode(Enum):
|
||||
COLD = 1
|
||||
HOT = 2
|
||||
HEATING_FLOOR = 4
|
||||
REFRESHING_FLOOR = 5
|
||||
HEATING_FLOOR_2 = 6
|
||||
|
||||
# Température de consigne = (data / 2) => delta: 15°C -> 35°C
|
||||
REG_TEMP_ORDER = 2 # 40003, 40007, 40011, etc ...
|
||||
# Température réelle = (data / 2) => delta: 0°C -> 50°C
|
||||
REG_TEMP_REAL = 3 # 40004, 40008, 40012, etc ...
|
||||
|
||||
# Temperature maximale de consigne : 35°C
|
||||
MAX_TEMP_ORDER = 35.0
|
||||
# Temperature minimale de consigne : 15°C
|
||||
MIN_TEMP_ORDER = 15.0
|
||||
|
||||
# (4 registres: 64 -> 67) Debit des machines (0: arret -> 15: debit maximum)
|
||||
REG_START_FLOW_ENGINE = 64
|
||||
NUM_REG_FLOG_ENGINE = 4
|
||||
FLOW_ENGINE_VAL_MAX = 15
|
||||
FLOW_ENGINE_VAL_MIN = 0
|
||||
|
||||
# (4 registres : 68 -> 71) Température de consigne de la machine.
|
||||
# Valeur décimale de 30 à 60 = double de la temp de consigne des consignes AC1, AC2, AC3 et AC4
|
||||
REG_START_ORDER_TEMP = 68
|
||||
NUM_REG_ORDER_TEMP = 4
|
||||
|
||||
# (4 registres : 72 -> 75) Programmation des débit des machines du système
|
||||
REG_START_FLOW_STATE_ENGINE = 72
|
||||
NUM_REG_FLOW_STATE_ENGINE = 4
|
||||
|
||||
class FlowEngine(Enum):
|
||||
MANUAL_MIN = 1
|
||||
MANUAL_MED = 2
|
||||
MANUAL_HIGH = 3
|
||||
AUTO = 4
|
||||
|
||||
# Communication Modbus
|
||||
REG_COMM = 76
|
||||
|
||||
REG_ADDR_MODBUS = 77 # dispo (1 - 127)
|
||||
REG_EFFICIENCY = 78
|
||||
|
||||
# Point d'equilibre entre efficience/vitesse du système de zone
|
||||
# chiffre élevé = meilleur efficience
|
||||
# chiffre bas = temp réglée atteinte au plus tot
|
||||
class Efficiency(Enum):
|
||||
LOWER_EFF = 1
|
||||
LOW_EFF = 2
|
||||
MED_EFF = 3
|
||||
HIGH_EFF = 4
|
||||
HIGHER_EFF = 5
|
||||
|
||||
REG_CLIM_ID = 79
|
||||
REG_SYS_STATE = 80
|
||||
|
||||
class SysState(Enum):
|
||||
SYS_STATE_OFF = 0
|
||||
SYS_STATE_ON = 1
|
||||
|
||||
REG_GLOBAL_MODE = 81
|
||||
|
||||
class GlobalMode(Enum):
|
||||
COLD = 1
|
||||
HEAT = 2
|
||||
HEATING_FLOOR = 4
|
||||
REFRESHING_FLOOR = 5
|
||||
HEATING_FLOOR_2 = 6
|
||||
@@ -1,414 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author: sinseman44 <vincent.benoit@benserv.fr>
|
||||
# @date: 10/2023
|
||||
# @brief: System, Unit and Zone classes
|
||||
|
||||
###########################################################################
|
||||
# import external modules
|
||||
|
||||
import re, sys, os
|
||||
import logging as log
|
||||
import asyncio
|
||||
|
||||
###########################################################################
|
||||
# import internal modules
|
||||
|
||||
from . import const
|
||||
from .operations import Operations, ModbusConnexionError
|
||||
|
||||
###########################################################################
|
||||
# class & methods
|
||||
|
||||
logger = log.getLogger('koolnova_bms')
|
||||
|
||||
class Koolnova:
|
||||
''' koolnova Device class '''
|
||||
|
||||
def __init__(self,
|
||||
port:str = "",
|
||||
timeout:int = 0,
|
||||
) -> None:
|
||||
''' Class constructor '''
|
||||
self._client = Operations(port=port, timeout=timeout)
|
||||
self._global_mode = const.GlobalMode.COLD
|
||||
self._efficiency = const.Efficiency.LOWER_EFF
|
||||
self._sys_state = const.SysState.SYS_STATE_OFF
|
||||
self._units = []
|
||||
self._zones = []
|
||||
|
||||
async def connect(self) -> bool:
|
||||
''' connect to the modbus serial server '''
|
||||
await self._client.connect()
|
||||
if not self.connected():
|
||||
raise ClientNotConnectedError("Client Modbus connexion error")
|
||||
|
||||
logger.info("Retreive system status ...")
|
||||
ret, self._sys_state = await self._client.system_status()
|
||||
if not ret:
|
||||
logger.error("Error retreiving system status")
|
||||
self._sys_state = const.SysState.SYS_STATE_OFF
|
||||
|
||||
logger.info("Retreive global mode ...")
|
||||
ret, self._global_mode = await self._client.global_mode()
|
||||
if not ret:
|
||||
logger.error("Error retreiving global mode")
|
||||
self._global_mode = const.GlobalMode.COLD
|
||||
|
||||
logger.info("Retreive efficiency ...")
|
||||
ret, self._efficiency = await self._client.efficiency()
|
||||
if not ret:
|
||||
logger.error("Error retreiving efficiency")
|
||||
self._efficiency = const.Efficiency.LOWER_EFF
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
logger.info("Retreive units ...")
|
||||
for idx in range(1, const.NUM_OF_ENGINES + 1):
|
||||
logger.debug("Unit id: {}".format(idx))
|
||||
unit = Unit(unit_id = idx)
|
||||
ret, unit.flow_engine = await self._client.flow_engine(unit_id = idx)
|
||||
ret, unit.flow_state = await self._client.flow_state_engine(unit_id = idx)
|
||||
ret, unit.order_temp = await self._client.order_temp_engine(unit_id = idx)
|
||||
self._units.append(unit)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
return True
|
||||
|
||||
def connected(self) -> bool:
|
||||
''' get modbus client status '''
|
||||
return self._client.connected
|
||||
|
||||
def disconnect(self) -> None:
|
||||
''' close the underlying socket connection '''
|
||||
self._client.disconnect()
|
||||
|
||||
async def discover_zones(self) -> None:
|
||||
''' Set all registered zones for system '''
|
||||
if not self._client.connected:
|
||||
raise ModbusConnexionError('Client Modbus not connected')
|
||||
zones_lst = await self._client.discover_registered_zones()
|
||||
for zone in zones_lst:
|
||||
self._zones.append(Zone(id_zone = zone['id'],
|
||||
state = zone['state'],
|
||||
register = zone['register'],
|
||||
fan_mode = zone['fan'],
|
||||
clim_mode = zone['clim'],
|
||||
real_temp = zone['real_temp'],
|
||||
order_temp = zone['order_temp']
|
||||
))
|
||||
return
|
||||
|
||||
async def add_manual_registered_zone(self,
|
||||
id_zone:int = 0) -> bool:
|
||||
''' Add zone to koolnova System '''
|
||||
if not self._client.connected:
|
||||
raise ModbusConnexionError('Client Modbus not connected')
|
||||
|
||||
ret, zone_dict = await self._client.zone_registered(zone_id = id_zone)
|
||||
if not ret:
|
||||
logger.error("Zone with ID: {} is not registered".format(id_zone))
|
||||
return False
|
||||
for zone in self._zones:
|
||||
if id_zone == zone.id_zone:
|
||||
logger.error('Zone registered with ID: {} is already saved')
|
||||
return False
|
||||
|
||||
self._zones.append(Zone(id_zone = id_zone,
|
||||
state = zone_dict['state'],
|
||||
register = zone_dict['register'],
|
||||
fan_mode = zone_dict['fan'],
|
||||
clim_mode = zone_dict['clim'],
|
||||
real_temp = zone_dict['real_temp'],
|
||||
order_temp = zone_dict['order_temp']
|
||||
))
|
||||
logger.debug("Zones registered: {}".format(self._zones))
|
||||
return True
|
||||
|
||||
def get_zones(self) -> list:
|
||||
''' get zones '''
|
||||
return self._zones
|
||||
|
||||
def get_zone(self, zone_id:int = 0) -> str:
|
||||
''' get specific zone '''
|
||||
return self._zones[zone_id - 1]
|
||||
|
||||
def get_units(self) -> list:
|
||||
''' get units '''
|
||||
return self._units
|
||||
|
||||
@property
|
||||
def global_mode(self) -> const.GlobalMode:
|
||||
''' Get Global Mode '''
|
||||
return self._global_mode
|
||||
|
||||
@global_mode.setter
|
||||
def global_mode(self, val:const.GlobalMode) -> None:
|
||||
''' Set Global Mode '''
|
||||
if not isinstance(val, const.GlobalMode):
|
||||
raise AssertionError('Input variable must be Enum GlobalMode')
|
||||
self._global_mode = val
|
||||
|
||||
@property
|
||||
def efficiency(self) -> const.Efficiency:
|
||||
''' Get Efficiency '''
|
||||
return self._efficiency
|
||||
|
||||
@efficiency.setter
|
||||
def efficiency(self, val:const.Efficiency) -> None:
|
||||
''' Set Efficiency '''
|
||||
if not isinstance(val, const.Efficiency):
|
||||
raise AssertionError('Input variable must be Enum Efficiency')
|
||||
self._efficiency = val
|
||||
|
||||
@property
|
||||
def sys_state(self) -> const.SysState:
|
||||
''' Get System State '''
|
||||
return self.sys_state
|
||||
|
||||
@sys_state.setter
|
||||
def sys_state(self, val:const.SysState) -> None:
|
||||
''' Set System State '''
|
||||
if not isinstance(val, const.SysState):
|
||||
raise AssertionError('Input variable must be Enum SysState')
|
||||
self._sys_state = val
|
||||
|
||||
def __repr__(self) -> str:
|
||||
''' repr method '''
|
||||
return repr('System(Global Mode:{}, Efficiency:{}, State:{})'.format( self._global_mode,self._efficiency,self._sys_state))
|
||||
|
||||
class Unit:
|
||||
''' koolnova Unit class '''
|
||||
|
||||
def __init__(self,
|
||||
unit_id:int = 0,
|
||||
flow_engine:int = 0,
|
||||
flow_state:const.FlowEngine = const.FlowEngine.AUTO,
|
||||
order_temp:float = 0
|
||||
) -> None:
|
||||
''' Constructor class '''
|
||||
self._unit_id = unit_id
|
||||
self._flow_engine = flow_engine
|
||||
self._flow_state = flow_state
|
||||
self._order_temp = order_temp
|
||||
|
||||
@property
|
||||
def unit_id(self) -> int:
|
||||
''' Get Unit ID '''
|
||||
return self._unit_id
|
||||
|
||||
@unit_id.setter
|
||||
def unit_id(self, val:int) -> None:
|
||||
''' Set Unit ID '''
|
||||
if not isinstance(val, int):
|
||||
raise AssertionError('Input variable must be Int')
|
||||
if val > const.NUM_OF_ENGINES:
|
||||
raise NumUnitError('Unit ID must be lower than {}'.format(const.NUM_OF_ENGINES))
|
||||
self._unit_id = val
|
||||
|
||||
@property
|
||||
def flow_engine(self) -> int:
|
||||
''' Get Flow Engine '''
|
||||
return self._flow_engine
|
||||
|
||||
@flow_engine.setter
|
||||
def flow_engine(self, val:int) -> None:
|
||||
''' Set Flow Engine '''
|
||||
if not isinstance(val, int):
|
||||
raise AssertionError('Input variable must be Int')
|
||||
if val > const.FLOW_ENGINE_VAL_MAX or val < const.FLOW_ENGINE_VAL_MIN:
|
||||
raise FlowEngineError('Flow Engine value ({}) must be between {} and {}'.format(val, const.FLOW_ENGINE_VAL_MIN, const.FLOW_ENGINE_VAL_MAX))
|
||||
self._flow_engine = val
|
||||
|
||||
@property
|
||||
def flow_state(self) -> const.FlowEngine:
|
||||
''' Get Flow State '''
|
||||
return self._flow_state
|
||||
|
||||
@flow_state.setter
|
||||
def flow_state(self, val:const.FlowEngine) -> None:
|
||||
''' Set Flow State '''
|
||||
if not isinstance(val, const.FlowEngine):
|
||||
raise AssertionError('Input variable must be Enum FlowEngine')
|
||||
self._flow_state = val
|
||||
|
||||
@property
|
||||
def order_temp(self) -> float:
|
||||
''' Get Order Temp '''
|
||||
return self._order_temp
|
||||
|
||||
@order_temp.setter
|
||||
def order_temp(self, val:float = 0.0) -> None:
|
||||
''' Set Flow Engine '''
|
||||
if not isinstance(val, float):
|
||||
raise AssertionError('Input variable must be Int')
|
||||
if val > 0 and (val > 30.0 or val < 15.0):
|
||||
raise OrderTempError('Flow Engine value ({}) must be between 15 and 30'.format(val))
|
||||
self._flow_engine = val
|
||||
|
||||
def __repr__(self) -> str:
|
||||
''' repr method '''
|
||||
return repr('Unit(Id:{}, Flow Engine:{}, Flow State:{}, Order Temp:{})'.format(self._unit_id,
|
||||
self._flow_engine,
|
||||
self._flow_state,
|
||||
self._order_temp))
|
||||
|
||||
class Zone:
|
||||
''' koolnova Zone class '''
|
||||
|
||||
def __init__(self,
|
||||
id_zone:int = 0,
|
||||
state:const.ZoneState = const.ZoneState.STATE_OFF,
|
||||
register:const.ZoneRegister = const.ZoneRegister.REGISTER_OFF,
|
||||
fan_mode:const.ZoneFanMode = const.ZoneFanMode.FAN_OFF,
|
||||
clim_mode:const.ZoneClimMode = const.ZoneClimMode.COLD,
|
||||
real_temp:float = 0,
|
||||
order_temp:float = 0
|
||||
) -> None:
|
||||
''' Class constructor '''
|
||||
self._id = id_zone
|
||||
self._state = state
|
||||
self._register = register
|
||||
self._fan_mode = fan_mode
|
||||
self._clim_mode = clim_mode
|
||||
self._real_temp = real_temp
|
||||
self._order_temp = order_temp
|
||||
|
||||
@property
|
||||
def id_zone(self) -> int:
|
||||
''' Get Zone Id '''
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def state(self) -> const.ZoneState:
|
||||
''' Get state '''
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, val:const.ZoneState) -> None:
|
||||
''' Set state '''
|
||||
if not isinstance(val, const.ZoneState):
|
||||
raise AssertionError('Input variable must be Enum ZoneState')
|
||||
self._state = val
|
||||
|
||||
@property
|
||||
def register(self) -> const.ZoneRegister:
|
||||
''' Get register state '''
|
||||
return self._register
|
||||
|
||||
@register.setter
|
||||
def register(self, val:const.ZoneRegister) -> None:
|
||||
''' Set register state '''
|
||||
if not isinstance(val, const.ZoneRegister):
|
||||
raise AssertionError('Input variable must be Enum ZoneRegister')
|
||||
self._register = val
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> const.ZoneFanMode:
|
||||
''' Get Fan Mode '''
|
||||
return self._fan_mode
|
||||
|
||||
@fan_mode.setter
|
||||
def fan_mode(self, val:const.ZoneFanMode) -> None:
|
||||
''' Set Fan Mode '''
|
||||
if not isinstance(val, const.ZoneFanMode):
|
||||
raise AssertionError('Input variable must be Enum ZoneFanMode')
|
||||
self._fan_mode = val
|
||||
|
||||
@property
|
||||
def clim_mode(self) -> const.ZoneClimMode:
|
||||
''' Get Clim Mode '''
|
||||
return self._clim_mode
|
||||
|
||||
@clim_mode.setter
|
||||
def clim_mode(self, val:const.ZoneClimMode) -> None:
|
||||
''' Set Clim Mode '''
|
||||
if not isinstance(val, const.ZoneClimMode):
|
||||
raise AssertionError('Input variable must be Enum ZoneClimMode')
|
||||
self._clim_mode = val
|
||||
|
||||
@property
|
||||
def real_temp(self) -> float:
|
||||
''' Get real temp '''
|
||||
return self._real_temp
|
||||
|
||||
@real_temp.setter
|
||||
def real_temp(self, val:float) -> None:
|
||||
''' Set Real Temp '''
|
||||
if not isinstance(val, float):
|
||||
raise AssertionError('Input variable must be Float')
|
||||
self._real_temp = val
|
||||
|
||||
@property
|
||||
def order_temp(self) -> float:
|
||||
''' Get order temp '''
|
||||
return self._order_temp
|
||||
|
||||
@order_temp.setter
|
||||
def order_temp(self, val:float) -> None:
|
||||
''' Set Order Temp '''
|
||||
if not isinstance(val, float):
|
||||
raise AssertionError('Input variable must be float')
|
||||
if val > const.MAX_TEMP_ORDER or val < const.MIN_TEMP_ORDER:
|
||||
raise OrderTempError('Order temp value must be between {} and {}'.format(const.MIN_TEMP_ORDER, const.MAX_TEMP_ORDER))
|
||||
self._order_temp = val
|
||||
|
||||
def __repr__(self) -> str:
|
||||
''' repr method '''
|
||||
return repr('Zone(Id:{}, State:{}, Register:{}, Fan:{}, Clim:{}, Real Temp:{}, Order Temp:{})'.format(self._id,
|
||||
self._state,
|
||||
self._register,
|
||||
self._fan_mode,
|
||||
self._clim_mode,
|
||||
self._real_temp,
|
||||
self._order_temp))
|
||||
|
||||
class NumUnitError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class FlowEngineError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class OrderTempError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class ClientNotConnectedError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
# @author: sinseman44 <vincent.benoit@benserv.fr>
|
||||
# @date: 10/2023
|
||||
# @brief: Communicate with Koolnova BMS Modbus RTU
|
||||
|
||||
###########################################################################
|
||||
# import external modules
|
||||
|
||||
import re, sys, os
|
||||
import logging as log
|
||||
|
||||
import asyncio
|
||||
|
||||
from pymodbus import pymodbus_apply_logging_config
|
||||
from pymodbus.client import AsyncModbusSerialClient as ModbusClient
|
||||
from pymodbus.exceptions import ModbusException
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
from pymodbus.transaction import ModbusRtuFramer
|
||||
|
||||
###########################################################################
|
||||
# import internal modules
|
||||
|
||||
from . import const
|
||||
|
||||
###########################################################################
|
||||
# class & methods
|
||||
|
||||
logger = log.getLogger('koolnova_bms')
|
||||
|
||||
class Operations:
|
||||
''' koolnova BMS Modbus operations class '''
|
||||
|
||||
def __init__(self, port:str, timeout:int) -> None:
|
||||
''' Class constructor '''
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
self._client = ModbusClient(port=self._port,
|
||||
baudrate=const.DEFAULT_BAUDRATE,
|
||||
parity=const.DEFAULT_PARITY,
|
||||
stopbits=const.DEFAULT_STOPBITS,
|
||||
bytesize=const.DEFAULT_BYTESIZE,
|
||||
timeout=self._timeout)
|
||||
pymodbus_apply_logging_config("DEBUG")
|
||||
|
||||
async def __read_register(self, reg:int) -> (int, bool):
|
||||
''' Read one holding register (code 0x03) '''
|
||||
ret = True
|
||||
rr = None
|
||||
if not self._client.connected:
|
||||
raise ModbusConnexionError('Client Modbus not connected')
|
||||
try:
|
||||
rr = await self._client.read_holding_registers(address=reg, count=1, slave=const.DEFAULT_ADDR)
|
||||
if rr.isError():
|
||||
ret = False
|
||||
except ModbusException as e:
|
||||
logger.error("{}".format(e))
|
||||
ret = False
|
||||
|
||||
if isinstance(rr, ExceptionResponse):
|
||||
logger.error("Received modbus exception ({})".format(rr))
|
||||
ret = False
|
||||
return rr.registers[0], ret
|
||||
|
||||
async def __read_registers(self, start_reg:int, count:int) -> (int, bool):
|
||||
''' Read holding registers (code 0x03) '''
|
||||
ret = True
|
||||
rr = None
|
||||
if not self._client.connected:
|
||||
raise ModbusConnexionError('Client Modbus not connected')
|
||||
try:
|
||||
rr = await self._client.read_holding_registers(address=start_reg, count=count, slave=const.DEFAULT_ADDR)
|
||||
if rr.isError():
|
||||
ret = False
|
||||
except ModbusException as e:
|
||||
logger.error("{}".format(e))
|
||||
ret = False
|
||||
|
||||
if isinstance(rr, ExceptionResponse):
|
||||
logger.error("Received modbus exception ({})".format(rr))
|
||||
ret = False
|
||||
return rr.registers, ret
|
||||
|
||||
async def __write_register(self, reg:int, val:int) -> bool:
|
||||
''' Write one register (code 0x06) '''
|
||||
rq = None
|
||||
ret = True
|
||||
if not self._client.connected:
|
||||
raise ModbusConnexionError('Client Modbus not connected')
|
||||
try:
|
||||
rq = await self._client.write_register(address=reg, value=val, slave=const.DEFAULT_ADDR)
|
||||
if rq.isError():
|
||||
logger.error("Write error: {}".format(rq))
|
||||
ret = False
|
||||
except ModbusException as e:
|
||||
logger.error("{}".format(e))
|
||||
ret = False
|
||||
|
||||
if isinstance(rq, ExceptionResponse):
|
||||
logger.error("Received modbus exception ({})".format(rr))
|
||||
ret = False
|
||||
return ret
|
||||
|
||||
async def connect(self) -> None:
|
||||
''' connect to the modbus serial server '''
|
||||
await self._client.connect()
|
||||
|
||||
def connected(self) -> bool:
|
||||
''' get modbus client status '''
|
||||
return self._client.connected
|
||||
|
||||
def disconnect(self) -> None:
|
||||
''' close the underlying socket connection '''
|
||||
self._client.close()
|
||||
|
||||
async def discover_registered_zones(self) -> list:
|
||||
''' Discover all zones registered to the system '''
|
||||
regs, ret = await self.__read_registers(start_reg=const.REG_START_ZONE, count=const.NB_ZONE_MAX * const.NUM_REG_PER_ZONE)
|
||||
if not ret:
|
||||
raise ReadRegistersError("Read holding regsiter error")
|
||||
zones_lst = []
|
||||
zone_dict = {}
|
||||
jdx = 1
|
||||
flag = False
|
||||
for idx, reg in enumerate(regs):
|
||||
if idx % const.NUM_REG_PER_ZONE == 0:
|
||||
zone_dict = {}
|
||||
if const.ZoneRegister(reg >> 1) == const.ZoneRegister.REGISTER_ON:
|
||||
zone_dict['id'] = jdx
|
||||
zone_dict['state'] = const.ZoneState(reg & 0b01)
|
||||
zone_dict['register'] = const.ZoneRegister(reg >> 1)
|
||||
flag = True
|
||||
elif idx % const.NUM_REG_PER_ZONE == 1 and flag:
|
||||
zone_dict['fan'] = const.ZoneFanMode((reg & 0xF0) >> 4)
|
||||
zone_dict['clim'] = const.ZoneClimMode(reg & 0x0F)
|
||||
elif idx % const.NUM_REG_PER_ZONE == 2 and flag:
|
||||
zone_dict['order_temp'] = reg/2
|
||||
elif idx % const.NUM_REG_PER_ZONE == 3 and flag:
|
||||
zone_dict['real_temp'] = reg/2
|
||||
jdx += 1
|
||||
flag = False
|
||||
zones_lst.append(zone_dict)
|
||||
return zones_lst
|
||||
|
||||
async def zone_registered(self, zone_id:int = 0) -> (bool, dict):
|
||||
''' Get Zone Status from Id '''
|
||||
if zone_id > const.NB_ZONE_MAX or zone_id == 0:
|
||||
raise ZoneIdError('Zone Id must be between 1 to 16')
|
||||
zone_dict = {}
|
||||
regs, ret = await self.__read_registers(start_reg = const.REG_START_ZONE + (4 * (zone_id - 1)), count = const.NUM_REG_PER_ZONE)
|
||||
if not ret:
|
||||
raise ReadRegistersError("Read holding regsiter error")
|
||||
if const.ZoneRegister(regs[0] >> 1) == const.ZoneRegister.REGISTER_OFF:
|
||||
return False, {}
|
||||
|
||||
zone_dict['state'] = const.ZoneState(regs[0] & 0b01)
|
||||
zone_dict['register'] = const.ZoneRegister(regs[0] >> 1)
|
||||
zone_dict['fan'] = const.ZoneFanMode((regs[1] & 0xF0) >> 4)
|
||||
zone_dict['clim'] = const.ZoneClimMode(regs[1] & 0x0F)
|
||||
zone_dict['order_temp'] = regs[2]/2
|
||||
zone_dict['real_temp'] = regs[3]/2
|
||||
return True, zone_dict
|
||||
|
||||
async def system_status(self) -> (bool, const.SysState):
|
||||
''' Read system status register '''
|
||||
reg, ret = await self.__read_register(const.REG_SYS_STATE)
|
||||
if not ret:
|
||||
logger.error('Error retreive system status')
|
||||
reg = 0
|
||||
return ret, const.SysState(reg)
|
||||
|
||||
async def global_mode(self) -> (bool, const.GlobalMode):
|
||||
''' Read global mode '''
|
||||
reg, ret = await self.__read_register(const.REG_GLOBAL_MODE)
|
||||
if not ret:
|
||||
logger.error('Error retreive global mode')
|
||||
reg = 0
|
||||
return ret, const.GlobalMode(reg)
|
||||
|
||||
async def efficiency(self) -> (bool, const.Efficiency):
|
||||
''' read efficiency/speed '''
|
||||
reg, ret = await self.__read_register(const.REG_EFFICIENCY)
|
||||
if not ret:
|
||||
logger.error('Error retreive efficiency')
|
||||
reg = 0
|
||||
return ret, const.Efficiency(reg)
|
||||
|
||||
async def flow_engines(self) -> (bool, list):
|
||||
''' read flow engines AC1, AC2, AC3, AC4 '''
|
||||
engines_lst = []
|
||||
regs, ret = await self.__read_registers(const.REG_START_FLOW_ENGINE, const.NUM_OF_ENGINES)
|
||||
if ret:
|
||||
for idx, reg in enumerate(regs):
|
||||
engines_lst.append(const.FlowEngine(reg))
|
||||
else:
|
||||
logger.error('Error retreive flow engines')
|
||||
return ret, engines_lst
|
||||
|
||||
async def flow_engine(self, unit_id:int = 0) -> (bool, int):
|
||||
''' read flow unit specified by unit id '''
|
||||
if unit_id < 1 or unit_id > 4:
|
||||
raise UnitIdError("Unit Id must be between 1 and 4")
|
||||
reg, ret = await self.__read_register(const.REG_START_FLOW_ENGINE + (unit_id - 1))
|
||||
if not ret:
|
||||
logger.error('Error retreive flow engine for id:{}'.format(unit_id))
|
||||
reg = 0
|
||||
return ret, reg
|
||||
|
||||
async def flow_state_engine(self, unit_id:int = 0) -> (bool, const.FlowEngine):
|
||||
if unit_id < 1 or unit_id > 4:
|
||||
raise UnitIdError("Unit Id must be between 1 and 4")
|
||||
reg, ret = await self.__read_register(const.REG_START_FLOW_STATE_ENGINE + (unit_id - 1))
|
||||
if not ret:
|
||||
logger.error('Error retreive flow state for id:{}'.format(unit_id))
|
||||
reg = 0
|
||||
return ret, const.FlowEngine(reg)
|
||||
|
||||
async def order_temp_engine(self, unit_id:int = 0) -> (bool, float):
|
||||
if unit_id < 1 or unit_id > 4:
|
||||
raise UnitIdError("Unit Id must be between 1 and 4")
|
||||
reg, ret = await self.__read_register(const.REG_START_ORDER_TEMP + (unit_id - 1))
|
||||
if not ret:
|
||||
logger.error('Error retreive order temp for id:{}'.format(unit_id))
|
||||
reg = 0
|
||||
return ret, reg / 2
|
||||
|
||||
async def orders_temp(self) -> (bool, list):
|
||||
''' read orders temperature AC1, AC2, AC3, AC4 '''
|
||||
engines_lst = []
|
||||
regs, ret = await self.__read_registers(const.REG_START_ORDER_TEMP, const.NUM_OF_ENGINES)
|
||||
if ret:
|
||||
for idx, reg in enumerate(regs):
|
||||
engines_lst.append(reg/2)
|
||||
else:
|
||||
logger.error('error reading flow engines registers')
|
||||
return ret, engines_lst
|
||||
|
||||
async def set_zone_order_temp(self, zone_id:int = 0, val:float = 0.0) -> bool:
|
||||
''' Set zone order temp '''
|
||||
if zone_id > const.NB_ZONE_MAX or zone_id == 0:
|
||||
raise ZoneIdError('Zone Id must be between 1 to 16')
|
||||
if val > const.MAX_TEMP_ORDER or val < const.MIN_TEMP_ORDER:
|
||||
logger.error('Order Temperature must be between {} and {}'.format(const.MIN_TEMP_ORDER, const.MAX_TEMP_ORDER))
|
||||
return False
|
||||
ret = await self.__write_register(reg = const.REG_START_ZONE + (4 * (zone_id - 1)) + const.REG_TEMP_ORDER, val = int(val * 2))
|
||||
if not ret:
|
||||
logger.error('Error writing zone order temperature')
|
||||
|
||||
return ret
|
||||
|
||||
@property
|
||||
def port(self) -> str:
|
||||
''' Get Port '''
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def timeout(self) -> int:
|
||||
''' Get Timeout '''
|
||||
return self._timeout
|
||||
|
||||
class ModbusConnexionError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class ReadRegistersError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class ZoneIdError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
|
||||
class UnitIdError(Exception):
|
||||
''' user defined exception '''
|
||||
|
||||
def __init__(self,
|
||||
msg:str = "") -> None:
|
||||
''' Class Constructor '''
|
||||
self._msg = msg
|
||||
|
||||
def __str__(self):
|
||||
''' print the message '''
|
||||
return self._msg
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"domain": "koolnova_bms",
|
||||
"name": "koolnova BMS Modbus RTU",
|
||||
"codeowners": ["@vbenoit"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://git.nas.benserv.fr/vincent/koolnova-BMS-Integration/src/branch/main/README.md",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://git.nas.benserv.fr/vincent/koolnova-BMS-Integration/issues",
|
||||
"integration_type": "hub",
|
||||
"requirements": [
|
||||
"pymodbus>=3.5.4",
|
||||
"pyserial>=3.5"
|
||||
],
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
""" for sensor integration. """
|
||||
# pylint: disable = too-few-public-methods
|
||||
|
||||
from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
CONF_HOST,
|
||||
CONF_ERROR,
|
||||
)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
|
||||
from .koolnova.device import Koolnova
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
ATTR_INSIDE_TEMPERATURE,
|
||||
CONF_OPERATOR_ID,
|
||||
CONF_AIRCO_ID,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_CONNECTED_ACCOUNTS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Setup sensor entries"""
|
||||
|
||||
for device in hass.data[DOMAIN]:
|
||||
if device.host == entry.data[CONF_HOST]:
|
||||
_LOGGER.info("Setup: %s, %s", device.name, device.airco_id)
|
||||
entities = [
|
||||
TemperatureSensor(device, "Indoor", ATTR_INSIDE_TEMPERATURE),
|
||||
DiagnosticsSensor(device, "Airco ID", CONF_AIRCO_ID),
|
||||
DiagnosticsSensor(device, "Operator ID", CONF_OPERATOR_ID, True),
|
||||
DiagnosticsSensor(device, "Device ID", ATTR_DEVICE_ID, True),
|
||||
DiagnosticsSensor(device, "IP", CONF_HOST, True),
|
||||
DiagnosticsSensor(device, "Accounts", ATTR_CONNECTED_ACCOUNTS, True),
|
||||
DiagnosticsSensor(device, "Error", CONF_ERROR),
|
||||
]
|
||||
if device.airco.Electric is not None:
|
||||
entities.append(EnergySensor(device))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
class DiagnosticsSensor(SensorEntity):
|
||||
# pylint: disable = too-many-instance-attributes
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
_attr_entity_category: EntityCategory | None = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(self, device: Device, name: str, custom_type: str, enable=False) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._device = device
|
||||
self._attr_name = f"{device.name} {name}"
|
||||
self._attr_entity_registry_enabled_default = enable
|
||||
self._custom_type = custom_type
|
||||
self._attr_device_info = device.device_info
|
||||
self._attr_native_unit_of_measurement = (
|
||||
"Accounts" if custom_type == ATTR_CONNECTED_ACCOUNTS else None
|
||||
)
|
||||
self._attr_icon = (
|
||||
"mdi:account-group" if custom_type == ATTR_CONNECTED_ACCOUNTS else None
|
||||
)
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}-{self._device.airco_id}-{self._custom_type}-sensor"
|
||||
)
|
||||
self._update_state()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
if self._custom_type == CONF_OPERATOR_ID:
|
||||
self._attr_native_value = self._device.operator_id
|
||||
elif self._custom_type == CONF_AIRCO_ID:
|
||||
self._attr_native_value = self._device.airco_id
|
||||
elif self._custom_type == CONF_HOST:
|
||||
self._attr_native_value = self._device.host
|
||||
elif self._custom_type == ATTR_DEVICE_ID:
|
||||
self._attr_native_value = self._device.device_id
|
||||
elif self._custom_type == ATTR_CONNECTED_ACCOUNTS:
|
||||
self._attr_native_value = self._device.num_accounts
|
||||
elif self._custom_type == CONF_ERROR:
|
||||
self._attr_native_value = self._device.airco.ErrorCode
|
||||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._update_state()
|
||||
|
||||
class TemperatureSensor(SensorEntity):
|
||||
"""Representation of a Sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = TEMP_CELSIUS
|
||||
_attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, device: Device, name: str, custom_type: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._device = device
|
||||
self._custom_type = custom_type
|
||||
self._attr_name = f"{device.name} {name}"
|
||||
self._attr_device_info = device.device_info
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}-{self._device.airco_id}-{self._custom_type}-sensor"
|
||||
)
|
||||
self._update_state()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
if self._custom_type == ATTR_INSIDE_TEMPERATURE:
|
||||
self._attr_native_value = self._device.airco.IndoorTemp
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._update_state()
|
||||
Reference in New Issue
Block a user