From 15e7745d90d5fa02fb87c3a0b33743a33ca6f41a Mon Sep 17 00:00:00 2001 From: JimBo CA Date: Sun, 9 Dec 2018 15:13:28 -0800 Subject: [PATCH] #4 #5 for 2.0.6 --- README.md | 33 +++++++++++--- ecobee-poly.py | 33 +++++++++++++- node_types.py | 90 +++++++++++++++++++++++++++----------- profile/editor/editors.xml | 2 +- profile/version.txt | 2 +- server.json | 2 +- zipprofile | 5 --- 7 files changed, 124 insertions(+), 43 deletions(-) mode change 100644 => 100755 README.md mode change 100644 => 100755 ecobee-poly.py mode change 100644 => 100755 node_types.py mode change 100644 => 100755 server.json delete mode 100755 zipprofile diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 7633c13..b39a4cd --- a/README.md +++ b/README.md @@ -6,17 +6,36 @@ Install through the NodeServer Store #### Requirements -On first start up you will be presented a PIN. +On first start up you will be given a PIN. -Login to the Ecobee web page, click on your profile, then -click 'My Apps' > 'Add Application'. +Login to the Ecobee web page, click on your profile, then +click 'My Apps' > 'Add Application'. -You will be prompted to enter the PIN provided. Once you approve -the integration, restart the nodeserver within 10 minutes of -being given the PIN. +You will be prompted to enter the PIN provided. + +The nodeserver will check every 60 seconds that you have completed the approval +so do not restart the nodeserver. You can monitor the log to see when the +approval is recognized. Your thermostat will be added to ISY, along with nodes for any sensors, a node for the current weather, and a node for the forecast. -After the first run. It will refresh any changes every 3 minutes. This is +After the first run. It will refresh any changes every 3 minutes. This is a limitation imposed by Ecobee. + +# Upgrading + +1. Open the Polyglot web page + 1. Go to nodeserver store and click "Update" for "Ecobee". + 1. Go to the dashboard, select Details for the Ecobee Nodeserver + 1. Click Restart +1. If the release has a (Profile Change) then the profile will be updated automatically but if you had the Admin Console open, you will need to close and open it again. + +# Release Notes + +- 2.0.6: JimBoCA + - [Fix lookup for setting Mode](https://github.com/Einstein42/udi-ecobee-poly/issues/4) + - [Fix crash when changing schedule mode](https://github.com/Einstein42/udi-ecobee-poly/issues/5) + - Fix "Climate Type" initialization when there is a manual change + - Automatically upload new profile when it is out of date. + - Change current temp for F to include one signficant digit, since that's what is sent. diff --git a/ecobee-poly.py b/ecobee-poly.py old mode 100644 new mode 100755 index ef79af5..f400a07 --- a/ecobee-poly.py +++ b/ecobee-poly.py @@ -40,6 +40,7 @@ def start(self): #self.removeNoticesAll() LOGGER.info('Started Ecobee v2 NodeServer') LOGGER.debug(self.polyConfig['customData']) + self.check_profile() if 'tokenData' in self.polyConfig['customData']: self.tokenData = self.polyConfig['customData']['tokenData'] self.auth_token = self.tokenData['access_token'] @@ -105,7 +106,7 @@ def _getRefresh(self): if 'error' in data: LOGGER.error('{} :: {}'.format(data['error'], data['error_description'])) self.auth_token = None - self.refreshingTokens = False + self.refreshingTokens = False return False elif 'access_token' in data: self._saveTokens(data) @@ -226,6 +227,7 @@ def discover(self, *args, **kwargs): tstat = fullData['thermostatList'][0] useCelsius = True if tstat['settings']['useCelsius'] else False self.addNode(Thermostat(self, address, address, 'Ecobee - {}'.format(thermostat['name']), thermostat, fullData, useCelsius)) + # TODO: Adding remoteSensors and weather should be done inside thermostat so we know it was created since it's the parent time.sleep(3) if 'remoteSensors' in tstat: for sensor in tstat['remoteSensors']: @@ -245,6 +247,33 @@ def discover(self, *args, **kwargs): self.discovery = False return True + def get_profile_info(self): + pvf = 'profile/version.txt' + try: + with open(pvf) as f: + pv = f.read().replace('\n', '') + f.close() + except Exception as err: + LOGGER.error('get_profile_info: failed to read file {0}: {1}'.format(pvf,err), exc_info=True) + pv = 0 + return { 'version': pv } + + def check_profile(self): + self.profile_info = self.get_profile_info() + # Set Default profile version if not Found + cdata = deepcopy(self.polyConfig['customData']) + LOGGER.info('check_profile: profile_info={0} customData={1}'.format(self.profile_info,cdata)) + if not 'profile_info' in cdata: + cdata['profile_info'] = { 'version': 0 } + if self.profile_info['version'] == cdata['profile_info']['version']: + self.update_profile = False + else: + self.update_profile = True + self.poly.installprofile() + LOGGER.info('check_profile: update_profile={}'.format(self.update_profile)) + cdata['profile_info'] = self.profile_info + self.saveCustomData(cdata) + def getThermostats(self): if not self._checkTokens(): LOGGER.debug('getThermostat failed. Couldn\'t get tokens.') @@ -367,4 +396,4 @@ def ecobeePost(self, thermostatId, postData = {}): control = Controller(polyglot) control.runForever() except (KeyboardInterrupt, SystemExit): - sys.exit(0) \ No newline at end of file + sys.exit(0) diff --git a/node_types.py b/node_types.py old mode 100644 new mode 100755 index 56cf120..6a3e18e --- a/node_types.py +++ b/node_types.py @@ -5,6 +5,8 @@ except ImportError: import pgc_interface as polyinterface from copy import deepcopy +# For debugging only +#import json LOGGER = polyinterface.LOGGER @@ -152,7 +154,6 @@ def toF(tempC): class Thermostat(polyinterface.Node): def __init__(self, controller, primary, address, name, revData, fullData, useCelsius): - super().__init__(controller, primary, address, name) self.controller = controller self.name = name self.tstat = fullData['thermostatList'][0] @@ -164,27 +165,36 @@ def __init__(self, controller, primary, address, name, revData, fullData, useCel self.drivers = self._convertDrivers(driversMap[self.id]) if self.controller._cloud else deepcopy(driversMap[self.id]) self.revData = revData self.fullData = fullData - + super(Thermostat, self).__init__(controller, primary, address, name) + def start(self): self.update(self.revData, self.fullData) def update(self, revData, fullData): self.revData = revData self.fullData = fullData + #LOGGER.debug("fullData={}".format(json.dumps(fullData, sort_keys=True, indent=2))) + #LOGGER.debug("revData={}".format(json.dumps(revData, sort_keys=True, indent=2))) self.tstat = fullData['thermostatList'][0] self.program = self.tstat['program'] events = self.tstat['events'] equipmentStatus = self.tstat['equipmentStatus'].split(',') self.settings = self.tstat['settings'] + #LOGGER.debug("update: settings={}".format(self.settings)) runtime = self.tstat['runtime'] clihcs = 0 for status in equipmentStatus: if status in equipmentStatusMap: clihcs = equipmentStatusMap[status] break + # This seems to be what the schedule says should be enabled. + climateType = self.program['currentClimateRef'] + # And the default mode, unless there is an event clismd = 0 if len(events) > 0 and events[0]['type'] == 'hold' and events[0]['running']: clismd = 1 if self.settings['holdAction'] == 'nextPeriod' else 2 + climateType = events[0]['holdClimateRef'] + LOGGER.debug("clismd={} climateType={}".format(clismd,climateType)) tempCurrent = runtime['actualTemperature'] / 10 if runtime['actualTemperature'] != 0 else 0 tempHeat = runtime['desiredHeat'] / 10 tempCool = runtime['desiredCool'] / 10 @@ -192,7 +202,15 @@ def update(self, revData, fullData): tempCurrent = toC(tempCurrent) tempHeat = toC(tempHeat) tempCool = toC(tempCool) - + else: + # F set points must be integer + tempHeat = int(tempHeat) + tempCool = int(tempCool) + + #LOGGER.debug("program['climates']={}".format(self.program['climates'])) + #LOGGER.debug("settings={}".format(json.dumps(self.settings, sort_keys=True, indent=2))) + #LOGGER.debug("program={}".format(json.dumps(self.program, sort_keys=True, indent=2))) + updates = { 'ST': tempCurrent, 'CLISPH': tempHeat, @@ -204,7 +222,11 @@ def update(self, revData, fullData): 'GV1': runtime['desiredHumidity'], 'CLISMD': clismd, 'GV4': self.settings['fanMinOnTime'], - 'GV3': climateMap[self.program['currentClimateRef']], + # This assumes our climate is in the last hash in the array + # thought it would work, but still has issues... + #'GV3': climateMap[self.program['climates'][-1]['climateRef']], + #'GV3': climateMap[self.program['climates'][-1]['climateRef']], + 'GV3': climateMap[climateType], 'GV5': runtime['desiredDehumidity'], 'GV6': 1 if self.settings['autoAway'] else 0, 'GV7': 1 if self.settings['followMeComfort'] else 0 @@ -224,7 +246,7 @@ def update(self, revData, fullData): node.update(weather) def query(self, command=None): - self.reportDrivers() + self.reportDrivers() def cmdSetPoint(self, cmd): if cmd['cmd'] == 'CLISPH': @@ -243,17 +265,29 @@ def cmdSetPoint(self, cmd): climate[cmdtype] = int(cmd['value']) * 10 if self.controller.ecobeePost(self.address, {'thermostat': {'program': currentProgram}}): self.setDriver(driver, cmd['value']) + LOGGER.debug("getDriver({})={}".format(driver,self.getDriver(driver))) + + def getMapName(self,map,val): + for name in map: + if map[name] == val: + return name def cmdSetMode(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: - LOGGER.info('Setting Thermostat {} to mode: {}'.format(self.name, [*modeMap][int(cmd['value'])])) - if self.controller.ecobeePost(self.address, {'thermostat': {'settings': {'hvacMode': [*modeMap][int(cmd['value'])]}}}): + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {} already set to {}".format(cmd['cmd'],int(cmd['value']))) + else: + name = self.getMapName(modeMap,int(cmd['value'])) + LOGGER.info('Setting Thermostat {} to mode: {} (value={})'.format(self.name, name, cmd['value'])) + if self.controller.ecobeePost(self.address, {'thermostat': {'settings': {'hvacMode': name}}}): self.setDriver(cmd['cmd'], cmd['value']) + def cmdSetScheduleMode(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {} already set to {}".format(cmd['cmd'],int(cmd['value']))) + else: func = {} - if cmd['value'] == '0': + if int(cmd['value']) == 0: func['type'] = 'resumeProgram' func['params'] = { 'resumeAll': False @@ -261,26 +295,28 @@ def cmdSetScheduleMode(self, cmd): else: func['type'] = 'setHold' heatHoldTemp = int(self.getDriver('CLISPH')) - coolHoldTemp = int(self.getDriver('CLISPH')) + coolHoldTemp = int(self.getDriver('CLISPC')) if self.useCelsius: headHoldTemp = toF(heatHoldTemp) coolHoldTemp = toF(coolHoldTemp) func['params'] = { - 'holdType': 'nextTransition' if cmd['value'] == "1" else 'indefinite', + 'holdType': 'nextTransition' if int(cmd['value']) == 1 else 'indefinite', 'heatHoldTemp': heatHoldTemp * 10, 'coolHoldTemp': coolHoldTemp * 10 } if self.controller.ecobeePost(self.address, {'functions': [func]}): - self.setDriver('CLISMD', cmd['value']) + self.setDriver('CLISMD', int(cmd['value'])) def cmdSetClimate(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {}={} already set to {}".format(cmd['cmd'],int(self.getDriver(cmd['cmd'])),int(cmd['value']))) + else: command = { 'functions': [{ 'type': 'setHold', 'params': { 'holdType': 'indefinite', - 'holdClimateRef': [*climateMap][int(cmd['value'])] + 'holdClimateRef': self.getMapName(climateMap,int(cmd['value'])) } }] } @@ -288,7 +324,9 @@ def cmdSetClimate(self, cmd): self.setDriver(cmd['cmd'], cmd['value']) def cmdSetFanOnTime(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {} already set to {}".format(cmd['cmd'],int(cmd['value']))) + else: command = { 'thermostat': { 'settings': { @@ -300,7 +338,9 @@ def cmdSetFanOnTime(self, cmd): self.setDriver(cmd['cmd'], cmd['value']) def cmdSmartHome(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {} already set to {}".format(cmd['cmd'],int(cmd['value']))) + else: command = { 'thermostat': { 'settings': { @@ -312,7 +352,9 @@ def cmdSmartHome(self, cmd): self.setDriver(cmd['cmd'], cmd['value']) def cmdFollowMe(self, cmd): - if self.getDriver(cmd['cmd']) != cmd['value']: + if int(self.getDriver(cmd['cmd'])) == int(cmd['value']): + LOGGER.debug("cmdSetClimate: {} already set to {}".format(cmd['cmd'],int(cmd['value']))) + else: command = { 'thermostat': { 'settings': { @@ -349,10 +391,6 @@ def setPoint(self, cmd): if self.controller.ecobeePost(self.address, {'thermostat': {'program': currentProgram}}): self.setDriver(driver, newTemp) - def getDriver(self, driver): - if driver in self.drivers: - return self.drivers[driver]['value'] - commands = { 'QUERY': query, 'CLISPH': cmdSetPoint, 'CLISPC': cmdSetPoint, @@ -374,7 +412,7 @@ def __init__(self, controller, primary, address, name, useCelsius): self.useCelsius = useCelsius self.id = 'EcobeeSensorC' if self.useCelsius else 'EcobeeSensorF' self.drivers = self._convertDrivers(driversMap[self.id]) if self.controller._cloud else deepcopy(driversMap[self.id]) - + def start(self): pass @@ -405,7 +443,7 @@ def __init__(self, controller, primary, address, name, useCelsius, forecast): self.useCelsius = useCelsius self.id = 'EcobeeWeatherC' if self.useCelsius else 'EcobeeWeatherF' self.drivers = self._convertDrivers(driversMap[self.id]) if self.controller._cloud else deepcopy(driversMap[self.id]) - + def start(self): pass @@ -438,8 +476,8 @@ def update(self, weather): } for key, value in updates.items(): self.setDriver(key, value) - + def query(self, command=None): self.reportDrivers() - commands = {'QUERY': query, 'STATUS': query} \ No newline at end of file + commands = {'QUERY': query, 'STATUS': query} diff --git a/profile/editor/editors.xml b/profile/editor/editors.xml index e47407b..30720af 100644 --- a/profile/editor/editors.xml +++ b/profile/editor/editors.xml @@ -9,7 +9,7 @@ - + diff --git a/profile/version.txt b/profile/version.txt index 38f77a6..157e54f 100644 --- a/profile/version.txt +++ b/profile/version.txt @@ -1 +1 @@ -2.0.1 +2.0.6 diff --git a/server.json b/server.json old mode 100644 new mode 100755 index f681c40..f44a631 --- a/server.json +++ b/server.json @@ -14,7 +14,7 @@ { "title": "ecobee: Polyglot NodeServer for Ecobee", "author": "James Milne (Einstein.42)", - "version": "2.0.5", + "version": "2.0.6", "date": "July 25, 2018", "source": "https://github.com/Einstein42/udi-ecobee-poly", "license": "https://github.com/Einstein42/udi-ecobee-poly/master/LICENSE" diff --git a/zipprofile b/zipprofile deleted file mode 100755 index 215ac55..0000000 --- a/zipprofile +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -cd profile -rm -f ../profile.zip -zip -r ../profile.zip *