From 67b1181ee7a6a7ecf4f74f6041d6c49ff2a20d1b Mon Sep 17 00:00:00 2001 From: vbenoit Date: Wed, 1 Nov 2023 09:06:26 +0100 Subject: [PATCH] add koolnova modbus api and operations classes --- .../koolnova_bms/koolnova/__init__.py | 0 .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 148 bytes .../koolnova/__pycache__/const.cpython-39.pyc | Bin 0 -> 2729 bytes .../__pycache__/device.cpython-39.pyc | Bin 0 -> 13205 bytes .../__pycache__/operations.cpython-39.pyc | Bin 0 -> 10652 bytes .../koolnova_bms/koolnova/app.py | 0 .../koolnova_bms/koolnova/const.py | 115 +++++ .../koolnova_bms/koolnova/device.py | 414 ++++++++++++++++++ .../koolnova_bms/koolnova/operations.py | 308 +++++++++++++ 9 files changed, 837 insertions(+) create mode 100644 custom_components/koolnova_bms/koolnova/__init__.py create mode 100644 custom_components/koolnova_bms/koolnova/__pycache__/__init__.cpython-39.pyc create mode 100644 custom_components/koolnova_bms/koolnova/__pycache__/const.cpython-39.pyc create mode 100644 custom_components/koolnova_bms/koolnova/__pycache__/device.cpython-39.pyc create mode 100644 custom_components/koolnova_bms/koolnova/__pycache__/operations.cpython-39.pyc create mode 100644 custom_components/koolnova_bms/koolnova/app.py create mode 100644 custom_components/koolnova_bms/koolnova/const.py create mode 100644 custom_components/koolnova_bms/koolnova/device.py create mode 100644 custom_components/koolnova_bms/koolnova/operations.py diff --git a/custom_components/koolnova_bms/koolnova/__init__.py b/custom_components/koolnova_bms/koolnova/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/koolnova_bms/koolnova/__pycache__/__init__.cpython-39.pyc b/custom_components/koolnova_bms/koolnova/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ff7911b5767656bf01ce166d3c3d9e53fa10ed0 GIT binary patch literal 148 zcmYe~<>g`k0+StPsUZ3>h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vKKO;XkRlh7L zH7`H2L_a*gD7&~IF*#K~J3l`sFTX4?J}I{t&e4yL&&!R?NA_|fEXO_y) zi=qU*B=rjPvZz3>NPPqPhNwcXO1&zs&=s*sn_`Q$#5Qe<9ooS@SH)GjhCQx}YqSgf zhPX~Q@xF!myW$4s-;ncfi<@*u+@ibUHq~IeBksa>SK9808r>K7=mGHi;sNjn5`QQj z(j#Dx#A9HOCH7c6X)AkA6OPZGI?=4*yFu~|l-|cjiZW5H1Tj()of*e1se+w?KKb#O z1JHOIEK*2?2N|Ivk{LuYiygGdI*o==`>cBw#XLODfw_CRi5`Z-?jQ=oNg)geBk``! zaw!adf7k7&7FuryMqq_rqe1$)=>Dz5WnPEbf8XuD9u1=X_b;O1sCTyid=$SKzwMqz z`)@|0{&4iZ8@?EfFO>b$(QthB?Cpnm9g}OIG5e;gnktd6@~_1H`O&N17#@D&V$9_j zqY{<41V$y_YTE6<@})z?ndZ(sN~H?V*#LV8nw^o6)n}~1x|jo0vxaKUp_T#JhC zp%g;L_?0SbLVo-pHI`X#dG()Cb@SYe&zs?z+-;uAZ;)*JvK_|UEYf8H%Q+4`-?V&^ z%Shd~j22a}RXAs^;M&gXJF`>p7Uzejt?AhTHZQ=r5#cY~0^r8h0*Jn_JzG_tO72LT z(?#d7o3oK}p|oJr5Yp~+!h%UQ4AOaW-E$n;V2N`XI-YshX{TYq9k%PX<#M_56o`6B~q!w9n7;lTA8W>C7aBUQ)ip%~1)J`~q?-3!CPKz@Ll6{UtM zsN5V5`-9=FPW$?{+oe%PpDYUJ@VaL(vdj=wKoO&5iD#uPv%(hHPb^+(ww054QTW6V zMNztEw+dmYEGj4!0ZDCDQA4Q|mZn4F$2-n);IQpH4>j`$AVNVK&VHe~A z!_EUMf_);cC)h=XeNuddVVC3*!!CxfPsR0meNT&LfYGwBu_w<3h1Wvb`U_4(X6K^3 z)$2;9+wZup6J!)^S+ijU_RdgwLEh_a%i*pUbphYK z9TYELd28irdu4SsD7R%&Us%8Gu6rF%2G#Z7SZ}XiJAZAZeFadZwtL%cyWEs(?+$vN z8|2#la3EbGk}LZg34#wYY8eS`b~Rt z`$t6az7eg1$0;$)yeI>&1yNqe+%+#|zWqCQOtdbF%Kp-xdEa1|Ww2)TW8;oRtK2%O zOL{C`sntHpEyq$?j&sY2l$MidarU3sWBMJ$@V);OHL&)S|Fi?99>}iectgj#Asuig zgrDO|)$8=>=ay7X(?5QJ8QamaQFkKY=tz=|{_<7nDcQRv9dM;5w;f_k40Gnp8UF+e zPzUFfAbNw%aTtFw?yC<6p&+^Ev5B4|5dK2kFF7yV?dU|e1YlD$;}2i)XX5UBc&Mi` z(dw7q1gASa@uKtL9q5QWHMe`>TH{GoMSGABDHasNj1<9wCQozdUC`u|f{_Sv{o&@O zRDs$?Mt~6WL0OdDT7+`&DS9{+AGaVYt5DQAS>kc-OV7kF;v?fFR*FFL3yJ; ze7`LRo4tW-O>=?8OIQ_@lB(A9PzkBp*y)|PIjK?$giEfovm*zhVXGx7T3~>{>WL3p zHc^xXl1`2i`X`Yz@VYM|$(VJcU=+-fF>lx;WKwf=)Xo`ov@i2t#h5YudMt{<-5C=> zB#!81%}_vwj}KV}hQI7=N-yGnEyp7^hOnotR}KHAvC7;UO0m$9I&4*{$y$L+S)zJ}bhUf()(R0Ik^@|wt zAdX*0-8$X~$JrRiq~jQpq{c|tv&6oEj3$>`_n=HkLNn49%SrRq45YFF8TaxXsM#&Z zZZ8M9WG!Wo+MH%N&2gINw7_X`38vo1EP3C1ij{5|l4%{w?DYGP9Gg7|6(xnk`pp5` zMl$5#(Q&~v7{;WLq8XSyfuq1mvJeduSR0)|U}J~3gQAk1KC`iU9?N@>5Ar?1T2#G& zvib&+pa6KSv5VA+vx>|L?$gL8yrIYrY%MW0AvfF6lv0oBszvJcRT|W8K~*;_=A~AV z*VXR3IE$1$uyH{x9+3gx!T>HQ^o)s9f)h|MYDO;kdsr&?3*&oB*l=I)%FF1ewKMZ7 zjf00!CzZ{6{a47O?e&(mX9)9S(sb6V8E7XHTFZ3LvBtCab9>Neq|-!po%-j&4JYo{ zFuv@q!c9}HG8}7N4fEbALG2g45@0MrJYfmq2}^({kd`A;F2t)$@HklmjrQb49R{U=+UV~!z>>7GK8u}CB@LLk@iYTJ)Q^eE#M3NwcPm6-JoA9YO3j7ep(5Q+6zCYMM7QSe?-9Dp*P}8xZU*~b1jHo!Qx_6<7pQ+EkHPH5VrGeOG5uM_ z#odwJ9?W(YMGjjGY88DjFslU~{Qz|f(eyu{!9(N{n>lLzTr?pa>N*XBb#0oo*EC_U z-yNAY(MvQfH$URM)6OLb~==NVh(UDKU|d!UsMIRUoz(fnyv7o*tu&#Vo0c0K1-2d3=xK1|zP z*K@8*XJxRv9j=3x(QIUcQqS!n63`iRrMAR~1A^Ll7X~mz1=yJ5#Xbo4`85C>I7PhX zKSaGtJe;vAkjsxq+d9*BCHDJ>v~1d?k@gu3{wFjIQ_S)cHJ%uT-N&P5ovAs3&!-bJ z0R%DCW#Fc%$cE|bROH1pDK~yL6fNe@@P2-p%`nYE$-YiH9#jj8FtN2;oOUP_nP`7b z(}lE4jziq=Bhl%> zwA?3~jqEe>HtqY?T{bo!R-ue`l(Q|p8$;;Nvl{WZ6bQcE{!3Z_Z-zM*M$oO0s$`{e@CQeN6h-uNl*Slzwq?7GU)Ba}0w7_H~#B>~tuHCLq&T5uwxylECi6;#8NY z{3<1DR9VA^nLei$*dcI0S()Ky9np647pJb5%w$vuapVAvKc z|Nk=${`*O#QhTOm<8M0iQ`4}2T3N2yGbo$JjBOipxr4uoM(g4od#LgpUiZgH#-fs@ zFr~ef-O34b!a4~w#dXk#AHNQ%i)w6HA8TNTv7Ec?V1<+wJ2 zqxdYrRb_Pqt`^spXciukBbtD$$r>Q3R*yMNkxs5J0UMf|Vz^W@C8xG3Vv%vA*^i(; z%k_1Jn->d+C{7Ctx_U|dJQnO9N$kHjp)JEuVi-uwDpD)z3?*kNA^Tq|qAYUJLwVv( zK10Mguv0T_cr-|1u_871D3l156dDN#0DyILkWdFNn++ShN~#tnC-`&3&qlB>@v2cs zeCWu8OT#i@C*~wV>u>P7bW}2VZIAg?T5}waQxv#+L}gTE!OA3CyGWYo+vuArO$oWb z#~Vr03eOp?RQ(!F$wwO*dMIz>Wnz*kuC+RJ`b_Ll8XjV^H*Iy=!wtfIj!O3vZ4cf9 znQgzyLN-G2;qoE!b#37J$0Mjn{UgeOkd}9&Gl0?EY!tM6yd23PbIV3@)xw}oh zwf1L=n@N!-jENNgh}SJ4$yg=m{t^u3MR5*11rng%HD*>;z2g-Bg9H&-UX-%IQ+!^kQ1~E54FuTinZ!s zeE<2;D%f|s(m4es5gTCFrT4y+M1H3;5WEY;v|R{hR}mHuqgL&2oxj|E{ruZ0m21sl zil)@08`&+UVA^gG1^<~SnC#XZVoi^@TiROVMMidqYGbt2%_8CQLz;?Dg5^&oo8^+x zGO<_0e|$oSr9RW{)t1@_E}w3xC1e^iH-b{qpicp>CyjKpo`0qF5KnypBlikL4xMX= z9Fj-(F2+bD8;Rq8$2(58L_Yo-+W48EUo2t1iS{&J7bfJ(3-Il%o3{HLer@pk3-Btk z`v_HxNE_%u@OfT!F%WY$>yAx5@5|@H%$akK%Y3JVi~{sZp_3ojxJG{M4oz%Vjanp3DK_5=$f5i>9Uw$$p9qSdR$MiMILur%rNW`d4yq$A?$2iPq1Z% z$d3dk(-x6pSlZxBj)!GRBY{4{uM(Xe!@OiNuL-hqwh6H`(Lfl_)BK_8 ztni#+fPO;I0ZD0WYjD_Hw7W+~<|^@A+%MgbC7Hu}OM>My)pVH+ zR*i=L^daUd2c~FV7O>+Ax)|4M{mct#(rOND8ZE@3g?w)PJjZr_6#h+UHl$h7_J?jq zo*ki?=80&43B~;RC^p9wiv|m^@lSmg>8^&CMdQ?C&WV~d0eMwv1f*4+{z3$-F#%WM zSVZB`ox?~+r|)YMq>Ik;W0J1Mq>J(JpZzSN0W^$+Cc(R)OdJ=sXp-@2(nvG*SJG||fX73$FvvX*8WrSumKBBSUdrYEDG0_rCKAY7Dc9Lw&YEhG>8?PvhZnUC{ zk3lyQ^bTFZlW0>IEcKYagof89=oTx9!|A3iMhd)-yXeqS_(BKT6W$C{?JD8HCcS^` z5MF~J+3^qw(G1468Amr6>onRB)^8-VA&2@AMZR{=*j3QPQ8x}F>q@YlJjY%u@4`tX;9vG?OjKq zjl&)5gy1BFgBl#^_O+GQ-)LXCdU53{%4^M$vbqT%ZN|S#xwQQu(!E2ZBO_J^Jm&B@ z#>5^`GgIsoGU@&Ylr`Vv;Y6T6rCrOysd>zvW?U7=?bz(Zd}U7rldG z{7Ikks9d?aa%pY-8omX@SAJ@js`9Jnn_Rku5`O}e>fj{y9eX*X#!@ZSPxp7uqZ=;p zGj0nR?e9c(A3UA^Ca;GbG^FeS{a0m%e4c{=>B%ztM+jVNKZ(umE~XfJ$aO!5*L@Dj zm_Gy`hrJs!^lsqAh{C;0E61UzlDdu#4;P(X+}AlmZXkjt9Qi?4?$GU>!IAneh0k{T4E$V$%Tl_*CV6h#MpG6 zI56mO(2jjz0E-=k+Y9}To^;*LrgYRtG(!rD;zLxp3%Ff*Kc3$7=MGHI0MHxcs65E0 zvMI9)FXJxFlvtXa*LAF)V+9g>@VO`?Bf&`|(t!KR+?mqmiAc@!P3nr`!30 zpCP~haZC%#hk&E6)+fGR{j%x*r}3PofAa7-A8jLuK7T|5P6!@qX&ebmC2{L}+YQX2 z8|3LbTzozmSjebniMAw0IC9u3(Y3Dr?+^ZtpFiW{?`JOa*SqRT0;NDIhw)f<1;v#} zy|AQHET8zS(EKAJCO)!3;U#@b{c;%)`~lx0tlC1hV$`!Gv-aK0vD)R@pkATB{|90u B>Er+a literal 0 HcmV?d00001 diff --git a/custom_components/koolnova_bms/koolnova/__pycache__/operations.cpython-39.pyc b/custom_components/koolnova_bms/koolnova/__pycache__/operations.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c261a84c1b2cb94027e54eb6d7ad5d962536cf6 GIT binary patch literal 10652 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 literal 0 HcmV?d00001 diff --git a/custom_components/koolnova_bms/koolnova/app.py b/custom_components/koolnova_bms/koolnova/app.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/koolnova_bms/koolnova/const.py b/custom_components/koolnova_bms/koolnova/const.py new file mode 100644 index 0000000..371ec9e --- /dev/null +++ b/custom_components/koolnova_bms/koolnova/const.py @@ -0,0 +1,115 @@ +# 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 diff --git a/custom_components/koolnova_bms/koolnova/device.py b/custom_components/koolnova_bms/koolnova/device.py new file mode 100644 index 0000000..4f6c90d --- /dev/null +++ b/custom_components/koolnova_bms/koolnova/device.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8 -*- + +# @author: sinseman44 +# @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 + diff --git a/custom_components/koolnova_bms/koolnova/operations.py b/custom_components/koolnova_bms/koolnova/operations.py new file mode 100644 index 0000000..1e6267b --- /dev/null +++ b/custom_components/koolnova_bms/koolnova/operations.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# -*- encoding: utf-8 -*- + +# @author: sinseman44 +# @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