Files
domoticz-Heatzy/plugin.py
2025-10-26 22:52:40 +01:00

484 lines
21 KiB
Python
Executable File

# Heatzy python plugin for Domoticz
#
# Author: fjumelle
#
#pylint: disable=line-too-long,broad-exception-caught,possibly-used-before-assignment
"""
<plugin key="HeatzyEx" name="Heatzy Pilote Ex" author="fjumelle" version="2.1.0" wikilink="" externallink="">
<description>
<h2>Heatzy Pilote</h2><br/>
Implementation of Heatzy Pilote as a Domoticz Plugin.<br/>
</description>
<params>
<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="Mode4" label="'Off=Normal' bug?" width="200px">
<options>
<option label="No" value="0" default="true"/>
<option label="Yes" value="1"/>
</options>
</param>
<param field="Mode5" label="Polling interval (s)" width="40px" required="true" default="60"/>
<param field="Mode6" label="Logging Level" width="200px">
<options>
<option label="Normal" value="0" default="true"/>
<option label="Verbose" value="1"/>
</options>
</param>
</params>
</plugin>
"""
import math
import time
from datetime import datetime
from enum import IntEnum
import requests
import DomoticzEx as 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',
'stop': '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 HeatzyUnit(IntEnum):
"""2 units: Control and Selector"""
CONTROL = 1
SELECTOR = 2
class BasePlugin:
"""Class for plugin"""
debug = False
token = ""
token_expire_at = 0
did = {}
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)
# Get Token
self.token, self.token_expire_at = self.get_token(Parameters["Username"], Parameters["Password"])
# Get Devide Id
self.did = self.get_heatzy_devices()
# 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'],
"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?
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 mode
self.get_mode()
def on_command(self, DeviceID, Unit, Command, Level, Color): #pylint: disable=unused-argument,invalid-name
"""Send a command"""
if Unit == HeatzyUnit.CONTROL:
self.on_off(DeviceID, Command)
elif Unit == HeatzyUnit.SELECTOR:
self.set_mode(DeviceID, 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.get_mode()
self.pooling_current_step = 1
else:
# Devices["9420ae048da545c88fc6274d204dd25f"].Refresh()
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_heatzy_devices(self):
"""Get the device id from the token, using the Heatzy API"""
if self.token == "" or self.retry<0:
return ""
if len(self.did) == 0:
Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.")
headers = {
'Accept': 'application/json',
'X-Gizwits-User-token': self.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))
if 'devices' in response:
devices = response['devices']
unit = 1
for device in devices:
if "dev_alias" in device and "did" in device and "product_key" in device:
product_key = device['product_key']
alias = device['dev_alias']
did = device['did']
self.did[product_key] = {"did":did, "alias":alias, "updated_at":0}
Domoticz.Status(f"Devide Id from Heatzy API: {alias} - {did}")
unit = unit + 1
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_heatzy_devices() #Not really needed
if self.retry<0:
return ""
headers = {
'Accept': 'application/json',
'X-Gizwits-User-token': self.token,
'X-Gizwits-Application-Id': 'c70a66ff039d41b4a220e198b0fcc8b3',
}
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:
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(f"Cannot open connection to Heatzy API to get the mode for {alias}: {exc}")
#Domoticz.Error("URL: " + str(url))
#Domoticz.Error("Headers: " + str(headers))
if 'response' in locals() and response != "":
Domoticz.Error("Response: " + str(response))
else:
Domoticz.Debug(f"Cannot open connection to Heatzy API to get the mode for {alias}: {exc}")
continue
Domoticz.Debug(f"Get Mode Response for {alias}: {response}")
#Last Update
if 'updated_at' in response:
obsolete_min = int((time.time() - response["updated_at"])//60)
if response["updated_at"] == device["updated_at"]:
#No update since last heartbeat
if obsolete_min >= 180 and Devices[deviceid].TimedOut == 0:
Domoticz.Status(f"Last update from '{alias}' was {obsolete_min} min earlier.")
Devices[deviceid].TimedOut = 1
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.retry = self.max_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:
#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: {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(deviceid, HEATZY_MODE_VALUE['FROSTFREE'])
continue
# 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, deviceid, mode):
"Set the device mode using the Heatzy API"
if Devices[deviceid].Units[HeatzyUnit.SELECTOR].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 = '{"attrs": {"mode":' + str(mode_str[mode]) + '}}'
did = self.did[deviceid]["did"]
url = f"https://euapi.gizwits.com/app/control/{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
Domoticz.Debug("Set Mode Response:" + str(response))
if response is not None:
mode_str = HEATZY_MODE_VALUE_INV[mode]
Devices[deviceid].Units[HeatzyUnit.SELECTOR].nValue = int(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 mode_str == '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_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:
if mode_str 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_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:
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: {did}")
if error_code == 9004:
#Invalid token
self.token = ""
self.did = {}
return
#Device is correctly running ==> we reset the retry counter
self.retry = self.max_retry
def on_off(self, deviceid, command):
"""Toggle device on/off"""
if Devices[deviceid].Units[HeatzyUnit.CONTROL].sValue != command:
if command == "On":
self.set_mode(deviceid, HEATZY_MODE_VALUE['NORMAL'])
else:
if not self.bug:
self.set_mode(deviceid, HEATZY_MODE_VALUE['OFF'])
else:
#Because of issue with the equipment (Off do not work...)
self.set_mode(deviceid, HEATZY_MODE_VALUE['FROSTFREE'])
_plugin = BasePlugin()
def onStart(): #NOSONAR #pylint: disable=invalid-name
"""OnStart"""
_plugin.on_start()
def onCommand(DeviceID, Unit, Command, Level, Color): #NOSONAR #pylint: disable=invalid-name
"""OnCommand"""
_plugin.on_command(DeviceID, Unit, Command, Level, Color)
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 device_name in Devices:
device = Devices[device_name]
Domoticz.Debug("Device ID: '" + str(device.DeviceID) + "'")
Domoticz.Debug("--->Unit Count: '" + str(len(device.Units)) + "'")
for unit_no in device.Units:
unit = device.Units[unit_no]
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