Compare commits

..

28 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
2 changed files with 259 additions and 164 deletions

1
.gitignore vendored
View File

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

422
plugin.py
View File

@@ -4,7 +4,7 @@
# #
#pylint: disable=line-too-long,broad-exception-caught,possibly-used-before-assignment #pylint: disable=line-too-long,broad-exception-caught,possibly-used-before-assignment
""" """
<plugin key="Heatzy_FJU" name="Heatzy Pilote" author="fjumelle" version="1.1.0" 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/>
@@ -12,14 +12,13 @@
<params> <params>
<param field="Username" label="Heatzy Username" width="200px" required="true" default=""/> <param field="Username" label="Heatzy Username" width="200px" required="true" default=""/>
<param field="Password" label="Heatzy Password" width="200px" required="true" default="" password="true"/> <param field="Password" label="Heatzy Password" width="200px" required="true" default="" password="true"/>
<param field="Mode3" label="Device name" width="200px" required="true" default=""/>
<param field="Mode4" label="'Off=Normal' bug?" width="200px"> <param field="Mode4" label="'Off=Normal' bug?" width="200px">
<options> <options>
<option label="No" value="0" default="true"/> <option label="No" value="0" default="true"/>
<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"/>
@@ -32,9 +31,10 @@
import math import math
import time import time
from datetime import datetime from datetime import datetime
from enum import IntEnum
import requests import requests
import Domoticz # type: ignore 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 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 Parameters = Parameters # type: ignore #NOSONAR #pylint: disable=undefined-variable,self-assigning-variable
@@ -45,7 +45,11 @@ HEATZY_MODE = {
'停止': 'OFF', '停止': 'OFF',
'解冻': 'FROSTFREE', '解冻': 'FROSTFREE',
'经济': 'ECONOMY', '经济': 'ECONOMY',
'舒适': 'NORMAL' '舒适': 'NORMAL',
'stop': 'OFF',
'fro': 'FROSTFREE',
'eco': 'ECONOMY',
'cft': 'NORMAL',
} }
HEATZY_MODE_NAME = { HEATZY_MODE_NAME = {
@@ -66,20 +70,26 @@ HEATZY_MODE_VALUE_INV = {v: k for k, v in HEATZY_MODE_VALUE.items()}
DEFAULT_POOLING = 60 DEFAULT_POOLING = 60
class HeatzyUnit(IntEnum):
"""2 units: Control and Selector"""
CONTROL = 1
SELECTOR = 2
class BasePlugin: class BasePlugin:
"""Class for plugin""" """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
@@ -105,65 +115,62 @@ class BasePlugin:
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
if 1 not in Devices: self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"])
Domoticz.Device(Name="Control", Unit=1, TypeName="Switch", Image=9, Used=1).Create()
if 2 not in Devices: # Get Devide Id
options = {"LevelActions": "||", self.did = self.get_heatzy_devices()
"LevelNames": HEATZY_MODE_NAME['OFF'] + "|" + HEATZY_MODE_NAME['FROSTFREE'] + "|" + HEATZY_MODE_NAME['ECONOMY'] + "|" + HEATZY_MODE_NAME['NORMAL'],
"LevelOffHidden": "false", #Bug with off mode... # max retry per device
#"LevelOffHidden": "true",t self.retry = self._MAX_RETRY_PER_DEVICE * len(self.did)
"SelectorStyle": "0"}
Domoticz.Device(Name="Mode", Unit=2, TypeName="Selector Switch", Switchtype=18, Image=15, # Create the child devices if these do not exist yet
Options=options, Used=1).Create() 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'],
"LevelOffHidden": "false", #Bug with off mode...
#"LevelOffHidden": "true",t
"SelectorStyle": "0"}
Domoticz.Unit(Name=f"Heatzy {alias} - Mode", DeviceID=deviceid, Unit=HeatzyUnit.SELECTOR,
TypeName="Selector Switch", Switchtype=18, Image=15,
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.get_token(Parameters["Username"], Parameters["Password"])
# Get Devide Id
self.did = self.get_device_id(self.token, Parameters["Mode3"])
# Get mode # Get mode
self.mode = self.get_mode() self.get_mode()
def on_command(self, unit, command, level, hue): #pylint: disable=unused-argument def on_command(self, DeviceID, Unit, Command, Level, Color): #pylint: disable=unused-argument,invalid-name
"""Send a command""" """Send a command"""
if unit == 1: if Unit == HeatzyUnit.CONTROL:
self.mode = self.on_off(command) self.send_command(DeviceID, Command)
elif unit == 2: elif Unit == HeatzyUnit.SELECTOR:
self.mode = self.set_mode(level) self.set_mode(DeviceID, Level)
def on_heartbeat(self): def on_heartbeat(self):
"""Time to heartbeat :)""" """Time to heartbeat :)"""
if self.pooling_current_step >= self.pooling_steps: if self.pooling_current_step >= self.pooling_steps:
Domoticz.Debug(f"Retry counter:{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.get_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 self.pooling_current_step = 1
else: else:
@@ -188,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" }'
time.sleep(0.5)
url = 'https://euapi.gizwits.com/app/login'
try: try:
time.sleep(0.5) response = http("post", url, headers, data)
url = 'https://euapi.gizwits.com/app/login'
response = requests.post(url, headers=headers, data=data, timeout=3).json()
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
@@ -208,59 +213,56 @@ 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(f"Cannot get Heatzy Token: {error_message} ({error_code})\n{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 get_device_id(self, token, alias): def get_heatzy_devices(self):
"""Get the device id from the token and the device name, using the Heatzy API""" """Get the device id from the token, using the Heatzy API"""
if token == "" or self.retry<0: if self.token == "" or self.retry<0:
return "" 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))
found = False
if 'devices' in response: if 'devices' in response:
devices = response['devices'] devices = response['devices']
unit = 1
for device in devices: for device in devices:
if "dev_alias" in device and "did" in device and device["dev_alias"].lower() == alias.lower(): if "dev_alias" in device and "did" in device and "product_key" in device:
found = True product_key = device['product_key']
self.did = device['did'] alias = device['dev_alias']
Domoticz.Status("Devide Id from Heatzy API: " + self.did) did = device['did']
self.did[product_key] = {"did":did, "alias":alias, "updated_at":0, "command_at":0}
if not found: Domoticz.Status(f"Devide Id from Heatzy API: {alias} - {did}")
self.did = "" unit = unit + 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 Devide Id: {error_message} ({error_code})\n{response}")
return self.did return self.did
@@ -270,7 +272,7 @@ class BasePlugin:
response = "" response = ""
self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"]) self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"])
self.did = self.get_device_id(self.token, Parameters["Mode3"]) #self.did = self.get_heatzy_devices() #Not really needed
if self.retry<0: if self.retry<0:
return "" return ""
@@ -280,71 +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 = 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.Debug(f"Get Mode for {[self.did[d]['alias'] for d in self.did]}...")
Domoticz.Error("Cannot open connection to Heatzy API to get the mode: " + str(exc)) for deviceid in self.did:
#Domoticz.Error("URL: " + str(url)) device = self.did[deviceid]
#Domoticz.Error("Headers: " + str(headers)) did = device["did"]
if 'response' in locals() and response != "": alias = device["alias"]
Domoticz.Error("Response: " + str(response))
return ""
Domoticz.Debug("Get Mode Response:" + str(response)) url = f"https://euapi.gizwits.com/app/devdata/{did}/latest"
try:
response = http("get", url, headers)
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})")
if 'attr' in response and 'mode' in response['attr']: #Decrease retry
mode = HEATZY_MODE[response['attr']['mode']] self.retry = self.retry - 1
Domoticz.Debug(f"Current Heatzy Mode: {HEATZY_MODE_NAME[mode]}")
#Reset retry counter continue
self.retry = self.max_retry
if Devices[2].nValue != HEATZY_MODE_VALUE[mode]: Domoticz.Debug(f"Get Mode Response for {alias}: {response}")
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: #Last Update
if mode == 'OFF' and Devices[1].nValue != 0: if 'updated_at' in response:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) if response["updated_at"] == device["updated_at"]:
elif mode != 'OFF' and Devices[1].nValue == 0: #No update since last heartbeat
Devices[1].Update(nValue=1, sValue="On", TimedOut = 0) 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']:
mode = HEATZY_MODE[response['attr']['mode']]
Domoticz.Debug(f"Current Heatzy Mode: {HEATZY_MODE_NAME[mode]} ({alias})")
#Reset retry counter
self.reset_retry()
if Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue != HEATZY_MODE_VALUE[mode]:
Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[mode]} ({alias})")
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 mode == 'OFF' and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
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:
if mode in ('OFF', 'FROSTFREE') and Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue != 0:
Devices[deviceid].Units[HeatzyUnit.CONTROL].nValue = 0
Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue = "Off"
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:
if mode in ('OFF', 'FROSTFREE') and Devices[1].nValue != 0: #Decrease retry
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) self.retry = self.retry - 1
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_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(f"Cannot get Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {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.set_mode(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 set_mode(self, mode): def set_mode(self, deviceid, mode):
"Set the device mode using the Heatzy API" "Set the device mode using the Heatzy API"
if Devices[2].nValue != mode: 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',
@@ -352,61 +391,112 @@ 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 = f"https://euapi.gizwits.com/app/control/{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 is not 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(f"New Heatzy Mode: {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(f"Cannot set Heatzy Mode: {error_message} ({error_code})\n{response}\nToken: {self.token}\nDeviceId: {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 on_off(self, command): if Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue != command:
"""Toggle device on/off"""
if Devices[1].sValue != command:
if command == "On": if command == "On":
self.mode = self.set_mode(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.set_mode(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.set_mode(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
_plugin = BasePlugin() _plugin = BasePlugin()
@@ -414,9 +504,9 @@ def onStart(): #NOSONAR #pylint: disable=invalid-name
"""OnStart""" """OnStart"""
_plugin.on_start() _plugin.on_start()
def onCommand(Unit, Command, Level, Hue): #NOSONAR #pylint: disable=invalid-name def onCommand(DeviceID, Unit, Command, Level, Color): #NOSONAR #pylint: disable=invalid-name
"""OnCommand""" """OnCommand"""
_plugin.on_command(Unit, Command, Level, Hue) _plugin.on_command(DeviceID, Unit, Command, Level, Color)
def onHeartbeat(): #NOSONAR #pylint: disable=invalid-name def onHeartbeat(): #NOSONAR #pylint: disable=invalid-name
"""onHeartbeat""" """onHeartbeat"""
@@ -429,11 +519,15 @@ def dump_config_to_log():
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