# Heatzy python plugin for Domoticz # # Author: fjumelle # #pylint: disable=line-too-long,broad-exception-caught,possibly-used-before-assignment """

Heatzy Pilote


Implementation of Heatzy Pilote as a Domoticz Plugin.
""" import math import time from datetime import datetime import requests import Domoticz # type: ignore if None is not None: #Fake statement to remove warning on global Domoticz variables #NOSONAR Parameters = Parameters # type: ignore #NOSONAR #pylint: disable=undefined-variable,self-assigning-variable Images = Images # type: ignore #NOSONAR #pylint: disable=undefined-variable,self-assigning-variable Devices = Devices # type: ignore #NOSONAR #pylint: disable=undefined-variable,self-assigning-variable HEATZY_MODE = { '停止': 'OFF', '解冻': 'FROSTFREE', '经济': 'ECONOMY', '舒适': 'NORMAL', 'off': 'OFF', 'fro': 'FROSTFREE', 'eco': 'ECONOMY', 'cft': 'NORMAL', } HEATZY_MODE_NAME = { 'OFF': 'Off', 'FROSTFREE': 'Hors Gel', 'ECONOMY': 'Eco', 'NORMAL': 'Confort' } HEATZY_MODE_VALUE = { 'OFF': 0, 'FROSTFREE': 10, 'ECONOMY': 20, 'NORMAL': 30 } HEATZY_MODE_VALUE_INV = {v: k for k, v in HEATZY_MODE_VALUE.items()} DEFAULT_POOLING = 60 class BasePlugin: """Class for plugin""" debug = False token = "" token_expire_at = 0 did = "" mode = 0 nextupdate = datetime.now() bug = False pooling = 30 pooling_steps = 1 pooling_current_step = 1 max_retry = 6 retry = max_retry def __init__(self): return def on_start(self): """At statup""" # setup the appropriate logging level debuglevel = int(Parameters["Mode6"]) if debuglevel != 0: self.debug = True Domoticz.Debugging(debuglevel) dump_config_to_log() else: self.debug = False Domoticz.Debugging(0) # Polling interval = X sec try: pooling = int(Parameters["Mode5"]) except Exception: pooling = DEFAULT_POOLING self.pooling_steps = math.ceil(pooling/30) self.pooling = pooling // self.pooling_steps Domoticz.Heartbeat(self.pooling) # create the child devices if these do not exist yet if 1 not in Devices: Domoticz.Device(Name="Control", Unit=1, TypeName="Switch", Image=9, Used=1).Create() if 2 not in Devices: options = {"LevelActions": "||", "LevelNames": HEATZY_MODE_NAME['OFF'] + "|" + HEATZY_MODE_NAME['FROSTFREE'] + "|" + HEATZY_MODE_NAME['ECONOMY'] + "|" + HEATZY_MODE_NAME['NORMAL'], "LevelOffHidden": "false", #Bug with off mode... #"LevelOffHidden": "true",t "SelectorStyle": "0"} Domoticz.Device(Name="Mode", Unit=2, TypeName="Selector Switch", Switchtype=18, Image=15, Options=options, Used=1).Create() # Bug Off = Normal? if str(Parameters["Mode4"]) != "0": Domoticz.Log("Heatzy plugin configured to support the bug Off=Confort. Then when switching to Off, Heatzy will switch to Frost Free.") self.bug = True # Get Token self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"]) # Get Devide Id self.did = self.get_device_id(self.token, Parameters["Mode3"]) # Get mode self.mode = self.get_mode() def on_command(self, unit, command, level, hue): #pylint: disable=unused-argument """Send a command""" if unit == 1: self.mode = self.on_off(command) elif unit == 2: self.mode = self.set_mode(level) def on_heartbeat(self): """Time to heartbeat :)""" if self.pooling_current_step >= self.pooling_steps: Domoticz.Debug(f"Retry counter:{self.retry}") if self.retry < 0: Domoticz.Status("No connection to Heatzy API ==> Device disabled for 15 minutes") self.pooling_current_step = - 15 * 60 // self.pooling + self.pooling_steps self.retry = self.max_retry #Force refresh token/did Domoticz.Status("Force refresh token and device id.") self.token = "" self.did = "" return self.mode = self.get_mode() # If mode = OFF and bug, then mode = FROSTFREE if self.bug and self.mode == 'OFF': Domoticz.Log("Switch to FROSTFREE because of the OFF bug...") self.mode = self.set_mode(HEATZY_MODE_VALUE['FROSTFREE']) # check if need to refresh device so that they do not turn red in GUI #now = datetime.now() #if self.nextupdate <= now: # self.nextupdate = now + timedelta(minutes=int(Settings["SensorTimeout"])) # Devices[2].Update(nValue=Devices[2].nValue, sValue=Devices[2].sValue) self.pooling_current_step = 1 else: self.pooling_current_step = self.pooling_current_step + 1 def get_token(self, user, password): """Get token using the Heatzy API""" need_to_get_token = False if self.token == "" or self.token_expire_at == "": need_to_get_token = True Domoticz.Status("Heatzy Token unknown, need to call Heatzy API.") elif (float(self.token_expire_at)-time.time()) < 24*60*60: #Token will expire in less than 1 day need_to_get_token = True Domoticz.Status("Heatzy Token expired, need to call Heatzy API.") if need_to_get_token and self.retry>=0: headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', } data = '{ "username": "'+user+'", "password": "'+password+'", "lang": "en" }' try: time.sleep(0.5) url = 'https://euapi.gizwits.com/app/login' response = requests.post(url, headers=headers, data=data, timeout=3).json() except Exception as exc: Domoticz.Error("Cannot open connection to Heatzy API to get the token: " + str(exc)) #Domoticz.Error("URL: " + str(url)) #Domoticz.Error("Headers: " + str(headers)) Domoticz.Error("Data: " + str(data)) #Decrease retry self.retry = self.retry - 1 return "", "" Domoticz.Debug("Get Token Response: " + str(response)) if 'token' in response: self.token = response['token'] self.token_expire_at = response['expire_at'] Domoticz.Status("Token from Heatzy API: " + self.token) #Reset retry counter self.retry = self.max_retry else: error_code = "Unknown" if 'error_code' not in response else response['error_code'] error_message = "Unknown" if 'error_message' not in response else response['error_message'] Domoticz.Error(f"Cannot get Heatzy Token: {error_message} ({error_code})\n{response}") self.token = "" self.token_expire_at = 0 self.did = "" #Decrease retry self.retry = self.retry - 1 return self.token, self.token_expire_at def get_device_id(self, token, alias): """Get the device id from the token and the device name, using the Heatzy API""" if token == "" or self.retry<0: return "" if self.did == "": Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.") headers = { 'Accept': 'application/json', 'X-Gizwits-User-token': token, 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', } params = (('limit', '20'), ('skip', '0'),) url = 'https://euapi.gizwits.com/app/bindings' try: response = requests.get(url, headers=headers, params=params, timeout=3).json() except Exception as exc: Domoticz.Error("Cannot open connection to Heatzy API to get the device id: " + str(exc)) #Domoticz.Error("URL: " + str(url)) #Domoticz.Error("Headers: " + str(headers)) #Domoticz.Error("Params: " + str(params)) return "" Domoticz.Debug("Get Device Id Response:" + str(response)) found = False if 'devices' in response: devices = response['devices'] for device in devices: if "dev_alias" in device and "did" in device and device["dev_alias"].lower() == alias.lower(): found = True self.did = device['did'] Domoticz.Status("Devide Id from Heatzy API: " + self.did) if not found: self.did = "" error_code = "Unknown" if 'error_code' not in response else response['error_code'] error_message = "Unknown" if 'error_message' not in response else response['error_message'] Domoticz.Error(f"Cannot get Heatzy Devide Id: {error_message} ({error_code})\n{response}") return self.did def get_mode(self): "Get the device mode using the Heatzy API" mode = "" response = "" self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"]) self.did = self.get_device_id(self.token, Parameters["Mode3"]) if self.retry<0: return "" headers = { 'Accept': 'application/json', 'X-Gizwits-User-token': self.token, 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', } url = f"https://euapi.gizwits.com/app/devdata/{self.did}/latest" try: response = requests.get(url, headers=headers, timeout=3).json() except Exception as exc: #Decrease retry self.retry = self.retry - 1 if self.retry < self.max_retry//2: Domoticz.Error("Cannot open connection to Heatzy API to get the mode: " + str(exc)) #Domoticz.Error("URL: " + str(url)) #Domoticz.Error("Headers: " + str(headers)) if 'response' in locals() and response != "": Domoticz.Error("Response: " + str(response)) return "" Domoticz.Debug("Get Mode Response:" + str(response)) if 'attr' in response and 'mode' in response['attr']: mode = HEATZY_MODE[response['attr']['mode']] Domoticz.Debug(f"Current Heatzy Mode: {HEATZY_MODE_NAME[mode]}") #Reset retry counter self.retry = self.max_retry if Devices[2].nValue != HEATZY_MODE_VALUE[mode]: Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[mode]}") Devices[2].Update(nValue=HEATZY_MODE_VALUE[mode], sValue=str(HEATZY_MODE_VALUE[mode]), TimedOut = 0) if not self.bug: if mode == 'OFF' and Devices[1].nValue != 0: Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) elif mode != 'OFF' and Devices[1].nValue == 0: Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) else: if mode in ('OFF', 'FROSTFREE') and Devices[1].nValue != 0: Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) elif mode not in ('OFF', 'FROSTFREE') and Devices[1].nValue == 0: Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) else: #Decrease retry self.retry = self.retry - 1 error_code = "Unknown" if 'error_code' not in response else response['error_code'] error_message = "Unknown" if 'error_message' not in response else response['error_message'] Domoticz.Error(f"Cannot get Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {self.did}") if error_code == 9004: #Invalid token self.token = "" self.did = "" elif 'attr' in response and len(response["attr"]) == 0: #attr is empty... Domoticz.Status("We force a setMode to try to get the correct mode at the next try...") self.set_mode(HEATZY_MODE_VALUE['FROSTFREE']) return "" return mode def set_mode(self, mode): "Set the device mode using the Heatzy API" if Devices[2].nValue != mode: mode_str = { HEATZY_MODE_VALUE['NORMAL']: 0, #'[1,1,0]', HEATZY_MODE_VALUE['ECONOMY']: 1, #'[1,1,1]', HEATZY_MODE_VALUE['FROSTFREE']: 2, #'[1,1,2]', HEATZY_MODE_VALUE['OFF']: 3, #'[1,1,3]', } headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Gizwits-User-token': self.token, 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', } #data = '{"raw": '+mode_str[mode]+'}' data = f'{"attrs": {"mode":{mode_str[mode]}}}' url = f"https://euapi.gizwits.com/app/control/{self.did}" try: response = requests.post(url, headers=headers, data=data, timeout=3).json() except Exception as exc: Domoticz.Error("Cannot open connection to Heatzy API to set the mode: " + str(exc)) #Domoticz.Error("URL: " + str(url)) #Domoticz.Error("Headers: " + str(headers)) Domoticz.Error("Data: " + str(data)) return self.mode Domoticz.Debug("Set Mode Response:" + str(response)) if response is not None: self.mode = HEATZY_MODE_VALUE_INV[mode] Devices[2].Update(nValue=int(mode), sValue=str(mode)) Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[self.mode]}") if not self.bug: if self.mode == 'OFF' and Devices[1].nValue != 0: Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) elif self.mode != 'OFF' and Devices[1].nValue == 0: Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) else: if self.mode in ('OFF', 'FROSTFREE') and Devices[1].nValue != 0: Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) elif self.mode not in ('OFF', 'FROSTFREE') and Devices[1].nValue == 0: Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) else: error_code = "Unknown" if 'error_code' not in response else response['error_code'] error_message = "Unknown" if 'error_message' not in response else response['error_message'] Domoticz.Error(f"Cannot set Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {self.did}") if error_code == 9004: #Invalid token self.token = "" self.did = "" return self.mode #Device is correctly running ==> we reset the retry counter self.retry = self.max_retry return self.mode def on_off(self, command): """Toggle device on/off""" if Devices[1].sValue != command: if command == "On": self.mode = self.set_mode(HEATZY_MODE_VALUE['NORMAL']) else: if not self.bug: self.mode = self.set_mode(HEATZY_MODE_VALUE['OFF']) else: #Because of issue with the equipment (Off do not work...) self.mode = self.set_mode(HEATZY_MODE_VALUE['FROSTFREE']) return self.mode _plugin = BasePlugin() def onStart(): #NOSONAR #pylint: disable=invalid-name """OnStart""" _plugin.on_start() def onCommand(Unit, Command, Level, Hue): #NOSONAR #pylint: disable=invalid-name """OnCommand""" _plugin.on_command(Unit, Command, Level, Hue) def onHeartbeat(): #NOSONAR #pylint: disable=invalid-name """onHeartbeat""" _plugin.on_heartbeat() # Generic helper functions def dump_config_to_log(): """Dump the config to the Domoticz Log""" for x in Parameters: if Parameters[x] != "": Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'") Domoticz.Debug("Device count: " + str(len(Devices))) for x in Devices: Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x])) Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'") Domoticz.Debug("Device Name: '" + Devices[x].Name + "'") Domoticz.Debug("Device nValue: " + str(Devices[x].nValue)) Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'") Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel)) return