Compare commits

..

3 Commits

2 changed files with 95 additions and 87 deletions

4
.gitignore vendored Normal file
View File

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

178
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="Heatzy_FJU" name="Heatzy Pilote" author="fjumelle" version="1.1.0" 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/>
@@ -11,6 +12,7 @@
<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"/>
@@ -27,16 +29,17 @@
</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 datetime import datetime, timedelta
global Parameters import requests
global Devices 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 = { HEATZY_MODE = {
'停止': 'OFF', '停止': 'OFF',
@@ -63,14 +66,8 @@ HEATZY_MODE_VALUE_INV = {v: k for k, v in HEATZY_MODE_VALUE.items()}
DEFAULT_POOLING = 60 DEFAULT_POOLING = 60
class deviceparam:
def __init__(self, unit, nvalue, svalue):
self.unit = unit
self.nvalue = nvalue
self.svalue = svalue
class BasePlugin: class BasePlugin:
"""Class for plugin"""
debug = False debug = False
token = "" token = ""
token_expire_at = 0 token_expire_at = 0
@@ -87,15 +84,14 @@ class BasePlugin:
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,26 +99,23 @@ 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 # create the child devices if these do not exist yet
devicecreated = []
if 1 not in Devices: if 1 not in Devices:
Domoticz.Device(Name="Control", Unit=1, TypeName="Switch", Image=9, Used=1).Create() Domoticz.Device(Name="Control", Unit=1, TypeName="Switch", Image=9, Used=1).Create()
devicecreated.append(deviceparam(1, 0, "Off")) # default is Off
if 2 not in Devices: if 2 not in Devices:
Options = {"LevelActions": "||", 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.Device(Name="Mode", Unit=2, TypeName="Selector Switch", Switchtype=18, Image=15,
Options=Options, Used=1).Create() Options=options, Used=1).Create()
devicecreated.append(deviceparam(2, 0, "30")) # default is confort mode
# Bug Off = Normal? # Bug Off = Normal?
if str(Parameters["Mode4"]) != "0": if str(Parameters["Mode4"]) != "0":
@@ -130,23 +123,25 @@ class BasePlugin:
self.bug = True self.bug = True
# Get Token # Get Token
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"])
# Get Devide Id # Get Devide Id
self.did = self.getDevideId(self.token) self.did = self.get_device_id(self.token, Parameters["Mode3"])
# Get mode # Get mode
self.mode = self.getMode() self.mode = self.get_mode()
def onCommand(self, Unit, Command, Level, Hue):
if Unit == 1:
self.mode = self.onOff(Command)
elif Unit == 2:
self.mode = self.setMode(Level)
def onHeartbeat(self): 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: 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
@@ -157,13 +152,13 @@ class BasePlugin:
self.did = "" self.did = ""
return return
self.mode = self.getMode() self.mode = self.get_mode()
# If mode = OFF and bug, then mode = FROSTFREE # If mode = OFF and bug, then mode = FROSTFREE
if self.bug and self.mode == 'OFF': if self.bug and self.mode == 'OFF':
Domoticz.Log("Switch to FROSTFREE because of the OFF bug...") Domoticz.Log("Switch to FROSTFREE because of the OFF bug...")
self.mode = self.setMode(HEATZY_MODE_VALUE['FROSTFREE']) self.mode = self.set_mode(HEATZY_MODE_VALUE['FROSTFREE'])
# check if need to refresh device so that they do not turn red in GUI # check if need to refresh device so that they do not turn red in GUI
#now = datetime.now() #now = datetime.now()
#if self.nextupdate <= now: #if self.nextupdate <= now:
@@ -173,10 +168,9 @@ class BasePlugin:
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 == "":
@@ -186,7 +180,7 @@ class BasePlugin:
#Token will expire in less than 1 day #Token will expire in less than 1 day
need_to_get_token = True need_to_get_token = True
Domoticz.Status("Heatzy Token expired, need to call Heatzy API.") Domoticz.Status("Heatzy Token expired, need to call Heatzy API.")
if need_to_get_token and self.retry>=0: if need_to_get_token and self.retry>=0:
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -218,19 +212,20 @@ class BasePlugin:
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_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: if token == "" or self.retry<0:
return "" return ""
if self.did == "": if self.did == "":
Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.") Domoticz.Status("Heatzy Devide Id unknown, need to call Heatzy API.")
@@ -249,42 +244,49 @@ class BasePlugin:
#Domoticz.Error("Headers: " + str(headers)) #Domoticz.Error("Headers: " + str(headers))
#Domoticz.Error("Params: " + str(params)) #Domoticz.Error("Params: " + str(params))
return "" return ""
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]: found = False
self.did = response['devices'][0]['did'] if 'devices' in response:
Domoticz.Status("Devide Id from Heatzy API: " + self.did) devices = response['devices']
else: 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 = "" self.did = ""
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 Devide Id: {} ({})\n{}".format(error_message, error_code, response)) Domoticz.Error(f"Cannot get Heatzy Devide Id: {error_message} ({error_code})\n{response}")
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_device_id(self.token, Parameters["Mode3"])
if self.retry<0: if self.retry<0:
return "" return ""
headers = { headers = {
'Accept': 'application/json', 'Accept': 'application/json',
'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) url = f"https://euapi.gizwits.com/app/devdata/{self.did}/latest"
try: try:
response = requests.get(url, headers=headers, timeout=3).json() response = requests.get(url, headers=headers, timeout=3).json()
except Exception as exc: except Exception as exc:
#Decrease retry #Decrease retry
self.retry = self.retry - 1 self.retry = self.retry - 1
if self.retry < self.max_retry//2: if self.retry < self.max_retry//2:
Domoticz.Error("Cannot open connection to Heatzy API to get the mode: " + str(exc)) Domoticz.Error("Cannot open connection to Heatzy API to get the mode: " + str(exc))
#Domoticz.Error("URL: " + str(url)) #Domoticz.Error("URL: " + str(url))
@@ -297,13 +299,13 @@ class BasePlugin:
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]}")
#Reset retry counter #Reset retry counter
self.retry = self.max_retry self.retry = self.max_retry
if Devices[2].nValue != HEATZY_MODE_VALUE[mode]: if Devices[2].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]}")
Devices[2].Update(nValue=HEATZY_MODE_VALUE[mode], sValue=str(HEATZY_MODE_VALUE[mode]), TimedOut = 0) Devices[2].Update(nValue=HEATZY_MODE_VALUE[mode], sValue=str(HEATZY_MODE_VALUE[mode]), TimedOut = 0)
if not self.bug: if not self.bug:
@@ -322,7 +324,7 @@ class BasePlugin:
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: {self.did}")
if error_code == 9004: if error_code == 9004:
#Invalid token #Invalid token
self.token = "" self.token = ""
@@ -330,12 +332,13 @@ class BasePlugin:
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(HEATZY_MODE_VALUE['FROSTFREE'])
return "" return ""
return mode return mode
def setMode(self, mode): def set_mode(self, mode):
"Set the device mode using the Heatzy API"
if Devices[2].nValue != mode: if Devices[2].nValue != mode:
mode_str = { mode_str = {
HEATZY_MODE_VALUE['NORMAL']: '[1,1,0]', HEATZY_MODE_VALUE['NORMAL']: '[1,1,0]',
@@ -350,7 +353,7 @@ class BasePlugin:
'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) url = f"https://euapi.gizwits.com/app/control/{self.did}"
try: try:
response = requests.post(url, headers=headers, data=data, timeout=3).json() response = requests.post(url, headers=headers, data=data, timeout=3).json()
except Exception as exc: except Exception as exc:
@@ -362,10 +365,10 @@ class BasePlugin:
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] self.mode = HEATZY_MODE_VALUE_INV[mode]
Devices[2].Update(nValue=int(mode), sValue=str(mode)) Devices[2].Update(nValue=int(mode), sValue=str(mode))
Domoticz.Status("New Heatzy Mode: {}".format(HEATZY_MODE_NAME[self.mode])) Domoticz.Status(f"New Heatzy Mode: {HEATZY_MODE_NAME[self.mode]}")
if not self.bug: if not self.bug:
if self.mode == 'OFF' and Devices[1].nValue != 0: if self.mode == 'OFF' and Devices[1].nValue != 0:
Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0) Devices[1].Update(nValue=0, sValue="Off", TimedOut = 0)
@@ -379,7 +382,7 @@ class BasePlugin:
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: {self.did}")
if error_code == 9004: if error_code == 9004:
#Invalid token #Invalid token
self.token = "" self.token = ""
@@ -391,36 +394,37 @@ class BasePlugin:
return self.mode return self.mode
def onOff(self, command): def on_off(self, command):
"""Toggle device on/off"""
if Devices[1].sValue != command: if Devices[1].sValue != command:
if command == "On": if command == "On":
self.mode = self.setMode(HEATZY_MODE_VALUE['NORMAL']) self.mode = self.set_mode(HEATZY_MODE_VALUE['NORMAL'])
else: else:
if not self.bug: if not self.bug:
self.mode = self.setMode(HEATZY_MODE_VALUE['OFF']) self.mode = self.set_mode(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.mode = self.set_mode(HEATZY_MODE_VALUE['FROSTFREE'])
return self.mode return self.mode
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(Unit, Command, Level, Hue): #NOSONAR #pylint: disable=invalid-name
global _plugin """OnCommand"""
_plugin.onCommand(Unit, Command, Level, Hue) _plugin.on_command(Unit, Command, Level, Hue)
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]) + "'")