Compare commits

..

31 Commits

Author SHA1 Message Date
7285595b32 Bump version, just for test purpose 2025-12-01 22:05:41 +01:00
f752b45ee1 Toggle device in case of timeout 2025-11-23 21:49:56 +01:00
ad3cfbe74d Improve http get/post with internal retry 2025-11-02 23:37:06 +01:00
122615dffe From Status to Debug 2025-11-02 22:43:03 +01:00
be985698a0 Fix trigger of error message 2025-11-02 22:42:03 +01:00
a9bcf978c9 Improve displaying of message for reset retry 2025-11-02 20:18:23 +01:00
81030f7ddc Fix retry mechanism 2025-11-02 18:53:35 +01:00
4eea028f0a Switch to TimedOut only when update_at < command_at 2025-11-02 15:56:35 +01:00
c77c76e2fe Better retry message 2025-11-02 15:26:46 +01:00
b01c09559e Http timeout = 5 sec and better retry mechanism 2025-11-02 12:03:08 +01:00
164f0c77ff Get devices if not present in self.did 2025-10-27 22:10:46 +01:00
70f4fafbfd Add debug info 2025-10-26 22:52:40 +01:00
1672ba6dc5 Fix bug get mode 2025-10-26 20:56:39 +01:00
7828542c6c Add debug info 2025-10-26 20:50:55 +01:00
c53fbc8a6d Add debug info 2025-10-26 20:45:12 +01:00
0600e28f30 Remove warnings 2024-12-15 22:32:52 +01:00
1b322c55e5 Better management of obsolete device 2024-12-15 14:27:23 +01:00
2d495ba0b3 Fix bug when device is back 2024-12-12 20:55:52 +01:00
95debb7ea3 Manage TimedOut device (>3h) 2024-12-10 12:47:00 +01:00
07240a1693 Change the way the devices are organized in the plugin 2024-12-09 22:40:40 +01:00
76a4169308 Move to DomoticsEx API and support multiple devices in the same plugin 2024-12-08 23:51:05 +01:00
8942775fbc Do not refresh the device when the "last update" is the same 2024-12-04 22:49:55 +01:00
80a3c9a96b Obsolete device when > 30min 2024-12-04 22:34:22 +01:00
ffc676d16b Add support for Pilote v2 2024-11-23 19:25:35 +01:00
d61b89ca45 Add support for Pilote v2 2024-11-23 19:23:11 +01:00
dfdd62415e Add support for Pilote v2 2024-11-23 19:20:57 +01:00
92023c0290 Add support for Pilote v2 2024-11-23 19:19:01 +01:00
816e2d3c7c Add support for Pilote v2 2024-11-23 18:07:37 +01:00
0d9d03ce4d Support multiple devices 2024-11-17 17:30:09 +01:00
ea9f35f3b6 Merge branch 'master' of https://git.cavogrenier.fr/fjumelle/domoticz-Heatzy 2024-11-17 17:26:39 +01:00
6eae6866e8 Support multiple devices 2024-11-17 17:23:37 +01:00
2 changed files with 309 additions and 206 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
Domoticz.py
DomoticzEx.py
parameters.properties
run.py
__pycache__/

442
plugin.py
View File

@@ -2,8 +2,9 @@
# #
# Author: fjumelle # Author: fjumelle
# #
#pylint: disable=line-too-long,broad-exception-caught,possibly-used-before-assignment
""" """
<plugin key="Heatzy_FJU" name="Heatzy Pilote" author="fjumelle" version="1.0.4" wikilink="" externallink=""> <plugin key="HeatzyEx" name="Heatzy Pilote Ex" author="fjumelle" version="2.3.1" wikilink="" externallink="">
<description> <description>
<h2>Heatzy Pilote</h2><br/> <h2>Heatzy Pilote</h2><br/>
Implementation of Heatzy Pilote as a Domoticz Plugin.<br/> Implementation of Heatzy Pilote as a Domoticz Plugin.<br/>
@@ -17,7 +18,7 @@
<option label="Yes" value="1"/> <option label="Yes" value="1"/>
</options> </options>
</param> </param>
<param field="Mode5" label="Polling interval (s)" width="40px" required="true" default="60"/> <param field="Mode5" label="Polling interval (s)" width="40px" required="true" default="120"/>
<param field="Mode6" label="Logging Level" width="200px"> <param field="Mode6" label="Logging Level" width="200px">
<options> <options>
<option label="Normal" value="0" default="true"/> <option label="Normal" value="0" default="true"/>
@@ -27,22 +28,28 @@
</params> </params>
</plugin> </plugin>
""" """
import Domoticz import math
import requests
import json
import time import time
import urllib.parse as parse from datetime import datetime
import urllib.request as request from enum import IntEnum
from datetime import datetime, timedelta
global Parameters import requests
global Devices import DomoticzEx as Domoticz #pylint: disable=import-error #pyright: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 = { HEATZY_MODE = {
'停止': 'OFF', '停止': 'OFF',
'解冻': 'FROSTFREE', '解冻': 'FROSTFREE',
'经济': 'ECONOMY', '经济': 'ECONOMY',
'舒适': 'NORMAL' '舒适': 'NORMAL',
'stop': 'OFF',
'fro': 'FROSTFREE',
'eco': 'ECONOMY',
'cft': 'NORMAL',
} }
HEATZY_MODE_NAME = { HEATZY_MODE_NAME = {
@@ -63,39 +70,38 @@ HEATZY_MODE_VALUE_INV = {v: k for k, v in HEATZY_MODE_VALUE.items()}
DEFAULT_POOLING = 60 DEFAULT_POOLING = 60
class deviceparam: class HeatzyUnit(IntEnum):
def __init__(self, unit, nvalue, svalue): """2 units: Control and Selector"""
self.unit = unit CONTROL = 1
self.nvalue = nvalue SELECTOR = 2
self.svalue = svalue
class BasePlugin: class BasePlugin:
"""Class for plugin"""
_HTTP_TIMEOUT = 5
_MAX_RETRY_PER_DEVICE = 3
debug = False debug = False
token = "" token = ""
token_expire_at = 0 token_expire_at = 0
did = "" did = {}
mode = 0
nextupdate = datetime.now() nextupdate = datetime.now()
bug = False bug = False
pooling = 30 pooling = 30
pooling_steps = 1 pooling_steps = 1
pooling_current_step = 1 pooling_current_step = 1
max_retry = 6 retry = 0
retry = max_retry
def __init__(self): def __init__(self):
return return
def onStart(self): def on_start(self):
import math """At statup"""
# setup the appropriate logging level # setup the appropriate logging level
debuglevel = int(Parameters["Mode6"]) debuglevel = int(Parameters["Mode6"])
if debuglevel != 0: if debuglevel != 0:
self.debug = True self.debug = True
Domoticz.Debugging(debuglevel) Domoticz.Debugging(debuglevel)
DumpConfigToLog() dump_config_to_log()
else: else:
self.debug = False self.debug = False
Domoticz.Debugging(0) Domoticz.Debugging(0)
@@ -103,80 +109,75 @@ class BasePlugin:
# Polling interval = X sec # Polling interval = X sec
try: try:
pooling = int(Parameters["Mode5"]) pooling = int(Parameters["Mode5"])
except: except Exception:
pooling = DEFAULT_POOLING pooling = DEFAULT_POOLING
self.pooling_steps = math.ceil(pooling/30) self.pooling_steps = math.ceil(pooling/30)
self.pooling = pooling // self.pooling_steps self.pooling = pooling // self.pooling_steps
Domoticz.Heartbeat(self.pooling) Domoticz.Heartbeat(self.pooling)
# create the child devices if these do not exist yet # Get Token
devicecreated = [] self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"])
if 1 not in Devices:
Domoticz.Device(Name="Control", Unit=1, TypeName="Switch", Image=9, Used=1).Create() # Get Devide Id
devicecreated.append(deviceparam(1, 0, "Off")) # default is Off self.did = self.get_heatzy_devices()
if 2 not in Devices:
Options = {"LevelActions": "||", # max retry per device
self.retry = self._MAX_RETRY_PER_DEVICE * len(self.did)
# Create the child devices if these do not exist yet
for deviceid in self.did:
if deviceid not in Devices:
alias = self.did[deviceid]["alias"]
#Control
Domoticz.Unit(Name=f"Heatzy {alias} - Control", DeviceID=deviceid, Unit=HeatzyUnit.CONTROL, TypeName="Switch", Image=9, Used=1).Create()
#Selector switch
options = {"LevelActions": "||",
"LevelNames": HEATZY_MODE_NAME['OFF'] + "|" + HEATZY_MODE_NAME['FROSTFREE'] + "|" + HEATZY_MODE_NAME['ECONOMY'] + "|" + HEATZY_MODE_NAME['NORMAL'], "LevelNames": HEATZY_MODE_NAME['OFF'] + "|" + HEATZY_MODE_NAME['FROSTFREE'] + "|" + HEATZY_MODE_NAME['ECONOMY'] + "|" + HEATZY_MODE_NAME['NORMAL'],
"LevelOffHidden": "false", #Bug with off mode... "LevelOffHidden": "false", #Bug with off mode...
#"LevelOffHidden": "true",t #"LevelOffHidden": "true",t
"SelectorStyle": "0"} "SelectorStyle": "0"}
Domoticz.Device(Name="Mode", Unit=2, TypeName="Selector Switch", Switchtype=18, Image=15, Domoticz.Unit(Name=f"Heatzy {alias} - Mode", DeviceID=deviceid, Unit=HeatzyUnit.SELECTOR,
Options=Options, Used=1).Create() TypeName="Selector Switch", Switchtype=18, Image=15,
devicecreated.append(deviceparam(2, 0, "30")) # default is confort mode Options=options, Used=1).Create()
# Bug Off = Normal? # Bug Off = Normal?
if str(Parameters["Mode4"]) != "0": 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.") 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 self.bug = True
# Get Token
self.token, self.token_expire_at = self.getToken(Parameters["Username"], Parameters["Password"])
# Get Devide Id
self.did = self.getDevideId(self.token)
# Get mode # Get mode
self.mode = self.getMode() self.get_mode()
def onCommand(self, Unit, Command, Level, Hue): def on_command(self, DeviceID, Unit, Command, Level, Color): #pylint: disable=unused-argument,invalid-name
if Unit == 1: """Send a command"""
self.mode = self.onOff(Command) if Unit == HeatzyUnit.CONTROL:
elif Unit == 2: self.send_command(DeviceID, Command)
self.mode = self.setMode(Level) elif Unit == HeatzyUnit.SELECTOR:
self.set_mode(DeviceID, Level)
def onHeartbeat(self): def on_heartbeat(self):
"""Time to heartbeat :)"""
if self.pooling_current_step >= self.pooling_steps: if self.pooling_current_step >= self.pooling_steps:
Domoticz.Debug("Retry counter:{}".format(self.retry)) Domoticz.Debug(f"Retry counter: {self.retry}")
if self.retry < 0: if self.retry < 0:
Domoticz.Status("No connection to Heatzy API ==> Device disabled for 15 minutes") Domoticz.Status("No connection to Heatzy API ==> Device disabled for 15 minutes")
self.pooling_current_step = - 15 * 60 // self.pooling + self.pooling_steps self.pooling_current_step = - 15 * 60 // self.pooling + self.pooling_steps
self.retry = self.max_retry self.reset_retry()
#Force refresh token/did #Force refresh token/did
Domoticz.Status("Force refresh token and device id.") Domoticz.Status("Force refresh token and device id.")
self.token = "" self.token = ""
self.did = "" self.did = {}
return return
self.mode = self.getMode() 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.setMode(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 self.pooling_current_step = 1
else: else:
self.pooling_current_step = self.pooling_current_step + 1 self.pooling_current_step = self.pooling_current_step + 1
def getToken(self, user, password): def get_token(self, user, password):
import time """Get token using the Heatzy API"""
need_to_get_token = False need_to_get_token = False
if self.token == "" or self.token_expire_at == "": if self.token == "" or self.token_expire_at == "":
@@ -194,14 +195,12 @@ class BasePlugin:
'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3',
} }
data = '{ "username": "'+user+'", "password": "'+password+'", "lang": "en" }' data = '{ "username": "'+user+'", "password": "'+password+'", "lang": "en" }'
try:
time.sleep(0.5) time.sleep(0.5)
url = 'https://euapi.gizwits.com/app/login' url = 'https://euapi.gizwits.com/app/login'
response = requests.post(url, headers=headers, data=data, timeout=3).json() try:
response = http("post", url, headers, data)
except Exception as exc: except Exception as exc:
Domoticz.Error("Cannot open connection to Heatzy API to get the token: " + str(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)) Domoticz.Error("Data: " + str(data))
#Decrease retry #Decrease retry
self.retry = self.retry - 1 self.retry = self.retry - 1
@@ -214,61 +213,66 @@ class BasePlugin:
self.token_expire_at = response['expire_at'] self.token_expire_at = response['expire_at']
Domoticz.Status("Token from Heatzy API: " + self.token) Domoticz.Status("Token from Heatzy API: " + self.token)
#Reset retry counter #Reset retry counter
self.retry = self.max_retry self.reset_retry()
else: else:
error_code = "Unknown" if 'error_code' not in response else response['error_code'] 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'] error_message = "Unknown" if 'error_message' not in response else response['error_message']
Domoticz.Error("Cannot get Heatzy Token: {} ({})\n{}".format(error_message, error_code, response)) Domoticz.Error(f"Cannot get Heatzy Token: {error_message} ({error_code})\n{response}")
self.token = "" self.token = ""
self.token_expire_at = 0 self.token_expire_at = 0
self.did = "" self.did = {}
#Decrease retry #Decrease retry
self.retry = self.retry - 1 self.retry = self.retry - 1
return self.token, self.token_expire_at return self.token, self.token_expire_at
def getDevideId(self, token): def get_heatzy_devices(self):
if token == "" or self.retry<0: """Get the device id from the token, using the Heatzy API"""
return "" if self.token == "" or self.retry<0:
return self.did
if self.did == "": if len(self.did) == 0:
Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.") Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.")
headers = { headers = {
'Accept': 'application/json', 'Accept': 'application/json',
'X-Gizwits-User-token': token, 'X-Gizwits-User-token': self.token,
'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3',
} }
params = (('limit', '20'), ('skip', '0'),) params = (('limit', '20'), ('skip', '0'),)
url = 'https://euapi.gizwits.com/app/bindings' url = 'https://euapi.gizwits.com/app/bindings'
try: try:
response = requests.get(url, headers=headers, params=params, timeout=3).json() response = http("get", url, headers, params=params)
except Exception as exc: except Exception as exc:
Domoticz.Error("Cannot open connection to Heatzy API to get the device id: " + str(exc)) Domoticz.Error("Cannot open connection to Heatzy API to get the device id: " + str(exc))
#Domoticz.Error("URL: " + str(url)) #Domoticz.Error("URL: " + str(url))
#Domoticz.Error("Headers: " + str(headers)) #Domoticz.Error("Headers: " + str(headers))
#Domoticz.Error("Params: " + str(params)) #Domoticz.Error("Params: " + str(params))
return "" return self.did
Domoticz.Debug("Get Device Id Response:" + str(response)) Domoticz.Debug("Get Device Id Response:" + str(response))
if 'devices' in response and 'did' in response['devices'][0]: if 'devices' in response:
self.did = response['devices'][0]['did'] devices = response['devices']
Domoticz.Status("Devide Id from Heatzy API: " + self.did) unit = 1
else: for device in devices:
self.did = "" if "dev_alias" in device and "did" in device and "product_key" in device:
error_code = "Unknown" if 'error_code' not in response else response['error_code'] product_key = device['product_key']
error_message = "Unknown" if 'error_message' not in response else response['error_message'] alias = device['dev_alias']
Domoticz.Error("Cannot get Heatzy Devide Id: {} ({})\n{}".format(error_message, error_code, response)) did = device['did']
self.did[product_key] = {"did":did, "alias":alias, "updated_at":0, "command_at":0}
Domoticz.Status(f"Devide Id from Heatzy API: {alias} - {did}")
unit = unit + 1
return self.did return self.did
def getMode(self): def get_mode(self):
"Get the device mode using the Heatzy API"
mode = "" mode = ""
response = "" response = ""
self.token, self.token_expire_at = self.getToken(Parameters["Username"], Parameters["Password"]) self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"])
self.did = self.getDevideId(self.token) #self.did = self.get_heatzy_devices() #Not really needed
if self.retry<0: if self.retry<0:
return "" return ""
@@ -278,70 +282,108 @@ class BasePlugin:
'X-Gizwits-User-token': self.token, 'X-Gizwits-User-token': self.token,
'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3',
} }
url = 'https://euapi.gizwits.com/app/devdata/{}/latest'.format(self.did)
Domoticz.Debug(f"Get Mode for {[self.did[d]['alias'] for d in self.did]}...")
for deviceid in self.did:
device = self.did[deviceid]
did = device["did"]
alias = device["alias"]
url = f"https://euapi.gizwits.com/app/devdata/{did}/latest"
try: try:
response = requests.get(url, headers=headers, timeout=3).json() response = http("get", url, headers)
except Exception as exc: except Exception as exc:
message = f"Cannot open connection to Heatzy API to get the mode for {alias} (retry={self.retry}): {exc}"
if self.retry <= 0:
Domoticz.Error(message)
else:
Domoticz.Debug(message + f" (retry left: {self.retry})")
#Decrease retry #Decrease retry
self.retry = self.retry - 1 self.retry = self.retry - 1
if self.retry < self.max_retry//2: continue
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)) Domoticz.Debug(f"Get Mode Response for {alias}: {response}")
#Last Update
if 'updated_at' in response:
if response["updated_at"] == device["updated_at"]:
#No update since last heartbeat
if device["command_at"] - 60 > device["updated_at"] and Devices[deviceid].TimedOut == 0:
#No update while a command has been sent
obsolete_min = int((time.time() - response["updated_at"])//60)
Domoticz.Error(f"Last update from '{alias}' was {obsolete_min} min earlier.")
Devices[deviceid].TimedOut = 1
#Toggle device to be sure it is not stucked
Domoticz.Error(f"Then I toggle the device '{alias}'.")
self.toggle(deviceid)
continue
device["updated_at"] = response["updated_at"]
if Devices[deviceid].TimedOut == 1:
Domoticz.Status(f"'{alias}' is now back.")
Devices[deviceid].TimedOut = 0
if 'attr' in response and 'mode' in response['attr']: if 'attr' in response and 'mode' in response['attr']:
mode = HEATZY_MODE[response['attr']['mode']] mode = HEATZY_MODE[response['attr']['mode']]
Domoticz.Debug("Current Heatzy Mode: {}".format(HEATZY_MODE_NAME[mode])) Domoticz.Debug(f"Current Heatzy Mode: {HEATZY_MODE_NAME[mode]} ({alias})")
#Reset retry counter #Reset retry counter
self.retry = self.max_retry self.reset_retry()
if Devices[2].nValue != HEATZY_MODE_VALUE[mode]: if Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue != HEATZY_MODE_VALUE[mode]:
Domoticz.Status("New Heatzy Mode: {}".format(HEATZY_MODE_NAME[mode])) Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[mode]} ({alias})")
Devices[2].Update(nValue=HEATZY_MODE_VALUE[mode], sValue=str(HEATZY_MODE_VALUE[mode]), TimedOut = 0) Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue = HEATZY_MODE_VALUE[mode]
Devices[deviceid].Units[HeatzyUnit.SELECTOR].sValue = str(HEATZY_MODE_VALUE[mode])
Devices[deviceid].Units[HeatzyUnit.SELECTOR].Update()
if not self.bug: if not self.bug:
if mode == 'OFF' and Devices[1].nValue != 0: if mode == 'OFF' and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
elif mode != 'OFF' and Devices[1].nValue == 0: Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) elif mode != 'OFF' and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue == 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 1
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "On"
else: else:
if mode in ('OFF', 'FROSTFREE') and Devices[1].nValue != 0: if mode in ('OFF', 'FROSTFREE') and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
elif mode not in ('OFF', 'FROSTFREE') and Devices[1].nValue == 0: Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) elif mode not in ('OFF', 'FROSTFREE') and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue == 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 1
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "On"
Devices[deviceid].Units[HeatzyUnit.CONTROL].Update()
else: else:
#Decrease retry #Decrease retry
self.retry = self.retry - 1 self.retry = self.retry - 1
error_code = "Unknown" if 'error_code' not in response else response['error_code'] 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'] error_message = "Unknown" if 'error_message' not in response else response['error_message']
Domoticz.Error("Cannot get Heatzy Mode: {} ({})\n{}\nToken: {}\nDeviceId: {}".format(error_message, error_code, response, self.token, self.did)) Domoticz.Error(f"Cannot get Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {did}")
if error_code == 9004: if error_code == 9004:
#Invalid token #Invalid token
self.token = "" self.token = ""
self.did = "" self.did = {}
elif 'attr' in response and len(response["attr"]) == 0: elif 'attr' in response and len(response["attr"]) == 0:
#attr is empty... #attr is empty...
Domoticz.Status("We force a setMode to try to get the correct mode at the next try...") Domoticz.Status("We force a setMode to try to get the correct mode at the next try...")
self.setMode(HEATZY_MODE_VALUE['FROSTFREE']) self.set_mode(deviceid, HEATZY_MODE_VALUE['FROSTFREE'])
return "" continue
return mode # If mode = OFF and bug, then mode = FROSTFREE
if self.bug and mode == 'OFF':
Domoticz.Log(f"Switch to FROSTFREE because of the OFF bug...({device})")
self.set_mode(deviceid, HEATZY_MODE_VALUE['FROSTFREE'])
def setMode(self, mode): def set_mode(self, deviceid, mode):
if Devices[2].nValue != mode: "Set the device mode using the Heatzy API"
if Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue != mode:
mode_str = { mode_str = {
HEATZY_MODE_VALUE['NORMAL']: '[1,1,0]', HEATZY_MODE_VALUE['NORMAL']: 0, #'[1,1,0]',
HEATZY_MODE_VALUE['ECONOMY']: '[1,1,1]', HEATZY_MODE_VALUE['ECONOMY']: 1, #'[1,1,1]',
HEATZY_MODE_VALUE['FROSTFREE']: '[1,1,2]', HEATZY_MODE_VALUE['FROSTFREE']: 2, #'[1,1,2]',
HEATZY_MODE_VALUE['OFF']: '[1,1,3]' HEATZY_MODE_VALUE['OFF']: 3, #'[1,1,3]',
} }
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -349,87 +391,143 @@ class BasePlugin:
'X-Gizwits-User-token': self.token, 'X-Gizwits-User-token': self.token,
'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3', 'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3',
} }
data = '{"raw": '+mode_str[mode]+'}' #data = '{"raw": '+mode_str[mode]+'}'
url = 'https://euapi.gizwits.com/app/control/{}'.format(self.did) data = '{"attrs": {"mode":' + str(mode_str[mode]) + '}}'
if deviceid not in self.did:
#Should not occur... but it occurs sometimes
self.did = self.get_heatzy_devices()
did = self.did[deviceid]["did"]
self.did[deviceid]["command_at"] = time.time()
url = f"https://euapi.gizwits.com/app/control/{did}"
try: try:
response = requests.post(url, headers=headers, data=data, timeout=3).json() response = http("post", url, headers, data)
except Exception as exc: except Exception as exc:
Domoticz.Error("Cannot open connection to Heatzy API to set the mode: " + str(exc)) Domoticz.Error("Cannot open connection to Heatzy API to set the mode: " + str(exc))
#Domoticz.Error("URL: " + str(url)) #Domoticz.Error("URL: " + str(url))
#Domoticz.Error("Headers: " + str(headers)) #Domoticz.Error("Headers: " + str(headers))
Domoticz.Error("Data: " + str(data)) Domoticz.Error("Data: " + str(data))
return self.mode return
Domoticz.Debug("Set Mode Response:" + str(response)) Domoticz.Debug("Set Mode Response:" + str(response))
if response != None: if response is not None:
self.mode = HEATZY_MODE_VALUE_INV[mode] mode_str = HEATZY_MODE_VALUE_INV[mode]
Devices[2].Update(nValue=int(mode), sValue=str(mode)) Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue = int(mode)
Domoticz.Status("New Heatzy Mode: {}".format(HEATZY_MODE_NAME[self.mode])) Devices[deviceid].Units[HeatzyUnit.SELECTOR].sValue = str(mode)
Devices[deviceid].Units[HeatzyUnit.SELECTOR].Update()
alias = self.did[deviceid]["alias"]
Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[mode_str]} ({alias})")
if not self.bug: if not self.bug:
if self.mode == 'OFF' and Devices[1].nValue != 0: if mode_str == 'OFF' and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
elif self.mode != 'OFF' and Devices[1].nValue == 0: Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) elif mode_str != 'OFF' and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue == 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 1
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "On"
else: else:
if self.mode in ('OFF', 'FROSTFREE') and Devices[1].nValue != 0: if mode_str in ('OFF', 'FROSTFREE') and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
elif self.mode not in ('OFF', 'FROSTFREE') and Devices[1].nValue == 0: Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) elif mode_str not in ('OFF', 'FROSTFREE') and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue == 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 1
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "On"
Devices[deviceid].Units[HeatzyUnit.CONTROL].Update()
else: else:
error_code = "Unknown" if 'error_code' not in response else response['error_code'] 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'] error_message = "Unknown" if 'error_message' not in response else response['error_message']
Domoticz.Error("Cannot set Heatzy Mode: {} ({})\n{}\nToken: {}\nDeviceId: {}".format(error_message, error_code, response, self.token, self.did)) Domoticz.Error(f"Cannot set Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {did}")
if error_code == 9004: if error_code == 9004:
#Invalid token #Invalid token
self.token = "" self.token = ""
self.did = "" self.did = {}
return self.mode return
#Device is correctly running ==> we reset the retry counter #Device is correctly running ==> we reset the retry counter
self.retry = self.max_retry self.reset_retry()
return self.mode def send_command(self, deviceid, command):
"""Send command to device, only when it is not already in the requested state"""
def onOff(self, command): if Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue != command:
if Devices[1].sValue != command:
if command == "On": if command == "On":
self.mode = self.setMode(HEATZY_MODE_VALUE['NORMAL']) self.set_mode(deviceid, HEATZY_MODE_VALUE['NORMAL'])
else: else:
if not self.bug: if not self.bug:
self.mode = self.setMode(HEATZY_MODE_VALUE['OFF']) self.set_mode(deviceid, HEATZY_MODE_VALUE['OFF'])
else: else:
#Because of issue with the equipment (Off do not work...) #Because of issue with the equipment (Off do not work...)
self.mode = self.setMode(HEATZY_MODE_VALUE['FROSTFREE']) self.set_mode(deviceid, HEATZY_MODE_VALUE['FROSTFREE'])
return self.mode def toggle(self, deviceid):
"""Toggle device"""
new_command1 = "Off" if Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue == "On" else "On"
new_command2 = "On" if Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue == "On" else "Off"
self.send_command(deviceid, new_command1)
time.sleep(5) #slepp 5 seconds
self.send_command(deviceid, new_command2)
def reset_retry(self):
"""Reset the retry counter"""
if self.retry != self._MAX_RETRY_PER_DEVICE * len(self.did):
Domoticz.Status("Reset retry counter")
self.retry = self._MAX_RETRY_PER_DEVICE * len(self.did)
def http(mode:str, url:str, headers:dict, data:str="", params:tuple|None=None)->dict:
"""HTTP GET/POST helper function"""
retries = 3
timeout = 10
retry = 0
while True:
try:
if mode.upper() == "GET":
if data != "":
raise ValueError("'data' shall be empty.")
response = requests.get(url, headers=headers, params=params, timeout=timeout).json()
Domoticz.Debug("HTTP GET Response:" + str(response))
else:
if params is not None:
raise ValueError("'params' shall be None.")
response = requests.post(url, headers=headers, data=data, timeout=timeout).json()
Domoticz.Debug("HTTP POST Response:" + str(response))
break
except Exception:
retry = retry + 1
if retry >= retries:
raise
time.sleep(0.5)
return response
global _plugin
_plugin = BasePlugin() _plugin = BasePlugin()
def onStart(): def onStart(): #NOSONAR #pylint: disable=invalid-name
global _plugin """OnStart"""
_plugin.onStart() _plugin.on_start()
def onCommand(Unit, Command, Level, Hue): def onCommand(DeviceID, Unit, Command, Level, Color): #NOSONAR #pylint: disable=invalid-name
global _plugin """OnCommand"""
_plugin.onCommand(Unit, Command, Level, Hue) _plugin.on_command(DeviceID, Unit, Command, Level, Color)
def onHeartbeat(): def onHeartbeat(): #NOSONAR #pylint: disable=invalid-name
global _plugin """onHeartbeat"""
_plugin.onHeartbeat() _plugin.on_heartbeat()
# Generic helper functions # Generic helper functions
def DumpConfigToLog(): def dump_config_to_log():
"""Dump the config to the Domoticz Log"""
for x in Parameters: for x in Parameters:
if Parameters[x] != "": if Parameters[x] != "":
Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'") Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Debug("Device count: " + str(len(Devices))) Domoticz.Debug("Device count: " + str(len(Devices)))
for x in Devices: for device_name in Devices:
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x])) device = Devices[device_name]
Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'") Domoticz.Debug("Device ID: '" + str(device.DeviceID) + "'")
Domoticz.Debug("Device Name: '" + Devices[x].Name + "'") Domoticz.Debug("--->Unit Count: '" + str(len(device.Units)) + "'")
Domoticz.Debug("Device nValue: " + str(Devices[x].nValue)) for unit_no in device.Units:
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'") unit = device.Units[unit_no]
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel)) Domoticz.Debug("--->Unit: " + str(unit_no))
Domoticz.Debug("--->Unit Name: '" + unit.Name + "'")
Domoticz.Debug("--->Unit nValue: " + str(unit.nValue))
Domoticz.Debug("--->Unit sValue: '" + unit.sValue + "'")
Domoticz.Debug("--->Unit LastLevel: " + str(unit.LastLevel))
return return