From f5b5af33eb707eda9f9effe8672cc42c6bf63507 Mon Sep 17 00:00:00 2001 From: gruberth Date: Thu, 20 Apr 2023 13:14:44 +0200 Subject: [PATCH 001/118] husky2: Added a poll function in addition to the websocket connection, to trigger state updates more regularly --- husky2/__init__.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/husky2/__init__.py b/husky2/__init__.py index 1809dafe9..507196512 100755 --- a/husky2/__init__.py +++ b/husky2/__init__.py @@ -27,7 +27,7 @@ import asyncio import threading from concurrent.futures import CancelledError -from datetime import datetime +from datetime import datetime, timedelta import time import json @@ -233,6 +233,9 @@ def __init__(self, sh): self.historylength = int(self.get_parameter_value('historylength')) self.maxgpspoints = int(self.get_parameter_value('maxgpspoints')) + # poll is only additional, because normal state updates are recieved by the websocket connection of the api + self.poll_cycle = 600 # call every 10 min to make sure the monthly api call limit of 10000 gets not exceeded + self.token = None self.tokenExp = 0 @@ -280,7 +283,7 @@ def run(self): Run method for the plugin """ # if you need to create child threads, do not make them daemon = True! - # They will not shutdown properly. (It's a python bug) + # They will not shut down properly. (It's a python bug) self.logger.debug("Run method called") try: @@ -328,11 +331,16 @@ def startFinished(self, args): self.alive = True self.logger.debug("Init finished, husky2 plugin is running") + dt = self.shtime.now() + timedelta(seconds=self.poll_cycle) + self.scheduler_add('poll_husky_device_' + self.instance, + self.poll_device, cycle=self.poll_cycle, prio=5, next=dt) + def stop(self): """ Stop method for the plugin """ self.logger.debug("Stop method called. Shutting down Thread...") + self.scheduler_remove('poll_husky_device_' + self.instance) self.asyncLoop.call_soon_threadsafe(self.asyncLoop.stop) time.sleep(2) try: @@ -463,6 +471,10 @@ def writeToStatusItem(self, txt): for item in self._items_state['message']: item(txt, self.get_shortname()) + def poll_device(self): + self.logger.debug("Poll new status") + asyncio.run_coroutine_threadsafe(self.update_worker(), self.asyncLoop) + def data_callback(self, status): """ Callback for data updates of the device @@ -491,7 +503,8 @@ def data_callback(self, status): posindex = -1 for gpsindex, gpspoint in enumerate(data['attributes']['positions']): - if (gpspoint['longitude'] == self.mowerGpspoints.get_last()[0]) and (gpspoint['latitude'] == self.mowerGpspoints.get_last()[1]): + if (gpspoint['longitude'] == self.mowerGpspoints.get_last()[0]) and ( + gpspoint['latitude'] == self.mowerGpspoints.get_last()[1]): posindex = gpsindex - 1 break elif gpsindex >= self.maxgpspoints: @@ -663,6 +676,12 @@ async def send_worker(self, cmd, value): self.logger.error("'{0}' not in available commands: {1}".format(cmd, commands.keys())) return + async def update_worker(self): + newstatus = await self.apiSession.get_status() + self.apiSession.action + self.data_callback(newstatus) + return + # ------------------------------------------ # Webinterface methods of the plugin # ------------------------------------------ From 6d81bd036d00d66fe64570e24df58eeccac56da0 Mon Sep 17 00:00:00 2001 From: gruberth Date: Thu, 20 Apr 2023 18:40:23 +0200 Subject: [PATCH 002/118] husky2: Changed script loading in maps widget --- husky2/sv_widgets/husky2.js | 97 ++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 56 deletions(-) diff --git a/husky2/sv_widgets/husky2.js b/husky2/sv_widgets/husky2.js index 640c1760c..6997ad434 100755 --- a/husky2/sv_widgets/husky2.js +++ b/husky2/sv_widgets/husky2.js @@ -1,88 +1,73 @@ - $.widget("sv.husky2", $.sv.widget, { initSelector: 'div[data-widget="husky2.map"]', options: { - mapskey: '', + mapskey: '', zoomlevel: 19, pathcolor: '#3afd02', - }, + }, _create: function () { this._super(); - this._create_map(); + + const scriptPromise = new Promise((resolve, reject) => { + const script = document.createElement('script'); + document.head.appendChild(script); + script.onload = resolve; + script.onerror = reject; + script.async = true; + script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.options.mapskey + '&callback=Function.prototype'; + }); + scriptPromise.then(() => { + this._create_map() + }); }, _create_map: function () { - try { - this.map = new google.maps.Map(this.element[0], { - zoom: this.options.zoomlevel, - mapTypeId: 'hybrid', - center: new google.maps.LatLng(0.0, 0.0), - }); - } - catch (e) { - if (e.name == "ReferenceError") { // google maps script not loaded yet - var that = this; - // google maps script is already loading in another widget - if (window.google_maps_loading) { - window.setTimeout(function () { - that._create_map() - }, 100) - return; - } - // google maps script is not loading - window.google_maps_loading = true; - $.ajax({ - url: 'https://maps.googleapis.com/maps/api/js?key=' + this.options.mapskey + '&language=de', - dataType: "script", - complete: function () { - window.google_maps_loading = false; - that._create_map() - } - }); - return; - } - else // other exceptions should be thrown - throw e; - } - this.marker_myself = new google.maps.Marker({ - map: this.map, - position: new google.maps.LatLng(0.0, 0.0), - icon: '', - title:'', - zIndex:99999999 - }); + this.map = new google.maps.Map(this.element[0], { + zoom: this.options.zoomlevel, + mapTypeId: 'hybrid', + center: new google.maps.LatLng(0.0, 0.0), + }); - this.linePath = new google.maps.Polyline({ - path: [], - strokeColor: this.options.pathcolor, - strokeOpacity: 0.6, - strokeWeight: 2, - map: this.map - }); + this.marker_myself = new google.maps.Marker({ + map: this.map, + position: new google.maps.LatLng(0.0, 0.0), + icon: '', + title: '', + zIndex: 99999999 + }); + this.linePath = new google.maps.Polyline({ + path: [], + strokeColor: this.options.pathcolor, + strokeOpacity: 0.6, + strokeWeight: 2, + map: this.map + }); }, - _update: function(response) { - if(!this.map) { + _update: function (response) { + if (!this.map) { var that = this; - window.setTimeout(function() { that._update(response) }, 100) + window.setTimeout(function () { + that._update(response) + }, 500) return; } this.marker_myself.setTitle(response[3]); - var pos = new google.maps.LatLng(parseFloat(response[0]),parseFloat(response[1])); + var pos = new google.maps.LatLng(parseFloat(response[0]), parseFloat(response[1])); this.map.setCenter(pos); this.marker_myself.setPosition(pos); var coord = []; - for (const point of response[2]){ - coord.push(new google.maps.LatLng(parseFloat(point[0]),parseFloat(point[1]))); + for (const point of response[2]) { + coord.push(new google.maps.LatLng(parseFloat(point[0]), parseFloat(point[1]))); } this.linePath.setPath(coord); From 82ba1a3175fffc32b24738e412582f502e772cff Mon Sep 17 00:00:00 2001 From: gruberth Date: Thu, 20 Apr 2023 19:25:37 +0200 Subject: [PATCH 003/118] husky2: Check first if widget script already loaded --- husky2/sv_widgets/husky2.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/husky2/sv_widgets/husky2.js b/husky2/sv_widgets/husky2.js index 6997ad434..c2ef89edb 100755 --- a/husky2/sv_widgets/husky2.js +++ b/husky2/sv_widgets/husky2.js @@ -10,17 +10,21 @@ $.widget("sv.husky2", $.sv.widget, { _create: function () { this._super(); - const scriptPromise = new Promise((resolve, reject) => { - const script = document.createElement('script'); - document.head.appendChild(script); - script.onload = resolve; - script.onerror = reject; - script.async = true; - script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.options.mapskey + '&callback=Function.prototype'; - }); - scriptPromise.then(() => { - this._create_map() - }); + // First check if the script already exists on the dom by searching for an id + if (document.getElementById('googleMapsScript') === null) { + const scriptPromise = new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.id = 'googleMapsScript'; + script.onload = resolve; + script.onerror = reject; + script.async = true; + script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.options.mapskey + '&callback=Function.prototype'; + document.body.appendChild(script); + }); + scriptPromise.then(() => { + this._create_map(); + }); + } }, _create_map: function () { From 5a75ec5065d043d3747b26f6ef15c6aacd64137b Mon Sep 17 00:00:00 2001 From: gruberth Date: Fri, 21 Apr 2023 17:36:38 +0200 Subject: [PATCH 004/118] husky2: Changed script import to speed up sv page containing the map --- husky2/sv_widgets/husky2.html | 42 +++++++++++++++++++---------------- husky2/sv_widgets/husky2.js | 37 ++++++++++-------------------- 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/husky2/sv_widgets/husky2.html b/husky2/sv_widgets/husky2.html index 1207b1338..84b35ff67 100755 --- a/husky2/sv_widgets/husky2.html +++ b/husky2/sv_widgets/husky2.html @@ -8,24 +8,28 @@ */ /** - * Displays a google maps (from https://www.smarthomeng.de/google-maps-widget-fuer-smartvisu-2-9 ) map with the position and the path of the mower - * - * @param {id=''} unique id for this widget - * @param {item(txt)=''} a gad/item with the name of the mower - * @param {item(num)=0.0} a gad/item for latitude - * @param {item(num)=0.0} a gad/item for longitude - * @param {item(list)=[]} a gad/item for gps points - * @param {text=''} the google maps key - * @param {num=19} zoom level for map - * @param {text='#3afd02'} color of mower path - */ +* Displays a google maps (from https://www.smarthomeng.de/google-maps-widget-fuer-smartvisu-2-9 ) map with the position and the path of the mower +* +* @param {id=''} unique id for this widget +* @param {item(txt)=''} a gad/item with the name of the mower +* @param {item(num)=0.0} a gad/item for latitude +* @param {item(num)=0.0} a gad/item for longitude +* @param {item(list)=[]} a gad/item for gps points +* @param {text=''} the google maps key +* @param {num=19} zoom level for map +* @param {text='#3afd02'} color of mower path +*/ {% macro map(id, gad_name, gad_lat, gad_lon, gad_points, mapskey, zoomlevel, pathcolor) %} -
-
+
+
+
+ +
{% endmacro %} \ No newline at end of file diff --git a/husky2/sv_widgets/husky2.js b/husky2/sv_widgets/husky2.js index c2ef89edb..f09cb5ef8 100755 --- a/husky2/sv_widgets/husky2.js +++ b/husky2/sv_widgets/husky2.js @@ -1,34 +1,18 @@ $.widget("sv.husky2", $.sv.widget, { initSelector: 'div[data-widget="husky2.map"]', + map: null, + options: { - mapskey: '', zoomlevel: 19, pathcolor: '#3afd02', }, _create: function () { this._super(); - - // First check if the script already exists on the dom by searching for an id - if (document.getElementById('googleMapsScript') === null) { - const scriptPromise = new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.id = 'googleMapsScript'; - script.onload = resolve; - script.onerror = reject; - script.async = true; - script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.options.mapskey + '&callback=Function.prototype'; - document.body.appendChild(script); - }); - scriptPromise.then(() => { - this._create_map(); - }); - } }, _create_map: function () { - this.map = new google.maps.Map(this.element[0], { zoom: this.options.zoomlevel, mapTypeId: 'hybrid', @@ -50,16 +34,19 @@ $.widget("sv.husky2", $.sv.widget, { strokeWeight: 2, map: this.map }); - }, _update: function (response) { - if (!this.map) { - var that = this; - window.setTimeout(function () { - that._update(response) - }, 500) - return; + if (this.map === null) { + if (typeof google == 'undefined') { + var that = this; + window.setTimeout(function () { + that._update(response) + }, 500) + return; + } else { + this._create_map(); + } } this.marker_myself.setTitle(response[3]); From 797ebbdeffb325aa02bfb08b6d54b8fea8a9d5db Mon Sep 17 00:00:00 2001 From: Hasenradball Date: Fri, 21 Apr 2023 20:18:17 +0200 Subject: [PATCH 005/118] restructure send method and improve try except clause --- rcs1000n/__init__.py | 6 ++-- rcs1000n/cRcSocketSwitch/cRcSocketSwitch.py | 35 +++++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/rcs1000n/__init__.py b/rcs1000n/__init__.py index 0ea66c936..b024384b7 100755 --- a/rcs1000n/__init__.py +++ b/rcs1000n/__init__.py @@ -117,11 +117,11 @@ def update_item(self, item, caller=None, source=None, dest=None): try: # create Brennenstuhl RCS1000N object obj = cRcSocketSwitch.RCS1000N(self._gpio) - # prepare and send values - obj.send(*values) except Exception as err: - self.logger.error('Error: during instantiation of object or during send to device: {}'.format(err)) + self.logger.error('Error: during instantiation of object: {}'.format(err)) else: + # prepare and send values + obj.send(*values) self.logger.info('Info: setting Device {} with SystemCode {} to {}'.format(ButtonCode, SystemCode, value)) finally: # give the transmitter time to complete sending of the command (but not more than 10s) diff --git a/rcs1000n/cRcSocketSwitch/cRcSocketSwitch.py b/rcs1000n/cRcSocketSwitch/cRcSocketSwitch.py index 33d3d6161..ff8b82607 100755 --- a/rcs1000n/cRcSocketSwitch/cRcSocketSwitch.py +++ b/rcs1000n/cRcSocketSwitch/cRcSocketSwitch.py @@ -204,19 +204,36 @@ def calc_DecimalCode_python_style(self, SystemCode, ButtonCode, status): logging.info("binary string: {}\n".format(binstr)) return int(binstr, 2) + def calculateDecimalCode(self, systemCode, buttonCode, status): + ''' + Calculate the Decimal/Binary Code which to send to actuator + ''' + values = self.prepareCodes(systemCode, buttonCode, status) + self.config['code'] = self.calc_DecimalCode_python_style(*values) + return None - def send(self, systemCode, btn_code, status): + + def sendData(self, device): + ''' + send data to device + ''' + device.enable_tx() + device.tx_repeat = 10 + device.tx_code(**self.config) + return None + + + def send(self, systemCode, buttonCode, status): ''' Method to prepare the codes and send it to the actuator ''' try: rfdevice = RFDevice(self.gpio) - rfdevice.enable_tx() - rfdevice.tx_repeat = 10 - values = self.prepareCodes(systemCode, btn_code, status) - send_code = self.calc_DecimalCode_python_style(*values) - self.config['code'] = send_code - rfdevice.tx_code(**self.config) - + except Exception as err: + logging.error('Error: during instantiation of object: {}'.format(err)) + else: + self.calculateDecimalCode(systemCode, buttonCode, status) + self.sendData(rfdevice) + rfdevice.cleanup() finally: - rfdevice.cleanup() \ No newline at end of file + pass From 0726b15b5910ac86432b5e49ce357053bc72b61a Mon Sep 17 00:00:00 2001 From: gruberth Date: Tue, 25 Apr 2023 20:11:01 +0200 Subject: [PATCH 006/118] husky2: Disabled street view controls on map --- husky2/sv_widgets/husky2.js | 1 + 1 file changed, 1 insertion(+) diff --git a/husky2/sv_widgets/husky2.js b/husky2/sv_widgets/husky2.js index f09cb5ef8..ebdd72deb 100755 --- a/husky2/sv_widgets/husky2.js +++ b/husky2/sv_widgets/husky2.js @@ -17,6 +17,7 @@ $.widget("sv.husky2", $.sv.widget, { zoom: this.options.zoomlevel, mapTypeId: 'hybrid', center: new google.maps.LatLng(0.0, 0.0), + streetViewControl: false, }); this.marker_myself = new google.maps.Marker({ From c4a9c1f5460ebdc3554ab76f596a12b01fbd89ca Mon Sep 17 00:00:00 2001 From: AndreK01 Date: Sun, 7 May 2023 15:14:48 +0200 Subject: [PATCH 007/118] update 4.0.1 - Single-Key-Id-login --- indego4shng/README.md | 31 +- indego4shng/__init__.py | 1132 ++++++++++++++---------- indego4shng/plugin.yaml | 2 +- indego4shng/requirements.txt | 2 + indego4shng/webif/templates/index.html | 2 +- 5 files changed, 666 insertions(+), 503 deletions(-) create mode 100755 indego4shng/requirements.txt mode change 100755 => 100644 indego4shng/webif/templates/index.html diff --git a/indego4shng/README.md b/indego4shng/README.md index 005e693c4..ac6bc8b95 100755 --- a/indego4shng/README.md +++ b/indego4shng/README.md @@ -6,19 +6,19 @@ 2. [Credits](#credits) 3. [Change Log](#changelog) **Neu** 4. [Konfiguration](#konfiguration) **Update** -5. [Web-Interface](#webinterface) **Update** +5. [Web-Interface](#webinterface) 6. [Logik-Trigger](#logiktrigger) 7. [öffentlich Funktionen (API)](#api) 8. [Gartenkarte "pimpen"](#gardenmap) 9. [Nutzung der Original Bosch-Mäher-Symbole](#boschpics) -10. [Die Bosch-Api 3.0 - behind the scenes](#boschapi) +10. [Die Bosch-Api 4.0.1 - behind the scenes](#boschapi) ## Generell Das Indego-Plugin wurde durch ein Reverse-Engineering der aktuellen (Version 3.0) App von Bosch entwickelt. Als Basis diente das ursprüngliche Plugin von Marcov. Es werden alle Funktionen der App für den Betrieb sowie einige zusätzliche bereitgestellt. Für die Ersteinrichtung wird weiterhin die Bosch-App benötigt. -Das Plugin erhält die Version der aktuellen Bosch-API. (3.0) +Das Plugin erhält die Version der aktuellen Bosch-API. (4.0.1) ## Credits @@ -33,6 +33,12 @@ Vielen Dank an Jan Odvarko für die Entwicklung des Color-Pickers (http://jscolo ## Change Log +#### 2023-05-06 V4.0.1 +- Login via Single-Key-ID eingebaut +- Endpoit der Bosch-API wurde geändert (siehe Konfiguration) + +#### 2023-03-08 V4.0.0 +- Login via Bosch-ID eingebaut #### 2023-02-05 V3.0.2 - Anpassungen für die geänderten Daten für das Wetter (es werden nun 7 Tage statt 5 übermittelt, die Sonnenstunden je Tag wurden entfern) @@ -120,6 +126,7 @@ zum "pimpen" der Gartenkarte verwenden * `indego_credentials : XXXXXXX`: sind die Zugangsdaten für den Bosch-Server im Format base64 encoded. * `parent_item : indego`: name des übergeordneten items für alle Child-Items * `cycle : 30`: Intervall in Sekunden für das Abrufen des Mäher-Status (default = 30 Sekunden) +* `url: https://api.indego-cloud.iot.bosch-si.com/api/v1/` : Url des Bosch-Endpoints Die Zugangsdaten (indego_credentials) können nach dem Erststart des Plugins im Web-Interface erfasst und gespeichert werden @@ -136,7 +143,7 @@ Indego4shNG: indego_credentials: parent_item: indego cycle: '30' - url: https://api.indego.iot.bosch-si.com/api/v1/ + url: https://api.indego-cloud.iot.bosch-si.com/api/v1/ ``` @@ -361,18 +368,22 @@ Sobald die Dateien mit den Bildern vorhanden sind findet das Widget diese und ve Die entsprechenden Bilder für die "Großen"/"Kleinen" werden auf Grund des Mähertyps automatisch gewählt und dargestellt. -## Die Bosch-Api 3.0 - behind the scenes +## Die Bosch-Api 4.0.1 - behind the scenes Hier ist die Schnittstelle der Bosch-API kurz beschrieben und die Implementierung im Plugin dokumentiert. Der Header ist in den meisten Fällen mit der Session-ID zu füllen : ``` -headers = { - 'x-im-context-id' : SESSION-ID - } +headers = {'accept' : '*/*', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253', + 'content-type' : 'application/json' + } ``` -@Get - steht für einen get-request in Python. Die URL lautet : "https://api.indego.iot.bosch-si.com/api/v1/" gefolgt vom entsprechenden Zugriffspunkt +@Get - steht für einen get-request in Python. Die URL lautet : "https://api.indego-cloud.iot.bosch-si.com/api/v1/" gefolgt vom entsprechenden Zugriffspunkt ``` -url = "https://api.indego.iot.bosch-si.com/api/v1/" +"alms/{}/automaticUpdate".format(alm_sn) +url = "https://api.indego-cloud.iot.bosch-si.com/api/v1/" +"alms/{}/automaticUpdate".format(alm_sn) response = requests.get(url, headers=headers) ``` diff --git a/indego4shng/__init__.py b/indego4shng/__init__.py index 9888b1ebf..42d2e715e 100755 --- a/indego4shng/__init__.py +++ b/indego4shng/__init__.py @@ -44,9 +44,13 @@ from datetime import date import base64 +import urllib.parse +#sys.path.append('/home/smarthome/.p2/pool/plugins/org.python.pydev.core_6.5.0.201809011628/pysrc') +sys.path.append('/devtools/eclipse/plugins/org.python.pydev.core_8.0.0.202009061309/pysrc/') +import pydevd # If a package is needed, which might be not installed in the Python environment, @@ -64,7 +68,7 @@ class Indego4shNG(SmartPlugin): Main class of the Indego Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '4.0.0' + PLUGIN_VERSION = '4.0.1' def __init__(self, sh, *args, **kwargs): """ @@ -166,15 +170,16 @@ def run(self): self.password = self.credentials.split(":")[1] # taken from Init of the plugin if (self.user != '' and self.password != ''): - # self._auth() deprecated - self.logged_in = self._login2Bosch() + self.login_pending = True + self.logged_in, self._bearer, self._refresh_token, self.token_expires,self.alm_sn = self._login_single_key_id(self.user, self.password) + self.login_pending = False + self.context_id = self._bearer[:10]+ '.......' # start the refresh timers self.scheduler_add('operating_data',self._get_operating_data,cycle = 300) self.scheduler_add('get_state', self._get_state, cycle = self.cycle) self.scheduler_add('alert', self.alert, cycle=300) self.scheduler_add('get_all_calendars', self._get_all_calendars, cycle=300) - #self.scheduler_add('check_login_state', self._check_login_state, cycle=130) self.scheduler_add('refresh_token', self._getrefreshToken, cycle=self.token_expires-100) self.scheduler_add('device_data', self._device_data, cycle=120) self.scheduler_add('get_weather', self._get_weather, cycle=600) @@ -199,7 +204,6 @@ def stop(self): self.scheduler_remove('get_weather') self.scheduler_remove('get_next_time') - self._delete_auth() # Log off self.logger.debug("Stop method called") self.alive = False @@ -237,6 +241,7 @@ def parse_item(self, item): return self.update_item if self.has_iattr(item.conf, 'indego_parse_2_attr'): + #pydevd.settrace("192.168.178.37", port=5678) _attr_name = item.conf['indego_attr_name'] newStruct = {} myStruct= json.loads(item()) @@ -271,6 +276,7 @@ def update_item(self, item, caller=None, source=None, dest=None): :param source: if given it represents the source :param dest: if given it represents the dest """ + #pydevd.settrace("192.168.178.37", port=5678) # Function when item is triggered by VISU if caller != self.get_shortname() and caller != 'Autotimer' and caller != 'Logic': @@ -323,6 +329,7 @@ def update_item(self, item, caller=None, source=None, dest=None): self.logger.warning("Error sending command for item '{}' from caller '{}', source '{}' and dest '{}'".format(item,caller,source,dest)) if self.has_iattr(item.conf, 'indego_function_4_all'): + #pydevd.settrace("192.168.178.37", port=5678) try: self.logger.debug("Item '{}' has attribute '{}' found with {}".format( item, 'indego_plugin_function', self.get_iattr_value(item.conf, 'indego_function_4_all'))) myFunction_Name = self.get_iattr_value(item.conf, 'indego_function_4_all') @@ -379,6 +386,7 @@ def _handle_wartung(self, item): self._set_automatic_updates() if item.property.name == self.parent_item+".wartung.messer_zaehler": + #pydevd.settrace("192.168.178.37", port=5678) if (item.property.value == True): if (self._reset_bladeCounter() == True): item(False) @@ -415,6 +423,7 @@ def _handle_parse_map(self, item): def _handle_calendar_list(self, item): if item.property.name == self.parent_item+'.calendar_list': + #pydevd.settrace("192.168.178.37", port=5678) myList = item() myCal = self._get_childitem('calendar') myNewCal = self._parse_list_2_cal(myList, myCal,'MOW') @@ -607,7 +616,7 @@ def _set_clear_message(self): myClearMsg = self._get_childitem('visu.alerts') for message in msg2clear: - myResult = self._delete_url(self.indego_url +'alerts/{}'.format(message), self.context_id, 10,auth=(self.user,self.password)) + myResult = self._delete_url(self.indego_url +'alerts/{}'.format(message), self.context_id, 10,None) self._del_message_in_dict(myClearMsg, message) self._set_childitem('visu.alerts', myClearMsg) @@ -625,11 +634,8 @@ def _check_login_state(self): if self.expiration_timestamp < actTimeStamp+575: self.logged_in = False self.login_pending = True - self._delete_auth() self.context_id = '' - self._auth() self.login_pending = False - self.logged_in = self._check_auth() self._set_childitem('online', self.logged_in) actDate = datetime.now() self.logger.info("refreshed Session-ID at : {}".format(actDate.strftime('Date: %a, %d %b %H:%M:%S %Z %Y'))) @@ -687,6 +693,7 @@ def _auto_pred_cal_update(self): def _auto_mow_cal_update(self): self.cal_update_count += 1 self.cal_update_running = True + #pydevd.settrace("192.168.178.37", port=5678) # set actual Calendar in Calendar-structure myCal = self._get_childitem('calendar') actCalendar = self._get_childitem('calendar_sel_cal') @@ -789,6 +796,7 @@ def _get_all_calendars(self): 'days' : schedule['schedule_days'] }] } + #pydevd.settrace("192.168.178.37", port=5678) my_pred_list = self._parse_cal_2_list(my_pred_cal, None) my_smMow_list = self._parse_cal_2_list(my_smMow_cal, None) @@ -808,6 +816,7 @@ def _log_communication(self, type, url, result): self._set_childitem('webif.communication_protocoll', myLog) def _fetch_url(self, url, username=None, password=None, timeout=10, body=None): + #pydevd.settrace("192.168.178.37", port=5678) try: myResult, response = self._post_url(url, self.context_id, body, timeout,auth=(username,password),nowait = True) except Exception as e: @@ -836,15 +845,16 @@ def _delete_url(self, url, contextid=None, timeout=40, auth=None,nowait = True): myCouner += 1 time.sleep(2) - headers = {'accept-encoding' : 'gzip', - 'authorization' : 'Bearer '+ self._bearer, - 'connection' : 'Keep-Alive', - 'host' : 'api.indego-cloud.iot.bosch-si.com', - 'user-agent' : 'Indego-Connect_4.0.0.12253' + headers = {'accept' : '*/*', + 'authorization' : 'Bearer '+ self._bearer, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253', + 'content-type' : 'application/json' } response = False try: - response = requests.delete(url, headers=headers, auth=auth) + response = requests.delete(url, headers=headers) self._log_communication('delete', url, response.status_code) except Exception as e: self.logger.warning("Problem deleting {}: {}".format(url, e)) @@ -993,30 +1003,6 @@ def _check_state_4_protocoll(self): self.position_detection = False - def _delete_auth(self): - ''' - DELETE https://api.indego.iot.bosch-si.com/api/v1/authenticate - x-im-context-id: {contextId} - ''' - headers = {'Content-Type': 'application/json', - 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'x-im-context-id' : self.context_id - } - url = self.indego_url + 'authenticate' - try: - response = self._delete_url(url, self.context_id, 10,auth=None, nowait = True) - except Exception as e: - self.logger.warning("Problem logging off {0}: {1}".format(url, e)) - return False - if response == False: - return False - - if (response.status_code == 200 or response.status_code == 201): - self.logger.info("You logged off successfully") - return True - else: - self.logger.info("Log off was not successfull : {0}".format(response.status_code)) - return False def _store_calendar(self, myCal = None, myName = ""): ''' @@ -1038,54 +1024,7 @@ def _store_calendar(self, myCal = None, myName = ""): return response.status_code - - - def _check_auth(self): - ''' - GET https://api.indego.iot.bosch-si.com/api/v1/authenticate/check - Authorization: Basic bWF4Lm11c3RlckBhbnl3aGVyZS5jb206c3VwZXJzZWNyZXQ= - x-im-context-id: {contextId} - ''' - headers = {'Content-Type': 'application/json', - 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'x-im-context-id' : self.context_id - } - url = self.indego_url + 'authenticate/check' - - try: - response = self._get_url(url, self.context_id, 10, auth=(self.user,self.password)) - #response = requests.get(url,auth=(self.user,self.password), headers=headers) - - - except Exception as e: - self.logger.warning("Problem checking Authentication {0}: {1}".format(url, e)) - return False - if response != False: - self.logger.info("Your are still logged in to the Bosch-Web-API") - return True - else: - self.logger.info("Your are not logged in to the Bosch-Web-API") - return False - - - - def _auth(self): - url = self.indego_url + 'authenticate' - auth_response,expiration_timestamp = self._fetch_url(url, self.user, self.password, 10,{"device":"", "os_type":"Android", "os_version":"4.0", "dvc_manuf":"unknown", "dvc_type":"unknown", "accept_tc_id": "202012"}) - if auth_response == False: - self.logger.error('AUTHENTICATION INDEGO FAILED! Plugin not working now.') - else: - self.last_login_timestamp = datetime.timestamp(datetime.now()) - self.expiration_timestamp = expiration_timestamp - self.logger.debug("String Auth: " + str(auth_response)) - self.context_id = auth_response['contextId'] - self.logger.info("context ID received :{}".format(self.context_id)) - self.user_id = auth_response['userId'] - self.logger.info("User ID received :{}".format(self.user_id)) - self.alm_sn = auth_response['alm_sn'] - self.logger.info("Serial received : {}".format(self.alm_sn)) - self._log_communication('Auth ', 'Expiration time {}'.format(expiration_timestamp), str(auth_response)) def _getrefreshToken(self): myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token' @@ -1107,426 +1046,622 @@ def _getrefreshToken(self): myJson = json.loads (response.content.decode()) self._refresh_token = myJson['refresh_token'] self._bearer = myJson['access_token'] + self.context_id = self._bearer[:10]+ '.......' self.token_expires = myJson['expires_in'] self.last_login_timestamp = datetime.timestamp(datetime.now()) self.expiration_timestamp = self.last_login_timestamp + self.token_expires - def _login2Bosch(self): - # Standardvalues - self.login_pending = True - code_challenge = 'iGz3HXMCebCh65NomBE5BbfSTBWE40xLew2JeSrDrF4' - code_verifier = '9aOBN3dvc634eBaj7F8iUnppHeqgUTwG7_3sxYMfpcjlIt7Uuv2n2tQlMLhsd0geWMNZPoryk_bGPmeZKjzbwA' - nonce = 'LtRKgCy_l1abdbKPuf5vhA' - myClientID = '65bb8c9d-1070-4fb4-aa95-853618acc876' # that the Client-ID for the Bosch-App - - myPerfPayload ={ - "navigation": { - "type": 0, - "redirectCount": 0 - }, - "timing": { - "connectStart": 1678187315976, - "navigationStart": 1678187315876, - "loadEventEnd": 1678187317001, - "domLoading": 1678187316710, - "secureConnectionStart": 1678187315994, - "fetchStart": 1678187315958, - "domContentLoadedEventStart": 1678187316973, - "responseStart": 1678187316262, - "responseEnd": 1678187316322, - "domInteractive": 1678187316973, - "domainLookupEnd": 1678187315958, - "redirectStart": 0, - "requestStart": 1678187316010, - "unloadEventEnd": 0, - "unloadEventStart": 0, - "domComplete": 1678187317001, - "domainLookupStart": 1678187315958, - "loadEventStart": 1678187317001, - "domContentLoadedEventEnd": 1678187316977, - "redirectEnd": 0, - "connectEnd": 1678187316002 - }, - "entries": [ - { - "name": "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256".format(code_challenge), - "entryType": "navigation", - "startTime": 0, - "duration": 1125.3999999999849, - "initiatorType": "navigation", - "nextHopProtocol": "http/1.1", - "workerStart": 0, - "redirectStart": 0, - "redirectEnd": 0, - "fetchStart": 82.29999999997517, - "domainLookupStart": 82.29999999997517, - "domainLookupEnd": 82.29999999997517, - "connectStart": 99.99999999999432, - "connectEnd": 126.29999999998631, - "secureConnectionStart": 117.4999999999784, - "requestStart": 133.7999999999795, - "responseStart": 385.5999999999824, - "responseEnd": 445.699999999988, - "transferSize": 66955, - "encodedBodySize": 64581, - "decodedBodySize": 155950, - "serverTiming": [], - "workerTiming": [], - "unloadEventStart": 0, - "unloadEventEnd": 0, - "domInteractive": 1097.29999999999, - "domContentLoadedEventStart": 1097.29999999999, - "domContentLoadedEventEnd": 1100.999999999999, - "domComplete": 1125.2999999999815, - "loadEventStart": 1125.3999999999849, - "loadEventEnd": 1125.3999999999849, - "type": "navigate", - "redirectCount": 0 - }, - { - "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", - "entryType": "resource", - "startTime": 1038.0999999999858, - "duration": 21.600000000006503, - "initiatorType": "xmlhttprequest", - "nextHopProtocol": "", - "workerStart": 0, - "redirectStart": 0, - "redirectEnd": 0, - "fetchStart": 1038.0999999999858, - "domainLookupStart": 0, - "domainLookupEnd": 0, - "connectStart": 0, - "connectEnd": 0, - "secureConnectionStart": 0, - "requestStart": 0, - "responseStart": 0, - "responseEnd": 1059.6999999999923, - "transferSize": 0, - "encodedBodySize": 0, - "decodedBodySize": 0, - "serverTiming": [], - "workerTiming": [] - }, - { - "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/bosch-header.png", - "entryType": "resource", - "startTime": 1312.7999999999815, - "duration": 7.900000000006457, - "initiatorType": "css", - "nextHopProtocol": "", - "workerStart": 0, - "redirectStart": 0, - "redirectEnd": 0, - "fetchStart": 1312.7999999999815, - "domainLookupStart": 0, - "domainLookupEnd": 0, - "connectStart": 0, - "connectEnd": 0, - "secureConnectionStart": 0, - "requestStart": 0, - "responseStart": 0, - "responseEnd": 1320.699999999988, - "transferSize": 0, - "encodedBodySize": 0, - "decodedBodySize": 0, - "serverTiming": [], - "workerTiming": [] - } - ], - "connection": { - "onchange": None, - "effectiveType": "4g", - "rtt": 150, - "downlink": 1.6, - "saveData": False, - "downlinkMax": None, - "type": "unknown", - "ontypechange": None - } - } - - myReqPayload = { - "pageViewId":'', - "pageId":"CombinedSigninAndSignup", - "trace":[ - { - "ac":"T005", - "acST":1678187316, - "acD":7 - }, - { - "ac":"T021 - URL:https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", - "acST":1678187316, - "acD":119 - }, - { - "ac":"T019", - "acST":1678187317, - "acD":44 - }, - { - "ac":"T004", - "acST":1678187317, - "acD":19 - }, - { - "ac":"T003", - "acST":1678187317, - "acD":5 - }, - { - "ac":"T035", - "acST":1678187317, - "acD":0 + + def _login_single_key_id(self,user, pwd): + try: + # Standardvalues + code_challenge = 'iGz3HXMCebCh65NomBE5BbfSTBWE40xLew2JeSrDrF4' + code_verifier = '9aOBN3dvc634eBaj7F8iUnppHeqgUTwG7_3sxYMfpcjlIt7Uuv2n2tQlMLhsd0geWMNZPoryk_bGPmeZKjzbwA' + nonce = 'LtRKgCy_l1abdbKPuf5vhA' + myClientID = '65bb8c9d-1070-4fb4-aa95-853618acc876' # das ist die echte Client-ID + step = 0 + + myPerfPayload ={ + "navigation": { + "type": 0, + "redirectCount": 0 }, - { - "ac":"T030Online", - "acST":1678187317, - "acD":0 + "timing": { + "connectStart": 1678187315976, + "navigationStart": 1678187315876, + "loadEventEnd": 1678187317001, + "domLoading": 1678187316710, + "secureConnectionStart": 1678187315994, + "fetchStart": 1678187315958, + "domContentLoadedEventStart": 1678187316973, + "responseStart": 1678187316262, + "responseEnd": 1678187316322, + "domInteractive": 1678187316973, + "domainLookupEnd": 1678187315958, + "redirectStart": 0, + "requestStart": 1678187316010, + "unloadEventEnd": 0, + "unloadEventStart": 0, + "domComplete": 1678187317001, + "domainLookupStart": 1678187315958, + "loadEventStart": 1678187317001, + "domContentLoadedEventEnd": 1678187316977, + "redirectEnd": 0, + "connectEnd": 1678187316002 }, - { - "ac":"T002", - "acST":1678187328, - "acD":0 + "entries": [ + { + "name": "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id=65bb8c9d-1070-4fb4-aa95-853618acc876&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce={}&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256".format(nonce,code_challenge), + "entryType": "navigation", + "startTime": 0, + "duration": 1125.3999999999849, + "initiatorType": "navigation", + "nextHopProtocol": "http/1.1", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 82.29999999997517, + "domainLookupStart": 82.29999999997517, + "domainLookupEnd": 82.29999999997517, + "connectStart": 99.99999999999432, + "connectEnd": 126.29999999998631, + "secureConnectionStart": 117.4999999999784, + "requestStart": 133.7999999999795, + "responseStart": 385.5999999999824, + "responseEnd": 445.699999999988, + "transferSize": 66955, + "encodedBodySize": 64581, + "decodedBodySize": 155950, + "serverTiming": [], + "workerTiming": [], + "unloadEventStart": 0, + "unloadEventEnd": 0, + "domInteractive": 1097.29999999999, + "domContentLoadedEventStart": 1097.29999999999, + "domContentLoadedEventEnd": 1100.999999999999, + "domComplete": 1125.2999999999815, + "loadEventStart": 1125.3999999999849, + "loadEventEnd": 1125.3999999999849, + "type": "navigate", + "redirectCount": 0 + }, + { + "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", + "entryType": "resource", + "startTime": 1038.0999999999858, + "duration": 21.600000000006503, + "initiatorType": "xmlhttprequest", + "nextHopProtocol": "", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 1038.0999999999858, + "domainLookupStart": 0, + "domainLookupEnd": 0, + "connectStart": 0, + "connectEnd": 0, + "secureConnectionStart": 0, + "requestStart": 0, + "responseStart": 0, + "responseEnd": 1059.6999999999923, + "transferSize": 0, + "encodedBodySize": 0, + "decodedBodySize": 0, + "serverTiming": [], + "workerTiming": [] + }, + { + "name": "https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/bosch-header.png", + "entryType": "resource", + "startTime": 1312.7999999999815, + "duration": 7.900000000006457, + "initiatorType": "css", + "nextHopProtocol": "", + "workerStart": 0, + "redirectStart": 0, + "redirectEnd": 0, + "fetchStart": 1312.7999999999815, + "domainLookupStart": 0, + "domainLookupEnd": 0, + "connectStart": 0, + "connectEnd": 0, + "secureConnectionStart": 0, + "requestStart": 0, + "responseStart": 0, + "responseEnd": 1320.699999999988, + "transferSize": 0, + "encodedBodySize": 0, + "decodedBodySize": 0, + "serverTiming": [], + "workerTiming": [] + } + ], + "connection": { + "onchange": None, + "effectiveType": "4g", + "rtt": 150, + "downlink": 1.6, + "saveData": False, + "downlinkMax": None, + "type": "unknown", + "ontypechange": None } - ] - } - # Create a session - mySession = requests.session() - - # Collect some Cookies - - url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id={}&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256'.format(myClientID,code_challenge) + } - myHeader = {'accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'accept-encoding' : 'gzip, deflate, br', - 'accept-language' : 'en-US', - 'connection' : 'keep-alive', - 'host' : 'prodindego.b2clogin.com', - 'user-agent' : 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' - } - mySession.headers = myHeader + myReqPayload = { + "pageViewId":'', + "pageId":"CombinedSigninAndSignup", + "trace":[ + { + "ac":"T005", + "acST":1678187316, + "acD":7 + }, + { + "ac":"T021 - URL:https://swsasharedprodb2c.blob.core.windows.net/b2c-templates/bosch/unified.html", + "acST":1678187316, + "acD":119 + }, + { + "ac":"T019", + "acST":1678187317, + "acD":44 + }, + { + "ac":"T004", + "acST":1678187317, + "acD":19 + }, + { + "ac":"T003", + "acST":1678187317, + "acD":5 + }, + { + "ac":"T035", + "acST":1678187317, + "acD":0 + }, + { + "ac":"T030Online", + "acST":1678187317, + "acD":0 + }, + { + "ac":"T002", + "acST":1678187328, + "acD":0 + } + ] + } + # Create a session + mySession = requests.session() - response = mySession.get(url, allow_redirects=True ) - self._log_communication('GET ', url, response.status_code) - - myText= response.content.decode() - myText1 = myText[myText.find('"csrf"')+8:myText.find('"csrf"')+300] - myCsrf = (myText1[:myText1.find(',')-1]) + # Collect some Cookies - myText1 = myText[myText.find('nonce'):myText.find('nonce')+40] - myNonce = myText1.split('"')[1] + url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id={}&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce={}&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256'.format(myClientID,nonce,code_challenge) + loginReferer = url - myText1 = myText[myText.find('pageViewId'):myText.find('pageViewId')+60] - myPageViewID = myText1.split('"')[2] + myHeader = {'accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'accept-encoding' : 'gzip, deflate, br', + 'accept-language' : 'en-US', + 'connection' : 'keep-alive', + 'host' : 'prodindego.b2clogin.com', + 'user-agent' : 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' + } + mySession.headers = myHeader - myReqPayload['pageViewId']=myPageViewID - - mySession.headers['x-csrf-token'] = myCsrf - mySession.headers['referer'] = url - mySession.headers['origin'] = 'https://prodindego.b2clogin.com' - mySession.headers['host'] = 'prodindego.b2clogin.com' - mySession.headers['x-requested-with'] = 'XMLHttpRequest' - mySession.headers['content-length'] = str(len(json.dumps(myPerfPayload))) - mySession.headers['content-type'] = 'application/json; charset=UTF-8' - mySession.headers['accept-language'] = 'en-US,en;q=0.9' - - - myState = mySession.cookies['x-ms-cpim-trans'] - myCookie = json.loads(base64.b64decode(myState).decode()) - myNewState = '{"TID":"'+myCookie['C_ID']+'"}' - myNewState = base64.b64encode(myNewState.encode()).decode()[:-2] - #'{"TID":"8912c0e6-defb-4d58-858b-27d1cfbbe8f5"}' - #eyJUSUQiOiI4OTEyYzBlNi1kZWZiLTRkNTgtODU4Yi0yN2QxY2ZiYmU4ZjUifQ - - - myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/client/perftrace?tx=StateProperties={}&p=B2C_1A_signup_signin'.format(myNewState) - response=mySession.post(myUrl,data=json.dumps(myPerfPayload)) - self._log_communication('GET ', myUrl, response.status_code) - - - myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/api/CombinedSigninAndSignup/unified' - mySession.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' - mySession.headers['accept-encoding'] = 'gzip, deflate, br' - mySession.headers['upgrade-insecure-requests'] = '1' - mySession.headers['sec-fetch-mode'] = 'navigate' - mySession.headers['sec-fetch-dest'] = 'document' - mySession.headers['sec-fetch-user'] = '?1' - mySession.headers['sec-fetch-site'] = 'same-origin' - - del mySession.headers['content-length'] - del mySession.headers['content-type'] - del mySession.headers['x-requested-with'] - del mySession.headers['x-csrf-token'] - del mySession.headers['origin'] - - myParams = { - 'claimsexchange': 'BoschIDExchange', - 'csrf_token': myCsrf, - 'tx': 'StateProperties=' + myNewState, - 'p': 'B2C_1A_signup_signin', - 'diags': myReqPayload - } - # Get the redirect-URI - response = mySession.get(myUrl,allow_redirects=False,params=myParams) - self._log_communication('GET ', myUrl, response.status_code) - try: - if (response.status_code == 302): - myText = response.content.decode() - myText1 = myText[myText.find('href') + 6:] - myNewUrl = myText1.split('"')[0].replace('&','&') - else: + response = mySession.get(url, allow_redirects=True ) + self._log_communication('GET ', url, response.status_code) + myText= response.content.decode() + myText1 = myText[myText.find('"csrf"')+8:myText.find('"csrf"')+300] + myCsrf = (myText1[:myText1.find(',')-1]) + + myText1 = myText[myText.find('nonce'):myText.find('nonce')+40] + myNonce = myText1.split('"')[1] + + myText1 = myText[myText.find('pageViewId'):myText.find('pageViewId')+60] + myPageViewID = myText1.split('"')[2] + + myReqPayload['pageViewId']=myPageViewID + + mySession.headers['x-csrf-token'] = myCsrf + mySession.headers['referer'] = url + mySession.headers['origin'] = 'https://prodindego.b2clogin.com' + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['x-requested-with'] = 'XMLHttpRequest' + mySession.headers['content-length'] = str(len(json.dumps(myPerfPayload))) + mySession.headers['content-type'] = 'application/json; charset=UTF-8' + mySession.headers['accept-language'] = 'en-US,en;q=0.9' + + + myState = mySession.cookies['x-ms-cpim-trans'] + myCookie = json.loads(base64.b64decode(myState).decode()) + myNewState = '{"TID":"'+myCookie['C_ID']+'"}' + myNewState = base64.b64encode(myNewState.encode()).decode()[:-2] + #'{"TID":"8912c0e6-defb-4d58-858b-27d1cfbbe8f5"}' + #eyJUSUQiOiI4OTEyYzBlNi1kZWZiLTRkNTgtODU4Yi0yN2QxY2ZiYmU4ZjUifQ + + + myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/client/perftrace?tx=StateProperties={}&p=B2C_1A_signup_signin'.format(myNewState) + CollectingCookie = {} + for c in mySession.cookies: + CollectingCookie[c.name] = c.value + + + response=mySession.post(myUrl,data=json.dumps(myPerfPayload),cookies=CollectingCookie) + self._log_communication('POST ', myUrl, response.status_code) + + myUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/B2C_1A_signup_signin/api/CombinedSigninAndSignup/unified' + mySession.headers['accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' + mySession.headers['accept-encoding'] = 'gzip, deflate, br' + mySession.headers['upgrade-insecure-requests'] = '1' + mySession.headers['sec-fetch-mode'] = 'navigate' + mySession.headers['sec-fetch-dest'] = 'document' + mySession.headers['sec-fetch-user'] = '?1' + mySession.headers['sec-fetch-site'] = 'same-origin' + + + del mySession.headers['content-length'] + del mySession.headers['content-type'] + del mySession.headers['x-requested-with'] + del mySession.headers['x-csrf-token'] + del mySession.headers['origin'] + + myParams = { + 'claimsexchange': 'BoschIDExchange', + 'csrf_token': myCsrf, + 'tx': 'StateProperties=' + myNewState, + 'p': 'B2C_1A_signup_signin', + 'diags': myReqPayload + } + # Get the redirect-URI + + response = mySession.get(myUrl,allow_redirects=False,params=myParams) + self._log_communication('GET ', myUrl, response.status_code) + try: + if (response.status_code == 302): + myText = response.content.decode() + myText1 = myText[myText.find('href') + 6:] + myNewUrl = myText1.split('"')[0].replace('&','&') + else: + pass + except: pass - except: - pass - mySession.headers['sec-fetch-site'] = 'cross-site' - mySession.headers['host'] = 'identity.bosch.com' + mySession.headers['sec-fetch-site'] = 'cross-site' + mySession.headers['host'] = 'identity.bosch.com' - # Get the CIAMIDS - response = mySession.get(myNewUrl,allow_redirects=True) - self._log_communication('GET ', myNewUrl, response.status_code) - try: - if (response.history[0].status_code != 302): + # Get the CIAMIDS + response = mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET ', myNewUrl, response.status_code) + try: + if (response.history[0].status_code != 302): + pass + else: + myNewUrl = response.history[0].headers['location'] + except: pass - else: - myNewUrl = response.history[0].headers['location'] - except: - pass - - # Signin to Session - response = mySession.get(myNewUrl,allow_redirects=False) - self._log_communication('GET ', myNewUrl, response.status_code) - - # Authorize -IDS - myNewUrl = response.headers['location'] - mySession.headers['host'] = 'identity-myprofile.bosch.com' - mySession.headers['upgrade-insecure-requests']='1' - cookie_obj = requests.cookies.create_cookie(domain="identity-myprofile.bosch.com",name=".AspNetCore.Identity.Application",value="CfDJ8BbTLtgL3GFMvpWDXN913TQqlMWfpnzYNGGcX0qV3_e1mcxyuYndGzcNXVwoAHCyvY3Ad_1bkYnLsg-J56IdLUNQVMguFnS_KWkPbzib4u6SQtdCZfbiIPV_ZUh4xK-Pd-LgJ61Fi4ljxbb4CewKJRAaDyOhS7KPUu68EVdzte3mEYGm2z8PeSvViW6cGgQeIIOcJ1G3f7XG_s2synfm4o6MDA49a1WnkBIk1kXBodq-vKYXZNMLHOtNGVNE2aZ_k5b9E4mGQVeuncw6SupEku9dCXgO0tRRFK0qUX-41JVrgQdz5v4c_4NB--i1U1b7LUmoZrTtkv0a5KcGPTGz9cZqV5D_Ki4p5uoQxZCmDBPbyecSe6xF3m4yGpEC6hTfrOEJR4LdX6mnppjnXMSc1Y9Pr0Lui3FGeBGuK8GyT4QXJ-pnFrLyF8dh6g2ovkeRvI8MlS5DLSLy_d0s2nOgUxVQPxDsVCxtIMJhE14tSUnC9oRDB_6YUxOqMTEJ_dFacHt-s4iLD2ClBLtA6MsDQcF5pYe4ZOt9zLMuLcoO1NqD3Ca0r00Y0qdkGFGvckp5Xqf7QndkcZxKMPE3GtfH8o6uMsFd7hs1xstxBlT2pgrp0fjjk5R8ugOzJDv-BXarCbjXTzLJtAMVYO4dzorJ7xnXAZDK4IczfXIgxZliwOnTCBvwGIx5CHZfnkYlfhS1PbOE0bwR-sqvJXCS8Jmh6BjmSPHcoKxWxJbLa_wok5HsYmOJgQhVE49WgwuBV88sFvoxpnK_pp1IRR0jFfnV4stT905lkd8hNj5D8o3aZ35sHZDuNPYEXFNUPDORoFnfHkNAP33r126a00n-fLLjaBhFa7W5PnPDaD-M-luVP7nIL-c2tlVon_XRZRC5KMzO4FuOqCeCFwsh3jTtpJk5_iUS4EpHvHT5ldZtRVShC2uzZQ63N_LWl5KZwVlWXPCaLECCZwsGfaAJz0HKDlC-vgXuWL7odJKInmIsi4BJeM9xe280pPDwD6FNUhSOAM2GZgCAW2jilScn5hA2pS1HsLD9yLV0-80Rk9UR9RmRt7USsIOf_7qFMnijAV3MZq9wNKt7ZTBDCI40dxQ1WCYSUV0") - mySession.cookies.set_cookie(cookie_obj) - response = mySession.get(myNewUrl,allow_redirects=False) - self._log_communication('GET ', myNewUrl, response.status_code) - - # Get the login page with redirect URI - returnUrl = myNewUrl - myNewUrl='https://identity-myprofile.bosch.com/ids/login?ReturnUrl='+returnUrl - response=mySession.get(myNewUrl,allow_redirects=True) - self._log_communication('GET', myNewUrl, response.status_code) - myText = response.content.decode() - # find all the needed values - RequestVerificationToken = myText[myText.find('__RequestVerificationToken'):myText.find('__RequestVerificationToken')+300].split('"')[4] - postData = { - 'meta-information' : '', - 'uEmail' : self.user, - 'uPassword' : self.password, - 'ReturnUrl' : returnUrl[36:58]+'/callback'+returnUrl[58:], - '__RequestVerificationToken' : RequestVerificationToken - } - mySession.headers['content-type'] = 'application/x-www-form-urlencoded' - mySession.headers['sec-fetch-sites'] = 'same-origin' - mySession.headers['origin'] = '' - response=mySession.post(myNewUrl,data=postData,allow_redirects=True) - self._log_communication('POST ', myNewUrl, response.status_code) - - ######################################### - mySession.headers['pragma'] = 'no-cache' - mySession.headers['request-context'] = response.history[0].headers['request-context'] - mySession.headers['host'] = 'identity.bosch.com' - myNewUrl = response.history[1].headers['location'] - - # Collect next Cookie - response = mySession.get(myNewUrl,allow_redirects=False) - self._log_communication('GET ', myNewUrl, response.status_code) - - #Get Location for autorization - myNewUrl = 'https://identity.bosch.com/callback' - response=mySession.get(myNewUrl,allow_redirects=False) - self._log_communication('GET', myNewUrl, response.status_code) - myNewUrl = response.headers['location'] - - #Get Authorize-Informations - response = mySession.get(myNewUrl,allow_redirects=False) - self._log_communication('GET ', myNewUrl, response.status_code) - - # Get the post-Fields - myText= response.content.decode() - myCode = myText[myText.find('"code"')+14:myText.find('"code"')+300].split('"')[0] - mySessionState = myText[myText.find('"session_state"')+23:myText.find('"session_state"')+300].split('"')[0] - myState = myText[myText.find('"state"')+15:myText.find('"state"')+300].split('"')[0] - - request_body = {"code" : myCode, "state" : myState, "session_state=" : mySessionState } - - mySession.headers['host'] = 'prodindego.b2clogin.com' - mySession.headers['origin'] = 'https://identity.bosch.com' - mySession.headers['content-type'] = 'application/x-www-form-urlencoded' - mySession.headers['cache-control'] = 'max-age=0' - - del mySession.headers['pragma'] - del mySession.headers['request-context'] - - myNewUrl='https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/oauth2/authresp' - response = mySession.post(myNewUrl,data=request_body,allow_redirects=False) - self._log_communication('POST ', myNewUrl, response.status_code) - myNewUrl = response.headers['location'] - - myFinalCode = myNewUrl.split("code")[1].split("=")[1] - - # Get the new Login-Page - url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/authorize?redirect_uri=com.bosch.indegoconnect%3A%2F%2Flogin&client_id={}&response_type=code&state=j1A8L2zQMbolEja6yqbj4w&nonce=LtRKgCy_l1abdbKPuf5vhA&scope=openid%20profile%20email%20offline_access%20https%3A%2F%2Fprodindego.onmicrosoft.com%2Findego-mobile-api%2FIndego.Mower.User&code_challenge={}&code_challenge_method=S256'.format(myClientID,code_challenge) - mySession.headers['host'] = 'prodindego.b2clogin.com' - del mySession.headers['content-type'] - del mySession.headers['origin'] - del mySession.headers['referer'] - response = mySession.get(url,allow_redirects=False) - self._log_communication('GET ', url, response.status_code) - - # Now Post for a token - mySession.close() - request_body = { - 'code' : myFinalCode, - 'grant_type' : 'authorization_code', - 'redirect_uri' : 'com.bosch.indegoconnect://login', - 'code_verifier' : code_verifier, - 'client_id' : myClientID + # Signin to Session + response = mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # Authorize -IDS + myNewUrl = response.headers['location'] + mySession.headers['host'] = 'identity-myprofile.bosch.com' + mySession.headers['upgrade-insecure-requests']='1' + + response = mySession.get(myNewUrl,allow_redirects=False) + myNewUrl=response.headers['location'] + + postConfirmUrl = myNewUrl[myNewUrl.find('postConfirmReturnUrl'):myNewUrl.find('postConfirmReturnUrl')+300].split('"') + # Get the login page with redirect URI + #returnUrl = myNewUrl + #myNewUrl='https://identity-myprofile.bosch.com/ids/login?ReturnUrl='+returnUrl + + step2_AuthorizeUrl = myNewUrl + response=mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET ', myNewUrl, response.status_code) + myText = response.content.decode() + # find all the needed values + RequestVerificationToken = myText[myText.find('__RequestVerificationToken'):myText.find('__RequestVerificationToken')+300].split('"')[4] + contentReturnUrl = myText[myText.find('ReturnUrl'):myText.find('ReturnUrl')+1600].split('"')[4] + ReturnUrl =myText[myText.find('ReturnUrl'):myText.find('ReturnUrl')+300].split('"')[4] + + postConfirmUrl = myText[myText.find('postConfirmReturnUrl'):myText.find('postConfirmReturnUrl')+700].split('"') + + myNewUrl='https://identity-myprofile.bosch.com/ids/api/v1/clients/'+myText[myText.find('ciamids_'):myText.find('ciamids_')+300].split('%2')[0] + response=mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET ', myNewUrl, response.status_code) + + myNewUrl = step2_AuthorizeUrl+'&skid=true' + mySession.headers['sec-fetch-site']='same-origin' + mySession.headers['accept']='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' + del (mySession.headers['referer']) + # https://identity-myprofile.bosch.com + # /ids/login?ReturnUrl=%2Fids%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3Dcentralids_65EC204B-85B2-4EC3-8BB7-F4B0F69D77D7%26redirect_uri%3Dhttps%253A%252F%252Fidentity.bosch.com%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520email%26state%3DOpenIdConnect.AuthenticationProperties%253DZIkEldcO9j64ZsZ8lxkOF43KmLm9E-R7KiQ6vyWOHRY5coi-sQOCNbtzVfTmM30G2dQ8taj9dupmlMsdfl_aeQfBTLbXNPCoPMduVcoXcUVDx-G2Wo1BhyJmZZryQWMKBGVS5akW3441ocWSmzZ3sseK4ysrm14GxCYIjaXQLw-5-jqSp5xQ3fTbCIIuiEI0zql0bnoAQW2ElbUfFxCZGg2BPRJeIBGddPQOJ_TVR0fZ_Rb2Ex5CJorqDK-GzAq_eKEcqhLwSw3jLLJeyXqHiP8lVwo%26nonce%3D638175139721725147.NTYxMTVjOTEtZGQ2MC00NWFlLWFlMzgtZGRiNWVjZGNjZTNjMTk1ODMwMGQtNTY4OS00MDY5LThiYWItZDRjMTNkZmEzZTEy%26code_challenge%3DVZhREJ7Xv0gvQw6ehTBc55P9Lh3qWX7CiW7wTYYxqY0%26code_challenge_method%3DS256%26postConfirmReturnUrl%3Dhttps%253A%252F%252Fidentity.bosch.com%252Fconnect%252Fauthorize%253Fclient_id%253Dciamids_12E7F9D5-613D-444A-ACD3-838E4D974396%2526redirect_uri%253Dhttps%25253A%25252F%25252Fprodindego.b2clogin.com%25252Fprodindego.onmicrosoft.com%25252Foauth2%25252Fauthresp%2526response_type%253Dcode%2526scope%253Dopenid%252520profile%252520email%2526response_mode%253Dform_post%2526nonce%253DTRg%25252FDjkgw7qNuS2Rh3OslA%25253D%25253D%2526state%253DStateProperties%25253DeyJTSUQiOiJ4LW1zLWNwaW0tcmM6MjU2YzJjOWYtMzlkNC00Y2E2LWFlYTctMTYwZmE4ZTY1ZWRhIiwiVElEIjoiZTgxZjU1MWUtMmM4MC00YmNjLWI4ODgtYjU2NGJlMmEwYzllIiwiVE9JRCI6ImI4MTEzNjgxLWFlZjQtNDc0Yi05YmEyLTI1Mjk0Y2FhNDhmYyJ9%26x-client-SKU%3DID_NET461%26x-client-ver%3D6.7.1.0&skid=true + + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + myNewUrl = response.headers['location'] + + # Now go for the single-key-id + # Get Single-Key-Site + # 1. Step + # + # auth/connect/authorize?client_id=7ca4e64b-faf6-ce9e-937d-6639339b4dac&redirect_uri=https%3A%2F%2Fidentity-myprofile.bosch.com%2Fids%2Fsignin-oidc&response_type=code&scope=openid%20profile%20email&code_challenge=5U4qTGs6v14xAZWvC3lENuVSzuvWLJ0IodizL75YWzk&code_challenge_method=S256&nonce=638175138999064799.OTMzYTExZGMtOGVhZS00MTQ5LTk2MmYtMTA0Mzg4YmJmYmVhOWYyZjEwZDYtZDE3Ny00OTczLTk1MDEtN2FmMjVmOTZiZmJm&state=CfDJ8BbTLtgL3GFMvpWDXN913TRMZYhIvGAQNZTmV8MG88I2iNRhqMCEorJUpmP8ShwrAEBHAAVfh7FgjR4gVnk3eQVuq_P-BvSRzmMKejfb_qxh7fq_Nhp8ULWZ9lU1LZzNEj140CnHaTaLY7LwsP5rXBy-JrdnDiYpPJOYMVswdn6BDZI_EvLnHqd4JJZ0P5Itay4pC0wyfKv2plk3_EyoOMteqnUFvGvfKUeevbbUScXXLwfdNgWjej3nP3BkCW5HDu3PDAz2g4jsC8l5eDZIIcYxpm3jXOcNJC_8B_2JwY9QTjeHDyfV3JNhPUruDTOCHDI4MWuh79pV5Eo-mHrkumSHfQRuycpQS7H6H5UT59io4D9B2xJfPrl-7tBU_5toC1nah0nUkfyyxirPzI6vBTXuJPiHdXC1mjj3wDX2UbFJEFhuvHuOsAzdICLtxS1rySeRcKcFD8nFGnSvIuVodgJSR9PRpXvzmS3cgaS0zae5FYN5LsDO7tTtTPTLadfaqQd11LjNqT7EZTnvT1SpW0HGiBBwPxzr8vSZGQG-xelop7scHH8SYGL4SrUn-nxvHbBYGDIwPrAoMYsIYfzcjPLZ4_VSPFPxVZcAK-8_i_LqKbUhm1ECJlfgOn5hOZhQGTR5uqXZTBJTSIrbHcdc0XMSXZSIi56qBWXiEHwYLAOtiEbNUVjMckyUI-HnrBmfZMhpiLndgLXHhLDr7lNIsBjll_1QOhZxFukftPVwFXIrgKPXWvUz4zky9mwFk9mwQvD2Ip77WvZz5MhJrfCNaPlASLelSdJlVQVRY3-qBdg7CzxFyJs_HOZX37oXsqH02lwda5uAHU2GAtNLfkmj_WGE05qB04gLfn9Y6rf_cXix4oIltbQqf57VUt7xdBKplcQUqeGqoDOg5eHLiz_-9iHu7GHRmqt3SDVxBrZlvL9KxwHAAuUpDJ97oRD51KdZlFFYjTNDjrHHrajshD6yRqFx2a4mGsd_pI_wnS12d9oZs8ILn3Lhdz9ATpNADGoTjbf0dRG8L-hMEx1DBeVID1GztCT-WIbl57xK-2NfbGjQ3MGk5W4vNxwGcxt3L6eRgrAIgAIGlTLHVJc-nQ4RSabzOB-kfUx-mTCHJYMkawqGIrJKkfozj-8aNYoE-wXUVFB63D-xVS25r5V0ttUGehjc4eZjN9JQA6U-ZZXe4UNv5hW8XVCYd-IT83JV340pMqERBjRNYAOPUn3LrDwXwFSKyYecgXMoyZ2d7wEZ-zuqHNCnlAKkLpsR5fJuybDPYNDDdFFHz-du-G2Aq38EUSBPjoUZVRjIUHhQfOUOEicg29ReBOueI-I61pkJKdgfiI7Zezy7Uit71CiZ1kNDjLWC0JkvpiUbXAv_TUySqk62tNkDL2T8E7gj1aT250Pxg7gSmmpBxRWv_0ZltyXwRTR564egzv0BDe4mhIOX5sNGL1HjwBJidNrZ0Q2jL6qr1a2W6DFIyuQ68eZmFAiq4WDkdv928fWMedndQHjgw6t1gBnG6l-J_JINqYaw0vnDUsyWKSzErcf5LN-4_o9RjwcJ3A0iui6PpUyYpQlRlwhobOlCa7V4_4sNQXH5-dlD6lhvXEtHHdBjb9xB9MNIwAJCMkoNU3q3ln10yeFBh_W6Iy25bPthFOeIONFWhC_FslnJvAzlX_kLCieFrGpOmkgE5V9FN-I9fxDX18a3JgdG-qt3YZzgcjTwwiM7YtgRHU4Ikmo7TqaOMfdJjz-Y3FPoaSOKUv6_eVfoY22lNyOMvU-SGLec_7MfpOR0YD2Cvz9Ibo6uh0umjrRHQKEIjzeR0yBjdl68BkoLu7qE0A_tVbcUK918fK2eExs7LONzVshb0_Ruwk5u1sqeft5AWxfYBZSSfnOwzHhS1-PZuWZSF9YVZXd72aKVgvWcyAEDOnsCifsXXzaboJAzs7K00gq9Tq-o3Mlfd44jugQ5-_maYnV9oY646o7ILJ6FD1A93X1mYkR6V7Ma6hxADmoYmD3-teZo_EVmSH6w_ElnYF98-TRyhXqI7tUK10c92kqB_biWHlH25cE-KvH3MaqBkGt1PSBr7kbpX9bxAKS9vP9gYEwCqJG6Ho796cItahFicQDqL2XdxUARA6eyeQUwwz216rIKsPypst-hCyqFWVcv7IS_hzVtSzJfxPLCkmzMmD84u6OL_SMO_GVfnb0X5C33ndFqRu6_aa-6QnuHpyyOBsuWUV_JZ5GED7PajJ-K16Py_23vRp4gKXpfXCOH8E3wESB2aSCXWC5It7tgqQBpdxiavH6CcvsfN-JrBRbHgw&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.6.0.0' + # https://singlekey-id.com + step += 1 + del (mySession.headers['host']) + mySession.headers['user-agent'] = 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' + mySession.headers['sec-fetch-site']='cross-site' + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 2. Step + # https://singlekey-id.com + # auth/log-in?ReturnUrl=%2Fauth%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3D7ca4e64b-faf6-ce9e-937d-6639339b4dac%26redirect_uri%3Dhttps%253A%252F%252Fidentity-myprofile.bosch.com%252Fids%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520email%26code_challenge%3D_H7Bn1EBzLmdvYxRd-ZU9moDgCCOeLhuZlr2oWbTr8Y%26code_challenge_method%3DS256%26nonce%3D638175138466544692.Y2MwNjJlMDEtMjYzZS00MjZlLThhYTQtMTg0MmFjZGMwMzgzMmNmMTI0MGUtYWEyNC00OWExLWJlNTMtMTgzZDFmMDg3NTE5%26state%3DCfDJ8BbTLtgL3GFMvpWDXN913TTi4lyCR0HNQl1e_IHHsZzeJpmbm3hvFYhV6JhmVAlez_YwxFKyT18rCdVOrh8ncg6h6Wi3zCKgovjE7Jn7k9ZuZoWDC7XldFX_3Z2IzwG8WdD4V7ZVlwFChaohZ3fMDYvVPVhzbu7K-5VdMJdSGvyD0J5qaoSL0x-W76jQz15WAMP0npoq4Eyl1rjCVLTunSiQdt1mDJQE_3W1BFj41iW-1nfNeU5Xy8du_AnxyJ-UtWAAAeIr2lwaPKdyr1Mh_G3Q0QwJLxsJY-GhcJXxsBn95cUEDlHjBHRGbgn9T9ab87ppLvyMV3YI6PCu0RWs10IkIxdPFgAVZxb2PRggc8NOGQLnKIOr_2pHKmYw73JT2afa8cPc1CIobT-OS9Xt5_qfKgL6a2dl0RGc29tIPeHqlF-tolY3LSjyEEbOYJ1rLIO9MnR-HHRgq1JpmOPZe5DAl2wRmUDw16GdqQw71NRA0FmExtfLV6vKDrYJfs6IrJEaiC2dsU1mK8WVrTippBdINKhOmTgfvW0o8NiDnHmaKMg35niVI9SKgfEDlccj9EZYw9BOy9pdsPqkQzpAFpCP_BYHHhSmu-Pcj5KAqhY3wm2iRTuMuTLHWt0PP54c9l1kQ2JEoCYt9hHYLl4_D0szQVBUpJLYZgDrWtoLLRbEEPHLnO45pc6gtHyM8j15U0N4GWeiPXZJMnwA-d_dIU5meiWCc3HeA0o-F0IQ688U-GBNXg_QKyatAk6_pGdz7BAokNTiiA9IkJewfqINtjjPkujuNEMhSGNI65GSdk1PBgGAGVgY4o4G1JXUv8VMOQgR5ULtSMfepBf6z2ZIZu7lClg3YUdEGiEyfhU-Gz164D9JYbBtx0gCExg585eXsG2YZ1JNCWqu6eobXWjKBN0IejMmdw4N8uy4EvvWK9RBZ4TCZWXqPM-q0T-REUfm4HmvQvtL-WU8IO8FM1pZaHvePi6qcpivuZWnD1I7PWDbtNSai7uuEm4tMA7NpkBMlh2MNYG5XadLoFN0rrR4TJHHuq2r-AhiGXw8KlXsgff3yZdgCjn854gVjOnnvjwdTrzCaUSsSPiAZ5yFAVYUHKqIzGKXK6MQ7vBdJzfEPwFDNe4aIxEAHogn7hHLJn37b5WgrXZhUxha1zDQcEhdTtEr161soKFRJ1njLwWvXTSCbYUUfVN6BVCIAluWi0C2RLNYmSdUji3B4l_oJ5mq_gjmfdc37e3Xd1EWZtcgFRiO0yEldcscbwsltEzelF_lnK-VImZsr1Y1BD4VahyiykFZF15SAbrhVF6sAHf28mvO38dOqzdt7_B4K2VcOnmo9gM63BNw9rU_dMGLHXub4lJISoOMqeTFN_NmMUrkv41uKUc15e0e2fWS-faC4cZ6hRibrkkCdH5MyqHc8jtMDdIKp59LwXEKskaXrNUO9EL8XR_EfboHIER8dhEUG_ZuaY4FiO64ttgrRvPCC3uokeWhXsa7gx9HsONKqGjFAxCiMDba35uRpcWpunix601ex2le-6vuGpAQ-vqcMrUOvh45sAuHCIa8PLU08zx99lqrd9ERSodPfAGFBVyNh0Y0-y6d1vKYykOj4o7REphB3LnSotJruBVpCaLS1omcA88NtMkJoboKjDBPqzaIX3d9bZhnur3yoFcnjGKoismBmLgDtJY2PT3AAaOBbBjuM_KeqNC92gO_vUkXCa6MJ7JYmlnaRJVMtTFB3Ta4iSaGy7CGkz9KZZcaWPEpTEop-yb64cmkebDCWpcY9Tzouvsvg7CsX2ONM6ejOqDXo_ZpIQrEYVg7PMgZ7NxJhRlrjUDiyYQPofugwC_zaTA1p0oruIkPEmqUwgGSVaBZ8V9WBr2e_dregnUukkjKyft1secvXqaHdV1Ob684PRs_A3-zgKEHAjrkrglRljfsTzTDuDYC3uU3nxFtXFDG42SEcaDAHCPQWwr9j9KwZWgbbohX2dZly9ukvxO1WgPpfyvzKZOK7PpjsbghdIqTu9LX8gFbDNFuPoZ91X4jMG5SnL63YbdLgo68I8c_N_8bBRz1x233HRpJn0ltrxuQULalNjw7XQn9L0iNvZxDf6ZoFgOGNrtFe7PiZTRC6uksaWbFfhXAACmWrAIsi1_6KXLQfjXRhn7ZThfSVDdWWF0M9dv9AUHLoJDbt6eXYQqCTUkBJRkDXPDGzpkCpZT09FIOBFhRt5KKWkAkjldQ5l-6imVGir7LbIPoUVuMDN3C3y8oo2Vd3oWuT3-GhZXz6TkAwlTFeAGfe0g4XtW4wRrz8TYmHw%26x-client-SKU%3DID_NETSTANDARD2_0%26x-client-ver%3D5.6.0.0' + step += 1 + myNewUrl = response.headers['location'] + mySession.headers['Host']='singlekey-id.com' + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 3. Step + # https://singlekey-id.com + # auth/log-in/?ReturnUrl=%2Fauth%2Fconnect%2Fauthorize%2Fcallback%3Fclient_id%3D7ca4e64b-faf6-ce9e-937d-6639339b4dac%26redirect_uri%3Dhttps%253A%252F%252Fidentity-myprofile.bosch.com%252Fids%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520email%26code_challenge%3DTZdCd3C1FX0t08NtCr-rCmJxOrAQiPaNYytL-1Wi9TY%26code_challenge_method%3DS256%26nonce%3D638175136336911121.MGJiYTY0ZGQtN2E4My00MTJmLTg4NjgtNDc1NzAwYTExZDE3NmJhMzExMTEtYWIxZC00MjQ4LWFlNzItODI0MWYyMjg1Y2Zj%26state%3DCfDJ8BbTLtgL3GFMvpWDXN913TQFZ0MRP7aNSvpY-IwH5nqb4gdz1W3Ohx6L_5jrLoqk-iG8_Fb6txKNub_FyGnywp_hEdKhmOrG1QvFtB9Zok6PoKNloLcq1IwoxS2eFAS6qsvHBRR84Yq9D24B1d7klbITisHcQPNTYf-bsMv5w_CcMSrgzRHUFnqFPIHREMkunq1cqUfDCmOw_gFOCzZIAyp0GDRVMvJEmc7mBGjk8nhpJLtdIy2iPn2WXcueAfN7cF8jKtf_uOmSR233K2YM38GoIRgVb_ICWHJ_tqDWs7GIbMffl9D9Q5A2Aa_fopg4vZtk53G8P4jWoX47caPYSmyKwjzAPcP327lUR8tTVFduPEgcGwFrB_U41vtytxSGIrrSQAJU-GyvULXF-BwEJ_ScceQ4udNb_yFbUa8x1YMeVNqsyMvOItv46wPCW5OAycPEfzGfmcntKg4d1XVOkjr4hzc-3oehALLrCI4RFc_-3NtuUMkdZoV93QPA1pndlGajn5CkWu5_QCCa1aUR3unKd5g3OhiQ3ngxjEFbxiHcOrOt4PwaC6Nf6qvVixTXDLWvkL7MOjsSAjLZmoPtYHh4nWK6rnWKvh5J-kn-uJxZm0yKBnm59BQVemH-5XHAIzNGuXuo7R6nxUcsFWZnMCfyxu-d4ta7tUqI2LL-Sz3Fc6qw-3sVrXb9hyxITKAn1-KYNqhWokE3rzhU1itdgQyJIQd4b4eMPES-np8YNHld0hOKCeai4WD8rph054rW049Hfph1g4VVilswqfjEt7jPvIqIlEpMjAs38i6w8GREeJr2_lbrqPN0KF6Bj9jeZV4zy5LCDKItLc-ZRiBxon4dWHYhXy1UjjvPDpDepWtUhPF6FXHyYjN4D222itkTwxOuwLQc-AlWCD09A0bTi4FWOYy2IlwDhprTT15TLfb8QJ3upE8LvPNkRUiadvM0f0M214-Y_K0s53W6oUOvMtXnjqXmYRCoBW18HL1AOfyMto2aJtEB_83sE6Qf18Y2pBMZPMb2bkMURckCBhWwAVFxC0w30HaIVPfrXcRypgFBeh3GS54jrQTC3ropVvqQVUcZXNeZNCApV7M_eLsvgkUlVVeKB0-Dgce7SNaH8AcNtf-17c6WiY3T3FypjsNTzpBgobj7Ay0HL8B8FBighQ0wJxyARRyFRLHTubgVN4Y-tV2hFlc4o2eWzLeJyEFtE48KTpvz8ydXuhYoXAj6gPWoyt-VEdxYwE8OUcswQxWX7De4VyeUi8eOxHwdE3-T3blmDWLzsCuvhUSeL-ykXd0V-T6zMmSDonVL8taIt2xO14yM40X48xCp8d4slOXtZOFuVodMeV5otdZXZmeMVWgVPSUdcuCkCDP2KljEqhtOfCpDy5vDVgJKK9axabMpI-AbzINbz9vFId5wN6crBDjW0bwrQ68gCUhRcHtTtBoLC7y65onvzTLPTslASYTAIGQojvdxxOe5j_nB0iHqcnhbxUtp_vLv2UsrLVfI06MhXfJHO8Sx3LGgxhkXDMMorPnfe2gPLHB39SwZDinwewE2hQU0G-LCqUL5B3AG-lsT-i2FimJTqca6OqPkOo-QuVr2b72iskzxyOFxK6gQhNuvpmlj_47SVcRqK8HbLGU_rAYlmucAP9BbRBKnT0pcVmYpVxCt7tWnQ5uKywZRvUvWX9RVTICz7TssZCm4JnCp8_wRKMxrcJ7hFNb4h2qdjCUm4QgU16h-3L6E1j0UlRzf3w2gPiONPWt3vOGgyn-SGM1jXpLDzWfr-dUxeVlr1Z6we1fjaDo3dDZEJrj1fEeQFb9NxH6LlZmPLDHBXYex3YzO1OxUzaigPqmsMXIuh5STg78lB8k1m2cN96b8I0ohwL0eWbDvoTaLlLudfeo9RkGQ9cMkjTvQvQ4rQEfKVU1YnBM1NijW98yr9Fq8WRuoQL01_s8jnSI7htLo4u8VVpnDC1dSvErrcoM6ob-GBVstIeHdPJV36NHevlylkabKgoZKhl5tqpbVzjrKuyrc2IKdFiavPiTsSxojpFmL8fpqGZiK6XDEe6TWmrZ8Xy8QL6vDTbVtHRvW4-2WIgMdOqP20IzTQTDfUDs9FWrvo0z4JtJ_iC0ZTP3eEk5q1DGHNmzSTkAp4qq1pjgAkiU7hOrTNTFgLkBcy7wAk0eD16DzACp7mY4Z04exIHhPbou7p_904xcptBXT4XtjrS2qneEP8P5j51W0y3kCdEqiR73L0nBpLxj26NVXPDUYxLym1trfQrVyKMiFYIS8u7KiVIlWodukE7fJ1E7gQ_dfpA%26x-client-SKU%3DID_NETSTANDARD2_0%26x-client-ver%3D5.6.0.0 + step += 1 + myNewUrl = response.headers['location'] + postConfirmUrl = myNewUrl[myNewUrl.find('ReturnUrl')+10:].split('"')[0] + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 4. Step - get XSRF-Token + #https://singlekey-id.com/static/roboto-latin-400-normal-b009a76ad6afe4ebd301e36f847a29be.woff2 + step += 1 + myNewUrl='https://singlekey-id.com/favicon.ico' + response=mySession.get(myNewUrl,allow_redirects=True) + self._log_communication('GET ', myNewUrl, response.status_code) + myXRSF_Token = response.history[0].cookies.get('XSRF-TOKEN') + + # 5. Step + step += 1 + mySession.headers['x-xsrf-token']=myXRSF_Token + + myNewUrl='https://singlekey-id.com/auth/api/v1/authentication/UserExists' + myJson = { "username": user } + + mySession.headers['origin']= 'https://singlekey-id.com' + mySession.headers['content-type']= 'application/json' + response=mySession.post(myNewUrl,json=myJson,allow_redirects=False) + self._log_communication('POST ', myNewUrl, response.status_code) + + + # 6. Step + step += 1 + postConfirmUrl = urllib.parse.unquote(postConfirmUrl) + myJson = { + "username": user, + "password": pwd, + "keepMeSignedIn": False, + "returnUrl": postConfirmUrl + } + RequestVerificationToken = mySession.cookies.get('X-CSRF-FORM-TOKEN') + mySession.cookies.set('XSRF-TOKEN',myXRSF_Token) + mySession.cookies.set('X-CSRF-FORM-TOKEN',mySession.cookies.get('X-CSRF-FORM-TOKEN')) + mySession.cookies.set('.AspNetCore.Antiforgery.085ONM3l57w',mySession.cookies.get('.AspNetCore.Antiforgery.085ONM3l57w')) + + + + mySession.headers['content-type']= 'application/json' + mySession.headers['accept'] = 'application/json, text/plain, */*' + mySession.headers['sec-fetch-site'] = 'same-origin' + mySession.headers['host'] = 'singlekey-id.com' + mySession.headers['origin'] = 'https://singlekey-id.com' + mySession.headers['sec-fetch-dest'] = 'empty' + mySession.headers['sec-fetch-mode'] = 'cors' + + del mySession.headers['upgrade-insecure-requests'] + del mySession.headers['sec-fetch-user'] + + mySession.headers['requestverificationtoken'] = RequestVerificationToken + + myNewUrl='https://singlekey-id.com/auth/api/v1/authentication/login' + response=mySession.post(myNewUrl,json=myJson,allow_redirects=True) + self._log_communication('POST ', myNewUrl, response.status_code) + + + # 7. Step + step += 1 + mySession.cookies.set('idsrv.session',mySession.cookies.get('idsrv.session')) + mySession.cookies.set('.AspNetCore.Identity.Application',mySession.cookies.get('.AspNetCore.Identity.Application')) + mySession.headers['accept']='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' + mySession.headers['host']='singlekey-id.com' + mySession.headers['sec-fetch-dest']='document' + mySession.headers['sec-fetch-site']='same-origin' + mySession.headers['sec-fetch-user']='?1' + mySession.headers['upgrade-insecure-requests']='1' + mySession.headers['sec-fetch-mode']='navigate' + del(mySession.headers['origin']) + del(mySession.headers['requestverificationtoken']) + + myNewUrl = 'https://singlekey-id.com'+postConfirmUrl + response= mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ',myNewUrl, response.status_code) + + + # 8. Step + step += 1 + myNewUrl = response.headers['location'] + mySession.headers['host']='identity-myprofile.bosch.com' + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 9. Step + step += 1 + myNewUrl = 'https://identity-myprofile.bosch.com'+response.headers['location'] + response=mySession.get(myNewUrl,allow_redirects=False) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 10. Step - Authorize + step += 1 + CollectingCookie = {} + CollectingCookie['idsrv.session'] = mySession.cookies.get_dict('identity-myprofile.bosch.com','/ids')['idsrv.session'] + CollectingCookie['.AspNetCore.Identity.Application'] = mySession.cookies.get_dict('identity-myprofile.bosch.com','/ids')['.AspNetCore.Identity.Application'] + myNewUrl = 'https://identity-myprofile.bosch.com'+response.headers['location'] + response=mySession.get(myNewUrl,allow_redirects=False,cookies=CollectingCookie) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 11. Step + step += 1 + del mySession.headers['x-xsrf-token'] + del mySession.headers['content-type'] + mySession.headers['host']='identity.bosch.com' + mySession.headers['sec-fetch-site']='same-origin' + CollectingCookie = {} + CollectingCookie['styleId'] = mySession.cookies.get_dict('identity.bosch.com','/')['styleId'] + myDict = mySession.cookies.get_dict('identity.bosch.com','/') + for c in myDict: + if ('SignInMessage' in c): + CollectingCookie[c] = myDict[c] + myNewUrl = response.headers['location'] + response=mySession.get(myNewUrl,allow_redirects=False,cookies=CollectingCookie) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 12. Step + step += 1 + CollectingCookie['idsrv.external'] = mySession.cookies.get_dict('identity.bosch.com','/')['idsrv.external'] + myNewUrl = 'https://identity.bosch.com'+ response.headers['location'] + response=mySession.get(myNewUrl,allow_redirects=False,cookies=CollectingCookie) + self._log_communication('GET ', myNewUrl, response.status_code) + + # 13. Step + step += 1 + CollectingCookie={} + CollectingCookie['idsrv'] = mySession.cookies.get_dict('identity.bosch.com','/')['idsrv'] + CollectingCookie['styleId'] = mySession.cookies.get_dict('identity.bosch.com','/')['styleId'] + CollectingCookie['idsvr.session'] = mySession.cookies.get_dict('identity.bosch.com','/')['idsvr.session'] + + myNewUrl = response.headers['location'] + response=mySession.get(myNewUrl,allow_redirects=False,cookies=CollectingCookie) + self._log_communication('GET ', myNewUrl, response.status_code) + myText= response.content.decode() + myCode = myText[myText.find('"code"')+14:myText.find('"code"')+300].split('"')[0] + mySessionState = myText[myText.find('"session_state"')+23:myText.find('"session_state"')+300].split('"')[0] + myState = myText[myText.find('"state"')+15:myText.find('"state"')+300].split('"')[0] + + # 14. Step - /csp/report + step += 1 + myReferer = myNewUrl # the last URL is the referer + CollectingCookie['idsvr.clients'] = mySession.cookies.get_dict('identity.bosch.com','/')['idsvr.clients'] + myNewUrl='https://identity.bosch.com/csp/report' + + myHeaders = { + 'accept' : '*/*', + 'accept-encoding':'gzip, deflate, br', + 'accept-language' : 'en-US,en;q=0.9', + 'connection':'keep-alive', + 'content-type' : 'application/csp-report', + 'origin' : 'https://identity.bosch.com', + 'referer' : myReferer, + 'sec-fetch-dest' : 'report', + 'sec-fetch-mode' : 'no-cors', + 'sec-fetch-site' : 'same-origin', + 'user-agent' : 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' } - url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token' - mySession = requests.session() - mySession.headers['accept'] = 'application/json' - mySession.headers['accept-encoding'] = 'gzip' - mySession.headers['connection'] = 'Keep-Alive' - mySession.headers['content-type'] = 'application/x-www-form-urlencoded' - mySession.headers['host'] = 'prodindego.b2clogin.com' - mySession.headers['user-agent'] = 'Dalvik/2.1.0 (Linux; U; Android 11; sdk_gphone_x86_arm Build/RSR1.201013.001)' - - response = mySession.post(url,data=request_body) - self._log_communication('POST ', url, response.status_code) - myJson = json.loads (response.content.decode()) - self._refresh_token = myJson['refresh_token'] - self._bearer = myJson['access_token'] - self.token_expires = myJson['expires_in'] - - - url='https://api.indego-cloud.iot.bosch-si.com/api/v1/alms' - myHeader = {'accept-encoding' : 'gzip', - 'authorization' : 'Bearer '+ myJson['access_token'], - 'connection' : 'Keep-Alive', - 'host' : 'api.indego-cloud.iot.bosch-si.com', - 'user-agent' : 'Indego-Connect_4.0.0.12253' - } - response = requests.get(url, headers=myHeader,allow_redirects=True ) - self._log_communication('GET ', url, response.status_code) - if (response.status_code == 200): + myPayload = {"csp-report":{"document-uri": myReferer ,"referrer":"","violated-directive":"script-src","effective-directive":"script-src","original-policy":"default-src 'self'; script-src 'self' ; style-src 'self' 'unsafe-inline' ; img-src *; report-uri https://identity.bosch.com/csp/report","disposition":"enforce","blocked-uri":"eval","line-number":174,"column-number":361,"source-file":"https://identity.bosch.com/assets/scripts.2.5.0.js","status-code":0,"script-sample":""}} + response=mySession.post(myNewUrl,allow_redirects=False,cookies=CollectingCookie) + self._log_communication('POST ', myNewUrl, response.status_code) + + # 15. Step + step += 1 + myHeaders = { + 'accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'cache-control' : 'max-age=0', + 'accept-encoding':'gzip, deflate, br', + 'accept-language' : 'en-US,en;q=0.9', + 'connection':'keep-alive', + 'content-type' : 'application/x-www-form-urlencoded', + 'host' : 'prodindego.b2clogin.com', + 'origin' : 'https://identity.bosch.com', + 'referer' : myReferer, + 'sec-fetch-dest' : 'document', + 'sec-fetch-mode' : 'navigate', + 'sec-fetch-site' : 'cross-site', + 'user-agent' : 'Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36' + } + request_body = { + 'code' : myCode, + 'state' : myState, + 'session_state' : mySessionState + } + myNewUrl = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/oauth2/authresp' + response=mySession.post(myNewUrl,allow_redirects=False,data=request_body,headers=myHeaders) + self._log_communication('POST ', myNewUrl, response.status_code) + + + + # 16. Step + # go end get the Token + step += 1 + + myText= response.content.decode() + myFinalCode = myText[myText.find('code%3d')+7:myText.find('"code%3"')+1700].split('"')[0] + + request_body = { + 'code' : myFinalCode, + 'grant_type' : 'authorization_code', + 'redirect_uri' : 'com.bosch.indegoconnect://login', + 'code_verifier' : code_verifier, + 'client_id' : myClientID + } + + url = 'https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/token' + mySession = requests.session() + mySession.headers['accept'] = 'application/json' + mySession.headers['accept-encoding'] = 'gzip' + mySession.headers['connection'] = 'Keep-Alive' + mySession.headers['content-type'] = 'application/x-www-form-urlencoded' + mySession.headers['host'] = 'prodindego.b2clogin.com' + mySession.headers['user-agent'] = 'Dalvik/2.1.0 (Linux; U; Android 11; sdk_gphone_x86_arm Build/RSR1.201013.001)' + + response = mySession.post(url,data=request_body) + self._log_communication('POST ', url,response.status_code) myJson = json.loads (response.content.decode()) - self.alm_sn = myJson[0]['alm_sn'] - self.login_pending = False - self.last_login_timestamp = datetime.timestamp(datetime.now()) - self.expiration_timestamp = self.last_login_timestamp + self.token_expires - return True - else: - return False + _refresh_token = myJson['refresh_token'] + _access_token = myJson['access_token'] + _token_expires = myJson['expires_in'] + + + # 17. Step + # Check-Login + step += 1 + url='https://api.indego-cloud.iot.bosch-si.com/api/v1/alms' + myHeader = {'accept-encoding' : 'gzip', + 'authorization' : 'Bearer '+ _access_token, + 'connection' : 'Keep-Alive', + 'host' : 'api.indego-cloud.iot.bosch-si.com', + 'user-agent' : 'Indego-Connect_4.0.0.12253' + } + response = requests.get(url, headers=myHeader,allow_redirects=True ) + self._log_communication('GET ', url, response.status_code) + if (response.status_code == 200): + myJson = json.loads (response.content.decode()) + _alm_sn = myJson[0]['alm_sn'] + self.last_login_timestamp = datetime.timestamp(datetime.now()) + self.expiration_timestamp = self.last_login_timestamp + _token_expires + self._log_communication('LOGIN ', 'Login to Sinlge-Key-ID successful done ', 666) + return True,_access_token,_refresh_token,_token_expires,_alm_sn + else: + return False,'','',0,'' + + except err as Exception: + self._log_communication('LOGIN ', 'something went wrong during getting Sinlge-Key-ID Login on Step : {} - {}'.format(step,err), 999) + self.logger.warning('something went wrong during getting Sinlge-Key-ID Login on Step : {} - {}'.format(step,err)) + return False,'','',0,'' + @@ -1723,6 +1858,7 @@ def _get_active_calendar(self, myCal = None): return activeCal def _parse_uzsu_2_list(self, uzsu_dict=None): + #pydevd.settrace("192.168.178.37", port=5678) weekDays = {'MO' : "0" ,'TU' : "1" ,'WE' : "2" ,'TH' : "3",'FR' : "4",'SA' : "5" ,'SU' : "6" } myCal = {} @@ -1814,6 +1950,7 @@ def _parse_cal_2_list(self, myCal = None, type=None): 'End' : myEndTime1, 'Days' : str(myDay) } + #pydevd.settrace("192.168.178.37", port=5678) if 'Attr' in slots: if slots['Attr'] == "C": # manual Exclusion Time mycolour = '#DC143C' @@ -2222,6 +2359,7 @@ def alert(self): else: actAlerts = self._get_childitem('visu.alerts') + #pydevd.settrace("192.168.178.37", port=5678) for myAlert in alert_response: if not (myAlert['alert_id'] in actAlerts): # add new alert to dict @@ -2341,6 +2479,7 @@ def _check_state_triggers(self, myStatecode): def _check_alarm_triggers(self, myAlarm): counter = 1 + #pydevd.settrace("192.168.178.37", port=5678) while counter <=4: myItemName="trigger.alarm_trigger_" + str(counter) + ".alarm" myAlarmTrigger = self._get_childitem(myItemName) @@ -2352,6 +2491,7 @@ def _check_alarm_triggers(self, myAlarm): def _get_state(self): if (self._get_childitem("wartung.wintermodus") == True or self.logged_in == False): return + #pydevd.settrace("192.168.178.37", port=5678) if (self.position_detection): self.position_count += 1 @@ -2369,6 +2509,7 @@ def _get_state(self): else: error_code = 0 self._set_childitem('stateError',error_code) + #pydevd.settrace("192.168.178.37", port=5678) state_code = states['state'] try: if not str(state_code) in str(self.states) and len(self.states) > 0: @@ -2543,6 +2684,7 @@ def _load_map(self): self.logger.debug('You have a new MAP') self._set_childitem('mapSvgCacheDate',self.shtime.now()) self._set_childitem('webif.garden_map', garden.decode("utf-8")) + #pydevd.settrace("192.168.178.37", port=5678) self._parse_map() def _parse_map(self): @@ -2557,6 +2699,7 @@ def _parse_map(self): #======================================================================= myMap = myMap.replace(">",">\n") mapArray = myMap.split('\n') + #pydevd.settrace("192.168.178.37", port=5678) # till here new # Get the Mower-Position and extract it i= 0 @@ -2686,6 +2829,7 @@ def store_state_trigger_html(self, Trigger_State_Item = None,newState=None): @cherrypy.expose def store_alarm_trigger_html(self, Trigger_Alarm_Item = None,newAlarm=None): + #pydevd.settrace("192.168.178.37", port=5678) myItemSuffix=Trigger_Alarm_Item myItem="trigger." + myItemSuffix + ".alarm" self.plugin._set_childitem(myItem,newAlarm) @@ -2701,6 +2845,7 @@ def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config= result2send={} resultParams={} + #pydevd.settrace("192.168.178.37", port=5678) myCredentials = user+':'+pwd byte_credentials = base64.b64encode(myCredentials.encode('utf-8')) encoded = byte_credentials.decode("utf-8") @@ -2721,13 +2866,15 @@ def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config= for line in new_conf.splitlines(): myFile.write(line+'\r\n') myFile.close() + #pydevd.settrace("192.168.178.37", port=5678) txt_Result.append("stored new config to filesystem") self.plugin.user = user self.plugin.password = pwd - if self.plugin.logged_in: - self.plugin._delete_auth() - self.plugin._auth() - self.plugin.logged_in = self.plugin._check_auth() + # Here the login-procedure + self.plugin.login_pending = True + self.plugin.logged_in, self.plugin._bearer, self.plugin._refresh_token, self.plugin.token_expires,self.plugin.alm_sn = self.plugin._login_single_key_id(self.plugin.user, self.plugin.password) + self.plugin.login_pending = False + if self.plugin.logged_in: txt_Result.append("logged in succesfully") else: @@ -2736,7 +2883,7 @@ def store_credentials_html(self, encoded='', pwd = '', user= '', store_2_config= myLastLogin = datetime.fromtimestamp(float(self.plugin.last_login_timestamp)).strftime('%Y-%m-%d %H:%M:%S') resultParams['logged_in']= self.plugin.logged_in resultParams['timeStamp']= myLastLogin + " / " + myExperitation_Time - resultParams['SessionID']= self.plugin.context_id + resultParams['SessionID']= self.plugin._bearer self.plugin._set_childitem('visu.refresh',True) txt_Result.append("refresh of Items initiated") @@ -2755,11 +2902,13 @@ def get_proto_html(self, proto_Name= None): @cherrypy.expose def clear_proto_html(self, proto_Name= None): + #pydevd.settrace("192.168.178.37", port=5678) self.plugin._set_childitem(proto_Name,[]) return None @cherrypy.expose def set_location_html(self, longitude=None, latitude=None): + pydevd.settrace("192.168.178.37", port=5678) self.plugin._set_childitem('webif.location_longitude',float(longitude)) self.plugin._set_childitem('webif.location_latitude',float(latitude)) myLocation = {"latitude":str(latitude),"longitude":str(longitude),"timezone":"Europe/Berlin"} @@ -2845,6 +2994,7 @@ def index(self, reload=None): myLatitude = "" myText = "" try: + #pydevd.settrace("192.168.178.37", port=5678) myLongitude = self.plugin._get_childitem('webif.location_longitude') myLatitude = self.plugin._get_childitem('webif.location_latitude') myText = 'Location from Indego-Server' diff --git a/indego4shng/plugin.yaml b/indego4shng/plugin.yaml index 8b519f87e..24f4a6ed4 100755 --- a/indego4shng/plugin.yaml +++ b/indego4shng/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: http://smarthomeng.de/user/plugins_doc/config/indego.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/966612-indego-connect - version: 4.0.0 # Plugin version + version: 4.0.1 # Plugin version sh_minversion: 1.6 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: False # plugin supports multi instance diff --git a/indego4shng/requirements.txt b/indego4shng/requirements.txt new file mode 100755 index 000000000..dbb901f09 --- /dev/null +++ b/indego4shng/requirements.txt @@ -0,0 +1,2 @@ +requests +urllib3 >= 1.25.8 diff --git a/indego4shng/webif/templates/index.html b/indego4shng/webif/templates/index.html old mode 100755 new mode 100644 index 53758c38b..ae658df37 --- a/indego4shng/webif/templates/index.html +++ b/indego4shng/webif/templates/index.html @@ -140,7 +140,7 @@
{{ _('Plugin') }}     : {% if p.aliv - Session-ID + Token {{ p.context_id }} From 64f3ec0adeae3dfc7a1f6eef9414ddd31a1ff1d6 Mon Sep 17 00:00:00 2001 From: psilo909 Date: Tue, 18 Jul 2023 09:57:08 +0200 Subject: [PATCH 008/118] Update __init__.py --- alexarc4shng/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alexarc4shng/__init__.py b/alexarc4shng/__init__.py index 4d686018f..c42111181 100755 --- a/alexarc4shng/__init__.py +++ b/alexarc4shng/__init__.py @@ -117,7 +117,7 @@ def __init__(self, id): ############################################################################## class AlexaRc4shNG(SmartPlugin): - PLUGIN_VERSION = '1.0.3' + PLUGIN_VERSION = '1.0.4' ALLOW_MULTIINSTANCE = False """ Main class of the Plugin. Does all plugin specific stuff and provides @@ -1122,7 +1122,7 @@ def auto_login_by_request(self): "Referer": myLocation } newUrl = "https://www.amazon.de"+"/ap/signin/"+actSessionID - postfields = urllib3.request.urlencode(PostData) + postfields = urlencode(PostData) myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) @@ -1165,7 +1165,7 @@ def auto_login_by_request(self): myResults.append('MFA : ' + 'use MFA/OTP - Login OTP : {}'.format(mfaCode)) - postfields = urllib3.request.urlencode(PostData) + postfields = urlencode(PostData) myStatus,myRespHeader, myRespCookie, myContent = self.send_post_request(newUrl,myHeaders,myCollectionCookie,PostData) myCollectionTxtCookie = self.parse_response_cookie_2_txt(myRespCookie,myCollectionTxtCookie) myCollectionCookie = self.parse_response_cookie(myRespCookie,myCollectionCookie) From 393b001aa1b098507abb2f70a69b802332684a75 Mon Sep 17 00:00:00 2001 From: psilo909 Date: Tue, 18 Jul 2023 09:57:22 +0200 Subject: [PATCH 009/118] Update plugin.yaml --- alexarc4shng/plugin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alexarc4shng/plugin.yaml b/alexarc4shng/plugin.yaml index 447b7ac04..1e00fb5f5 100755 --- a/alexarc4shng/plugin.yaml +++ b/alexarc4shng/plugin.yaml @@ -8,7 +8,7 @@ plugin: maintainer: AndreK tester: henfri, juergen, psilo #documentation: https://www.smarthomeng.de/user/plugins/alexarc4shng/user_doc.html # url of documentation - version: 1.0.3 # Plugin version + version: 1.0.4 # Plugin version sh_minversion: 1.5.2 # minimum shNG version to use this plugin multi_instance: False # plugin supports multi instance classname: AlexaRc4shNG # class containing the plugin From fa3d7e8db2e1aa190028265b7b51452983bbd655 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 23 Jul 2023 22:10:35 +0200 Subject: [PATCH 010/118] Pioneer Plugin: fix and improve commands --- pioneer/commands.py | 24 +- pioneer/plugin.yaml | 1268 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 1177 insertions(+), 115 deletions(-) diff --git a/pioneer/commands.py b/pioneer/commands.py index b02ca258b..cdac9906d 100755 --- a/pioneer/commands.py +++ b/pioneer/commands.py @@ -3,11 +3,11 @@ # commands for dev pioneer models = { - 'ALL': ['general.pqls', 'general.setup.surroundposition', 'general.setup.speakersystem', 'general.setup.xcurve', 'general.setup.xover', 'general.setup.hdmi', 'general.setup.name', 'general.setup.language', 'general.dimmer', 'general.sleep', 'general.display', 'general.error', 'general.multizone', 'tuner', 'zone1', 'zone2.control', 'hdzone'], - 'SC-LX87': ['general.amp', 'general.setup.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], - 'SC-LX77': ['general.amp', 'general.setup.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], - 'SC-LX57': ['general.amp', 'general.setup.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], - 'SC-2023': ['zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], + 'ALL': ['general.pqls', 'general.settings.speakersystem', 'general.settings.xcurve', 'general.settings.hdmi', 'general.settings.name', 'general.settings.language', 'general.dimmer', 'general.sleep', 'general.display', 'general.error', 'general.multizone', 'tuner', 'zone1', 'zone2.control', 'hdzone'], + 'SC-LX87': ['general.amp', 'general.settings.surroundposition', 'general.settings.xover', 'general.settings.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], + 'SC-LX77': ['general.amp', 'general.settings.surroundposition', 'general.settings.xover', 'general.settings.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], + 'SC-LX57': ['general.amp', 'general.settings.surroundposition', 'general.settings.xover', 'general.settings.loudness', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], + 'SC-2023': ['general.settings.surroundposition', 'general.settings.xover', 'zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control', 'zone3'], 'SC-1223': ['zone2.settings.sound.channel_level', 'zone2.settings.sound.tone_control'], 'VSX-1123': [], 'VSX-923': [] @@ -23,20 +23,20 @@ 'amp': {'read': True, 'write': True, 'read_cmd': '?SAC', 'write_cmd': '{VALUE}SAC', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'SAC{LOOKUP}', 'lookup': 'AMP', 'item_attrs': {'attributes': {'remark': '0 = AMP, 1 = THR'}, 'lookup_item': True}}, 'multizone': {'read': False, 'write': True, 'write_cmd': 'ZZ', 'item_type': 'str', 'dev_datatype': 'str'}, 'settings': { - 'language': {'read': True, 'write': True, 'read_cmd': '?SSE', 'write_cmd': '{RAW_VALUE:02}SSE', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSE{LOOKUP}', 'lookup': 'LANGUAGE', 'item_attrs': {'initial': True}}, + 'language': {'read': True, 'write': True, 'read_cmd': '?SSE', 'write_cmd': '{VALUE}SSE', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSE{LOOKUP}', 'lookup': 'LANGUAGE', 'item_attrs': {'initial': True}}, 'name': {'read': True, 'write': True, 'read_cmd': '?SSO', 'write_cmd': '{VALUE}SSO', 'item_type': 'str', 'dev_datatype': 'PioName', 'reply_pattern': r'SSO(?:\d{2})(.*)', 'item_attrs': {'initial': True}}, - 'speakersystem': {'read': True, 'write': True, 'read_cmd': '?SSF', 'write_cmd': '{RAW_VALUE:02}SSF', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSF{LOOKUP}', 'lookup': 'SPEAKERSYSTEM', 'item_attrs': {'initial': True}}, - 'surroundposition': {'read': True, 'write': True, 'read_cmd': '?SSP', 'write_cmd': '{RAW_VALUE:01}SSP', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSP{LOOKUP}', 'lookup': 'SURROUNDPOSITION', 'item_attrs': {'initial': True}}, - 'xover': {'read': True, 'write': True, 'read_cmd': '?SSQ', 'write_cmd': '{RAW_VALUE:01}SSQ', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSQ{LOOKUP}', 'lookup': 'XOVER', 'item_attrs': {'initial': True}}, - 'xcurve': {'read': True, 'write': True, 'read_cmd': '?SST', 'write_cmd': '{RAW_VALUE:01}SST', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SST{LOOKUP}', 'lookup': 'XCURVE', 'item_attrs': {'initial': True}}, + 'speakersystem': {'read': True, 'write': True, 'read_cmd': '?SSF', 'write_cmd': '{VALUE}SSF', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSF{LOOKUP}', 'lookup': 'SPEAKERSYSTEM', 'item_attrs': {'lookup_item': True, 'initial': True}}, + 'surroundposition': {'read': True, 'write': True, 'read_cmd': '?SSP', 'write_cmd': '{VALUE}SSP', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSP{LOOKUP}', 'lookup': 'SURROUNDPOSITION', 'item_attrs': {'lookup_item': True, 'initial': True}}, + 'xover': {'read': True, 'write': True, 'read_cmd': '?SSQ', 'write_cmd': '{VALUE}SSQ', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SSQ{LOOKUP}', 'lookup': 'XOVER', 'item_attrs': {'initial': True}}, + 'xcurve': {'read': True, 'write': True, 'read_cmd': '?SST', 'write_cmd': '{VALUE}SST', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'SST{LOOKUP}', 'lookup': 'XCURVE', 'item_attrs': {'initial': True}}, 'loudness': {'read': True, 'write': True, 'read_cmd': '?SSU', 'write_cmd': '{RAW_VALUE:01}SSU', 'item_type': 'bool', 'dev_datatype': 'raw', 'reply_pattern': r'SSU(\d{1})', 'item_attrs': {'initial': True}}, 'initialvolume': {'read': True, 'write': True, 'read_cmd': '?SUC', 'write_cmd': '{VALUE}SUC', 'item_type': 'num', 'dev_datatype': 'PioInitVol', 'reply_pattern': r'SUC(\d{3})', 'item_attrs': {'initial': True}}, - 'mutelevel': {'read': True, 'write': True, 'read_cmd': '?SUE', 'write_cmd': '{RAW_VALUE:01}SUE', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'SUE{LOOKUP}', 'lookup': 'MUTELEVEL', 'item_attrs': {'initial': True}}, + 'mutelevel': {'read': True, 'write': True, 'read_cmd': '?SUE', 'write_cmd': '{VALUE}SUE', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'SUE{LOOKUP}', 'lookup': 'MUTELEVEL', 'item_attrs': {'initial': True}}, 'hdmi': { 'control': {'read': True, 'write': True, 'read_cmd': '?STQ', 'write_cmd': '{RAW_VALUE:01}STQ', 'item_type': 'bool', 'dev_datatype': 'raw', 'reply_pattern': r'STQ(\d{1})', 'item_attrs': {'initial': True}}, 'controlmode': {'read': True, 'write': True, 'read_cmd': '?STR', 'write_cmd': '{RAW_VALUE:01}STR', 'item_type': 'bool', 'dev_datatype': 'raw', 'reply_pattern': r'STR(\d{1})', 'item_attrs': {'initial': True}}, 'arc': {'read': True, 'write': True, 'read_cmd': '?STT', 'write_cmd': '{RAW_VALUE:01}STT', 'item_type': 'bool', 'dev_datatype': 'raw', 'reply_pattern': r'STT(\d{1})', 'item_attrs': {'initial': True}}, - 'standbythrough': {'read': True, 'write': True, 'read_cmd': '?STU', 'write_cmd': '{RAW_VALUE:02}STU', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'STU{LOOKUP})', 'lookup': 'STANDBYTHROUGH', 'item_attrs': {'initial': True}} + 'standbythrough': {'read': True, 'write': True, 'read_cmd': '?STU', 'write_cmd': '{VALUE}STU', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'STU{LOOKUP}', 'lookup': 'STANDBYTHROUGH', 'item_attrs': {'lookup_item': True, 'initial': True}} } } diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index 5ce0ecee0..bae6e15a9 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -18,9 +18,10 @@ parameters: standby_item_path: type: str default: '' + description: - de: 'Item-Pfad für das Standby-Item' - en: 'item path for standby switch item' + de: Item-Pfad für das Standby-Item + en: item path for standby switch item host: type: str @@ -266,7 +267,7 @@ item_structs: - general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -345,6 +346,10 @@ item_structs: - general.settings pioneer_read_initial: true + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + surroundposition: type: str pioneer_command: general.settings.surroundposition @@ -355,6 +360,10 @@ item_structs: - general.settings pioneer_read_initial: true + lookup: + type: list + pioneer_lookup: SURROUNDPOSITION#list + xover: type: str pioneer_command: general.settings.xover @@ -456,6 +465,10 @@ item_structs: - general.settings.hdmi pioneer_read_initial: true + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -1310,7 +1323,7 @@ item_structs: - ALL.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -1341,6 +1354,120 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: ALL.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: ALL.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + - ALL.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + - ALL.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + - ALL.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - ALL + - ALL.general + - ALL.general.settings + - ALL.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -1992,7 +2119,7 @@ item_structs: - SC-LX87.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -2037,6 +2164,157 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX87.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + surroundposition: + type: str + pioneer_command: general.settings.surroundposition + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SURROUNDPOSITION#list + + xover: + type: str + pioneer_command: general.settings.xover + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + loudness: + type: bool + pioneer_command: general.settings.loudness + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX87.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + - SC-LX87.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + - SC-LX87.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + - SC-LX87.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX87 + - SC-LX87.general + - SC-LX87.general.settings + - SC-LX87.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -2924,7 +3202,7 @@ item_structs: - SC-LX77.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -2969,94 +3247,245 @@ item_structs: pioneer_read: false pioneer_write: true - tuner: - - read: - type: bool - enforce_updates: true - pioneer_read_group_trigger: SC-LX77.tuner - - tunerpreset: - type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: - - SC-LX77 - - SC-LX77.tuner - - tunerpresetup: - type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true - - tunerpresetdown: - type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true - - title: - type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false - - genre: - type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false - - station: - type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false - - zone1: - - read: - type: bool - enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1 - - control: + settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.control + pioneer_read_group_trigger: SC-LX77.general.settings - power: - type: bool - pioneer_command: zone1.control.power + language: + type: str + pioneer_command: general.settings.language pioneer_read: true pioneer_write: true pioneer_read_group: - SC-LX77 - - SC-LX77.zone1 - - SC-LX77.zone1.control + - SC-LX77.general + - SC-LX77.general.settings pioneer_read_initial: true - on_change: sh....read.timer(sh..readdelay(), True) if value else None - - readdelay: - type: num - initial_value: 1 - remark: After turning on a zone, the most likely needs some time to react to read commands. If not, set this value to 0 - mute: - type: bool - pioneer_command: zone1.control.mute + name: + type: str + pioneer_command: general.settings.name pioneer_read: true pioneer_write: true pioneer_read_group: - SC-LX77 - - SC-LX77.zone1 - - SC-LX77.zone1.control + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true - volume: + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + surroundposition: + type: str + pioneer_command: general.settings.surroundposition + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SURROUNDPOSITION#list + + xover: + type: str + pioneer_command: general.settings.xover + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true + + loudness: + type: bool + pioneer_command: general.settings.loudness + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX77.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + - SC-LX77.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + - SC-LX77.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + - SC-LX77.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.general + - SC-LX77.general.settings + - SC-LX77.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + + tuner: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX77.tuner + + tunerpreset: + type: num + pioneer_command: tuner.tunerpreset + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.tuner + + tunerpresetup: + type: bool + pioneer_command: tuner.tunerpresetup + pioneer_read: false + pioneer_write: true + + tunerpresetdown: + type: bool + pioneer_command: tuner.tunerpresetdown + pioneer_read: false + pioneer_write: true + + title: + type: str + pioneer_command: tuner.title + pioneer_read: true + pioneer_write: false + + genre: + type: str + pioneer_command: tuner.genre + pioneer_read: true + pioneer_write: false + + station: + type: str + pioneer_command: tuner.station + pioneer_read: true + pioneer_write: false + + zone1: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX77.zone1 + + control: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX77.zone1.control + + power: + type: bool + pioneer_command: zone1.control.power + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.zone1 + - SC-LX77.zone1.control + pioneer_read_initial: true + on_change: sh....read.timer(sh..readdelay(), True) if value else None + + readdelay: + type: num + initial_value: 1 + remark: After turning on a zone, the most likely needs some time to react to read commands. If not, set this value to 0 + + mute: + type: bool + pioneer_command: zone1.control.mute + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX77 + - SC-LX77.zone1 + - SC-LX77.zone1.control + + volume: type: num pioneer_command: zone1.control.volume pioneer_read: true @@ -3856,7 +4285,7 @@ item_structs: - SC-LX57.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -3901,6 +4330,157 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX57.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + surroundposition: + type: str + pioneer_command: general.settings.surroundposition + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SURROUNDPOSITION#list + + xover: + type: str + pioneer_command: general.settings.xover + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + loudness: + type: bool + pioneer_command: general.settings.loudness + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-LX57.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + - SC-LX57.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + - SC-LX57.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + - SC-LX57.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-LX57 + - SC-LX57.general + - SC-LX57.general.settings + - SC-LX57.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -4788,7 +5368,7 @@ item_structs: - SC-2023.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -4796,28 +5376,168 @@ item_structs: - SC-2023 - SC-2023.general - dimmer: - type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true - remark: 0 = very bright, 1 = bright, 2 = dark, 3 = off + dimmer: + type: num + pioneer_command: general.dimmer + pioneer_read: true + pioneer_write: true + remark: 0 = very bright, 1 = bright, 2 = dark, 3 = off + + sleep: + type: num + pioneer_command: general.sleep + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes + + multizone: + type: str + pioneer_command: general.multizone + pioneer_read: false + pioneer_write: true + + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-2023.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + surroundposition: + type: str + pioneer_command: general.settings.surroundposition + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SURROUNDPOSITION#list + + xover: + type: str + pioneer_command: general.settings.xover + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-2023.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + - SC-2023.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + - SC-2023.general.settings.hdmi + pioneer_read_initial: true - sleep: - type: num - pioneer_command: general.sleep - pioneer_read: true - pioneer_write: true - pioneer_read_group: - - SC-2023 - - SC-2023.general - remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + - SC-2023.general.settings.hdmi + pioneer_read_initial: true - multizone: - type: str - pioneer_command: general.multizone - pioneer_read: false - pioneer_write: true + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-2023 + - SC-2023.general + - SC-2023.general.settings + - SC-2023.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list tuner: @@ -5706,7 +6426,7 @@ item_structs: - SC-1223.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -5737,6 +6457,120 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-1223.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: SC-1223.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + - SC-1223.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + - SC-1223.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + - SC-1223.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - SC-1223 + - SC-1223.general + - SC-1223.general.settings + - SC-1223.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -6478,7 +7312,7 @@ item_structs: - VSX-1123.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -6509,6 +7343,120 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: VSX-1123.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: VSX-1123.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + - VSX-1123.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + - VSX-1123.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + - VSX-1123.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-1123 + - VSX-1123.general + - VSX-1123.general.settings + - VSX-1123.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: @@ -7160,7 +8108,7 @@ item_structs: - VSX-923.general pqls: - type: str + type: bool pioneer_command: general.pqls pioneer_read: true pioneer_write: true @@ -7191,6 +8139,120 @@ item_structs: pioneer_read: false pioneer_write: true + settings: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: VSX-923.general.settings + + language: + type: str + pioneer_command: general.settings.language + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + pioneer_read_initial: true + + name: + type: str + pioneer_command: general.settings.name + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + pioneer_read_initial: true + + speakersystem: + type: str + pioneer_command: general.settings.speakersystem + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: SPEAKERSYSTEM#list + + xcurve: + type: str + pioneer_command: general.settings.xcurve + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + pioneer_read_initial: true + + hdmi: + + read: + type: bool + enforce_updates: true + pioneer_read_group_trigger: VSX-923.general.settings.hdmi + + control: + type: bool + pioneer_command: general.settings.hdmi.control + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + - VSX-923.general.settings.hdmi + pioneer_read_initial: true + + controlmode: + type: bool + pioneer_command: general.settings.hdmi.controlmode + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + - VSX-923.general.settings.hdmi + pioneer_read_initial: true + + arc: + type: bool + pioneer_command: general.settings.hdmi.arc + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + - VSX-923.general.settings.hdmi + pioneer_read_initial: true + + standbythrough: + type: str + pioneer_command: general.settings.hdmi.standbythrough + pioneer_read: true + pioneer_write: true + pioneer_read_group: + - VSX-923 + - VSX-923.general + - VSX-923.general.settings + - VSX-923.general.settings.hdmi + pioneer_read_initial: true + + lookup: + type: list + pioneer_lookup: STANDBYTHROUGH#list + tuner: read: From 46a95e925ebfc9175222b9602194e9f9eb4c52bd Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 23 Jul 2023 22:10:53 +0200 Subject: [PATCH 011/118] Pioneer Plugin: Query some settings when powered on --- pioneer/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index b24003f9d..8d8472b27 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -71,5 +71,18 @@ def _transform_send_data(self, data=None, **kwargs): data['payload'] = f'{data.get("payload", "")}{data["limit_response"].decode("unicode-escape")}' return data + def _process_additional_data(self, command, data, value, custom, by): + + if command == 'zone1.control.power' and value: + self.logger.debug(f"Zone 1 is turned on. Requesting settings.") + self.send_command('general.settings.language') + self.send_command('general.settings.speakersystem') + self.send_command('general.settings.xcurve') + self.send_command('general.settings.hdmi.control') + self.send_command('general.settings.hdmi.controlmode') + self.send_command('general.settings.hdmi.arc') + self.send_command('general.settings.hdmi.standbythrough') + + if __name__ == '__main__': s = Standalone(pioneer, sys.argv[0]) From 80a8358775f20114796bdd23e1f5ca10f7c36afe Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 27 Jul 2023 09:27:39 +0200 Subject: [PATCH 012/118] Pioneer Plugin: improve settings read on power on --- pioneer/__init__.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index 8d8472b27..ac9d4aaac 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -24,6 +24,7 @@ import builtins import os import sys +import time if __name__ == '__main__': @@ -38,7 +39,7 @@ class SmartPluginWebIf(): from lib.model.sdp.globals import (PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_CONN_TERMINATOR, - CONN_NET_TCP_CLI, CONN_SER_ASYNC, CONN_NULL) + PLUGIN_ATTR_MODEL, CONN_NET_TCP_CLI, CONN_SER_ASYNC, CONN_NULL) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone # from .webif import WebInterface @@ -72,16 +73,23 @@ def _transform_send_data(self, data=None, **kwargs): return data def _process_additional_data(self, command, data, value, custom, by): - - if command == 'zone1.control.power' and value: - self.logger.debug(f"Zone 1 is turned on. Requesting settings.") - self.send_command('general.settings.language') - self.send_command('general.settings.speakersystem') - self.send_command('general.settings.xcurve') - self.send_command('general.settings.hdmi.control') - self.send_command('general.settings.hdmi.controlmode') - self.send_command('general.settings.hdmi.arc') - self.send_command('general.settings.hdmi.standbythrough') + cond1 = command == 'zone1.control.power' or command == 'zone2.control.power' or command == 'zone3.control.power' + if cond1 and value: + self.logger.debug(f"Device is turned on by command {command}. Requesting settings.") + time.sleep(1) + if self._parameters[PLUGIN_ATTR_MODEL] == '': + self.read_all_commands('ALL.general.settings') + else: + self.read_all_commands(f'{self._parameters[PLUGIN_ATTR_MODEL]}.general.settings') + #self.send_command('general.settings.language') + #self.send_command('general.settings.speakersystem') + #self.send_command('general.settings.surroundposition') + #self.send_command('general.settings.xover') + #self.send_command('general.settings.xcurve') + #self.send_command('general.settings.hdmi.control') + #self.send_command('general.settings.hdmi.controlmode') + #self.send_command('general.settings.hdmi.arc') + #self.send_command('general.settings.hdmi.standbythrough') if __name__ == '__main__': From e7091f94248765ff921d41e622197dfbb59af96e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 27 Jul 2023 22:35:02 +0200 Subject: [PATCH 013/118] oppo plugin: remove initial read for pureaudio and eject --- oppo/commands.py | 4 ++-- oppo/plugin.yaml | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/oppo/commands.py b/oppo/commands.py index d402f54c9..1c303d97f 100755 --- a/oppo/commands.py +++ b/oppo/commands.py @@ -52,10 +52,10 @@ }, 'control': { 'power': {'read': True, 'write': True, 'read_cmd': '#QPW', 'write_cmd': '#P{VALUE}', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': ['@POFF OK (OFF)', '@PON OK (ON)', '@QPW OK (ON|OFF)', '@UPW (0|1)'], 'item_attrs': {'initial': True}}, - 'pureaudio': {'read': True, 'write': True, 'write_cmd': '#PUR', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': '@PUR OK (ON|OFF)', 'item_attrs': {'initial': True}}, + 'pureaudio': {'read': True, 'write': True, 'write_cmd': '#PUR', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': '@PUR OK (ON|OFF)'}, 'playpause': {'read': True, 'write': True, 'read_cmd': '#QPL', 'write_cmd': '{VALUE}', 'item_type': 'bool', 'dev_datatype': 'playpause', 'reply_pattern': ['@PLA OK {LOOKUP}$', '@PAU OK {LOOKUP}$'], 'lookup': 'PLAY'}, 'stop': {'read': True, 'write': True, 'read_cmd': '#QPL', 'write_cmd': '#STP', 'item_type': 'bool', 'dev_datatype': 'raw', 'reply_pattern': ['@STP OK (?:(FULL\s)?){LOOKUP}$'], 'lookup': 'STOP'}, - 'eject': {'read': True, 'write': True, 'write_cmd': '#EJT', 'item_type': 'bool', 'dev_datatype': 'openclose', 'reply_pattern': ['@UPL (OPEN|CLOS)', '@EJT OK (OPEN|CLOSE)'], 'item_attrs': {'initial': True, 'enforce': True}}, + 'eject': {'read': True, 'write': True, 'write_cmd': '#EJT', 'item_type': 'bool', 'dev_datatype': 'openclose', 'reply_pattern': ['@UPL (OPEN|CLOS)', '@EJT OK (OPEN|CLOSE)'], 'item_attrs': {'enforce': True}}, 'chapter': {'read': True, 'write': True, 'read_cmd': '#QCH', 'write_cmd': '#SRH C{RAW_VALUE:03}', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': ['@SRH (OK|ER INVALID)', r'@QCH OK (\d{2})/(?:\d{2})']}, 'title': {'read': True, 'write': True, 'read_cmd': '#QTK', 'write_cmd': '#SRH T{RAW_VALUE:03}', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': [r'@QTK OK (\d{2})/(?:\d{2})', '@SRH (OK|ER INVALID)', r'@UAT (?:[A-Z]{2}) (\d{2})/(?:\d{2}) (?:[A-Z]{3}) (?:[0-7.]{3})']}, 'next': {'read': True, 'write': True, 'write_cmd': '#NXT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@NXT (.*)'], 'item_attrs': {'enforce': True}}, diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index b6fb35fa4..b4b4da9b8 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -456,7 +456,6 @@ item_structs: oppo_command: control.pureaudio oppo_read: true oppo_write: true - oppo_read_initial: true playpause: type: bool @@ -480,7 +479,6 @@ item_structs: oppo_read: true oppo_write: true enforce_updates: true - oppo_read_initial: true chapter: type: num @@ -971,7 +969,6 @@ item_structs: oppo_command: control.pureaudio oppo_read: true oppo_write: true - oppo_read_initial: true playpause: type: bool @@ -997,7 +994,6 @@ item_structs: oppo_read: true oppo_write: true enforce_updates: true - oppo_read_initial: true chapter: type: num @@ -1490,7 +1486,6 @@ item_structs: oppo_command: control.pureaudio oppo_read: true oppo_write: true - oppo_read_initial: true playpause: type: bool @@ -1516,7 +1511,6 @@ item_structs: oppo_read: true oppo_write: true enforce_updates: true - oppo_read_initial: true chapter: type: num From 2052d55ff4ae95416bd41897cc330477d6e068f8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:05:26 +0200 Subject: [PATCH 014/118] stateengine plugin: introduce new status function (if e.g. lamella items for sending and receiving values are separate) --- stateengine/StateEngineAction.py | 40 ++++++++++---- stateengine/StateEngineCondition.py | 70 ++++++++++++++++++------- stateengine/StateEngineConditionSet.py | 2 +- stateengine/StateEngineWebif.py | 55 ++++++++++++------- stateengine/__init__.py | 4 ++ stateengine/plugin.yaml | 6 +++ stateengine/user_doc/03_regelwerk.rst | 23 ++++---- stateengine/user_doc/05_bedingungen.rst | 11 ++-- stateengine/user_doc/06_aktionen.rst | 7 +++ 9 files changed, 154 insertions(+), 64 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 4f429e431..2cc9e279f 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -369,6 +369,7 @@ class SeActionSetItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self.__function = "set" @@ -427,19 +428,33 @@ def complete(self, item_state, evals_items=None): item = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self._name) self.__item = str(item) + # missing status in action: Try to find it. + if self.__status is None: + status = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self._name) + + if status is not None: + self.__status = self._abitem.return_item(status) + if self.__mindelta.is_empty(): mindelta = StateEngineTools.find_attribute(self._sh, item_state, "se_mindelta_" + self._name) if mindelta is not None: self.__mindelta.set(mindelta) - if isinstance(self.__item, str): - pass - elif self.__item is not None: - self.__value.set_cast(self.__item.cast) - self.__mindelta.set_cast(self.__item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) - if self._abitem.id == self.__item.property.path: + if self.__status is not None: + self.__value.set_cast(self.__status.cast) + self.__mindelta.set_cast(self.__status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__status.property.path) + if self._abitem.id == self.__status.property.path: self._caller += '_self' + elif self.__status is None: + if isinstance(self.__item, str): + pass + elif self.__item is not None: + self.__value.set_cast(self.__item.cast) + self.__mindelta.set_cast(self.__item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(self.__item.property.path) + if self._abitem.id == self.__item.property.path: + self._caller += '_self' # Write action to logger def write_to_logger(self): @@ -480,8 +495,13 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" if not self.__mindelta.is_empty(): mindelta = self.__mindelta.get() - # noinspection PyCallingNonCallable - delta = float(abs(self.__item() - value)) + if self.__status is not None: + # noinspection PyCallingNonCallable + delta = float(abs(self.__status() - value)) + additionaltext = "of statusitem " + else: + delta = float(abs(self.__item() - value)) + additionaltext = "" if delta < mindelta: text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) @@ -752,6 +772,8 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() self._log_debug("force update: yes") diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index 30b25b7df..b4f1908ec 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -40,6 +40,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem) self.__name = name self.__item = None + self.__status = None self.__eval = None self.__value = StateEngineValue.SeValue(self._abitem, "value", True) self.__min = StateEngineValue.SeValue(self._abitem, "min") @@ -55,7 +56,7 @@ def __init__(self, abitem, name: str): self.__error = None def __repr__(self): - return "SeCondition 'item': {}, 'eval': {}, 'value': {}".format(self.__item, self.__eval, self.__value) + return "SeCondition 'item': {}, 'status': {}, 'eval': {}, 'value': {}".format(self.__item, self.__status, self.__eval, self.__value) # set a certain function to a given value # func: Function to set ('item', 'eval', 'value', 'min', 'max', 'negate', 'changedby', 'updatedby', @@ -68,6 +69,12 @@ def set(self, func, value): "item without item: at the beginning!", value) _, _, value = value.partition(":") self.__item = self._abitem.return_item(value) + elif func == "se_status": + if ":" in value: + self._log_warning("Your status configuration '{0}' is wrong! Define a plain (relative) " + "item without item: at the beginning!", value) + _, _, value = value.partition(":") + self.__status = self._abitem.return_item(value) elif func == "se_eval": if ":" in value: self._log_warning("Your eval configuration '{0}' is wrong! Define a plain eval " @@ -96,21 +103,25 @@ def set(self, func, value): self.__negate = value elif func == "se_agenegate": self.__agenegate = value - elif func != "se_item" and func != "se_eval": + elif func != "se_item" and func != "se_eval" and func != "se_status": self._log_warning("Function '{0}' is no valid function! Please check item attribute.", func) def get(self): - eval_result = str(self.__eval) - if 'SeItem' in eval_result: - eval_result = eval_result.split('SeItem.')[1].split(' ')[0] - if 'SeCurrent' in eval_result: - eval_result = eval_result.split('SeCurrent.')[1].split(' ')[0] + _eval_result = str(self.__eval) + if 'SeItem' in _eval_result: + _eval_result = _eval_result.split('SeItem.')[1].split(' ')[0] + if 'SeCurrent' in _eval_result: + _eval_result = _eval_result.split('SeCurrent.')[1].split(' ')[0] _value_result = str(self.__value.get_for_webif()) try: _item = self.__item.property.path except Exception: _item = self.__item - result = {'item': _item, 'eval': eval_result, 'value': _value_result, + try: + _status = self.__status.property.path + except Exception: + _status = self.__status + result = {'item': _item, 'status': _status, 'eval': _eval_result, 'value': _value_result, 'min': str(self.__min), 'max': str(self.__max), 'agemin': str(self.__agemin), 'agemax': str(self.__agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), @@ -130,7 +141,7 @@ def complete(self, item_state): return False # set 'eval' for some known conditions if item and eval are not set, yet - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: if self.__name == "weekday": self.__eval = StateEngineCurrent.values.get_weekday elif self.__name == "sun_azimut": @@ -188,6 +199,12 @@ def complete(self, item_state): if result is not None: self.__item = self._abitem.return_item(result) + # missing status in condition: Try to find it + if self.__status is None: + result = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) + if result is not None: + self.__status = self._abitem.return_item(result) + # missing eval in condition: Try to find it if self.__eval is None: result = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) @@ -195,13 +212,13 @@ def complete(self, item_state): self.__eval = result # now we should have either 'item' or 'eval' set. If not, raise ValueError - if self.__item is None and self.__eval is None: - raise ValueError("Condition {}: Neither 'item' nor 'eval' given!".format(self.__name)) + if self.__item is None and self.__status is None and self.__eval is None: + raise ValueError("Condition {}: Neither 'item' nor 'status' nor 'eval' given!".format(self.__name)) - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if (self.__item is not None or self.__eval is not None)\ + if (self.__item is not None or self.__status is not None or self.__eval is not None)\ and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False @@ -209,6 +226,8 @@ def complete(self, item_state): try: if self.__item is not None: self.__cast_all(self.__item.cast) + elif self.__status is not None: + self.__cast_all(self.__status.cast) elif self.__name in ("weekday", "sun_azimut", "sun_altitude", "age", "delay", "random", "month"): self.__cast_all(StateEngineTools.cast_num) elif self.__name in ( @@ -229,15 +248,15 @@ def complete(self, item_state): cond_evalitem = self.__eval and ("get_relative_item(" in self.__eval or "return_item(" in self.__eval) except Exception: cond_evalitem = False - if self.__item is None and not cond_min_max and not cond_evalitem: + if self.__item is None and self.__status is None and not cond_min_max and not cond_evalitem: raise ValueError("Condition {}: 'agemin'/'agemax' can not be used for eval!".format(self.__name)) return True # Check if condition is matching def check(self): # Ignore if no current value can be determined (should not happen as we check this earlier, but to be sure ...) - if self.__item is None and self.__eval is None: - self._log_info("Condition '{0}': No item or eval found! Considering condition as matching!", self.__name) + if self.__item is None and self.__status is None and self.__eval is None: + self._log_info("Condition '{0}': No item, status or eval found! Considering condition as matching!", self.__name) return True self._log_debug("Condition '{0}': Checking all relevant stuff", self.__name) self._log_increase_indent() @@ -266,6 +285,12 @@ def write_to_logger(self): self._log_info("item: {0} ({1})", self.__name, i.property.path) else: self._log_info("item: {0} ({1})", self.__name, self.__item.property.path) + if self.__status is not None: + if isinstance(self.__status, list): + for i in self.__status: + self._log_info("status item: {0} ({1})", self.__name, i.property.path) + else: + self._log_info("status item: {0} ({1})", self.__name, self.__status.property.path) if self.__eval is not None: if isinstance(self.__item, list): for e in self.__item: @@ -541,7 +566,7 @@ def __check_age(self): return True # Ignore if no current value can be determined - if self.__item is None and self.__eval is None: + if self.__item is None and self.__status is None and self.__eval is None: self._log_warning("Age of '{0}': No item/eval found! Considering condition as matching!", self.__name) return True @@ -616,7 +641,16 @@ def __check_age(self): # Current value of condition (based on item or eval) def __get_current(self, eval_type='value'): - if self.__item is not None: + if self.__status is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of status item {}", eval_type, self.__status) + return self.__status.property.last_change_age if eval_type == 'age' else\ + self.__status.property.last_change_by if eval_type == 'changedby' else\ + self.__status.property.last_update_by if eval_type == 'updatedby' else\ + self.__status.property.value + elif self.__item is not None: + # noinspection PyUnusedLocal + self._log_debug("Trying to get {} of item {}", eval_type, self.__item) return self.__item.property.last_change_age if eval_type == 'age' else\ self.__item.property.last_change_by if eval_type == 'changedby' else\ self.__item.property.last_update_by if eval_type == 'updatedby' else\ diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index a3a29aaca..56d950512 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -96,7 +96,7 @@ def update(self, item, grandparent_item): continue # update item/eval in this condition - if func == "se_item" or func == "se_eval": + if func == "se_item" or func == "se_eval" or func == "se_status": if name not in self.__conditions: self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) try: diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 42b127ac1..b88959e24 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -154,7 +154,9 @@ def _conditionlabel(self, state, conditionset): for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) - item_none = condition_dict.get('item') == 'None' + + item_none = str(condition_dict.get('item')) == 'None' + status_none = str(condition_dict.get('status')) == 'None' eval_none = condition_dict.get('eval') == 'None' value_none = condition_dict.get('value') == 'None' min_none = condition_dict.get('min') == 'None' @@ -172,23 +174,37 @@ def _conditionlabel(self, state, conditionset): cond5 = not compare == 'agenegate' cond6 = not compare == 'changedbynegate' cond7 = not compare == 'updatedbynegate' - if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7: - conditionlist += '' - textlength = len(str(condition_dict.get('item'))) - condition_tooltip += '{} '.format(condition_dict.get('item')) \ - if textlength > self.__textlimit else '' - info_item = str(condition_dict.get('item'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) - info_eval = str(condition_dict.get('eval'))[:self.__textlimit] + '..  ' * (textlength > self.__textlimit) + cond8 = not compare == 'status' + if cond1 and cond2 and cond3 and cond4 and cond5 and cond6 and cond7 and cond8: + conditionlist += ''.format(compare, condition_dict.get(compare)) + if not status_none: + textlength = len(str(condition_dict.get('status'))) + condition_tooltip += '{} '.format(condition_dict.get('status')) \ + if textlength > self.__textlimit else '' + elif not item_none: + textlength = len(str(condition_dict.get('item'))) + condition_tooltip += '{} '.format(condition_dict.get('item')) \ + if textlength > self.__textlimit else '' + elif not eval_none: + textlength = len(str(condition_dict.get('eval'))) + condition_tooltip += '{} '.format(condition_dict.get('eval')) \ + if textlength > self.__textlimit else '' + else: + textlength = 0 + info_item = str(condition_dict.get('item'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_status = str(condition_dict.get('status'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) + info_eval = str(condition_dict.get('eval'))[:self.__textlimit] + '..  ' * int(textlength > self.__textlimit) info_value = str(condition_dict.get(compare))[:self.__textlimit] + '..  ' * \ - (len(str(condition_dict.get(compare))) > self.__textlimit) - info = info_eval if info_item == "None" and info_eval != "None" else info_item - conditionlist += '{}'.format(info) if not item_none else '' - textlength = len(str(condition_dict.get('eval'))) - condition_tooltip += '{} '.format(condition_dict.get('eval')) \ - if textlength > self.__textlimit else '' - info = info_value if info_item == "None" and info_eval != "None" else info_eval - conditionlist += '{}'.format(info) if not eval_none and item_none else '' - conditionlist += '' + int(len(str(condition_dict.get(compare))) > self.__textlimit) + if not status_none: + info = info_status + elif not item_none: + info = info_item + elif not eval_none: + info = info_eval + else: + info = "" + conditionlist += '{}'.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ else "older" if not agemin_none and compare == "agemin"\ @@ -203,13 +219,14 @@ def _conditionlabel(self, state, conditionset): and condition_dict.get('negate') == 'True')\ else "==" conditionlist += '{}'.format(comparison) - conditionlist += '"{}"'.format(info) if not item_none and not eval_none else '' + conditionlist += '"{}"'.format(info) if not item_none and not status_none and not eval_none else '' textlength = len(str(condition_dict.get(compare))) condition_tooltip += '{} '.format(condition_dict.get(compare)) \ if textlength > self.__textlimit else '' info = info_value conditionlist += '{}'.format(info) if not condition_dict.get(compare) == 'None' and ( - (eval_none and not item_none) or (not eval_none and item_none)) else '' + (eval_none and not item_none) or (eval_none and not status_none) or \ + (not eval_none and item_none) or (not eval_none and status_none)) else '' conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \ not in compare and not compare == "value" else '' conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else '' diff --git a/stateengine/__init__.py b/stateengine/__init__.py index ce2382c9d..0455cf18a 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -96,6 +96,10 @@ def parse_item(self, item): item.expand_relativepathes('se_item_*', '', '') except Exception: pass + try: + item.expand_relativepathes('se_status_*', '', '') + except Exception: + pass if self.has_iattr(item.conf, "se_manual_include") or self.has_iattr(item.conf, "se_manual_exclude"): item._eval = "sh.stateengine_plugin_functions.manual_item_update_eval('" + item.id() + "', caller, source)" elif self.has_iattr(item.conf, "se_manual_invert"): diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 5c2cae2ba..ec6da280d 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -1359,6 +1359,12 @@ item_attribute_prefixes: de: 'Definiert das Item, das in einem konkreten Zustand evaluiert oder geändert werden soll' en: 'Definition of an item that should be evaluated or changed in a specific state' + se_status_: + type: foo + description: + de: 'Definiert das Item, das in einem konkreten Zustand evaluiert werden soll' + en: 'Definition of an item that should be evaluated in a specific state' + se_eval_: type: foo description: diff --git a/stateengine/user_doc/03_regelwerk.rst b/stateengine/user_doc/03_regelwerk.rst index 8718ac663..ab841d13c 100755 --- a/stateengine/user_doc/03_regelwerk.rst +++ b/stateengine/user_doc/03_regelwerk.rst @@ -43,29 +43,28 @@ das Attribute ``se_plugin`` auf inactive zu setzen: Item-Definitionen ----------------- -Bedingungen und Aktionen beziehen sich überlicherweise auf Items wie beispielsweise +Bedingungen und Aktionen beziehen sich üblicherweise auf Items wie beispielsweise die Höhe einer Jalousie oder die Außenhelligkeit. Diese Items müssen auf Ebene des Regelwerk-Items über das Attribut -``se_item_`` bekannt gemacht werden. +``se_item_`` bekannt gemacht werden. Um einfacher zwischen Items, +die für Bedingungen und solchen, die für Aktionen genutzt werden, unterscheiden zu können, +können Items, die nur für Bedingungen gebraucht werden, mittels ``se_status_`` +deklariert werden. Diese Variante ist auch besonders dann relevant, wenn es zwei separate Items +für "Senden" und "Empfangen" gibt, also z.B. Senden der Jalousiehöhe und Empfangen des aktuellen +Werts vom KNX-Aktor. Anstatt direkt das Item in Form des absoluten oder relativen Pfades mittels ``se_item_`` zu setzen, kann auch die Angabe ``se_eval_`` genutzt werden. In diesem Fall wird eine beliebige -Funktion anstelle des Itemnamen angegeben. Dies ist sowohl für Bedingungsabfragen, -als auch für das Setzen von "dynamischen" Items möglich. +Funktion anstelle des Itemnamen angegeben. Dies ist primär für das Setzen von "dynamischen" Items +gedacht, allerdings ist es auch möglich, hier einen beliebigen Eval-Ausdruck als Bedingung festzulegen. -An dieser Stelle ist es auch möglich, über ``se_mindelta_`` zu definieren, um welchen Wert -sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Siehe auch :ref:`Aktionen`. - -Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, -ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration -kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel se_item ================ Im Beispiel wird durch ``se_item_height`` das Item ``beispiel.raffstore1.hoehe`` dem Plugin unter dem Namen "height" bekannt gemacht. Das Item ``beispiel.wetterstation.helligkeit`` -wird durch ``se_item_brightness`` als "brightness" referenziert. +wird durch ``se_item_brightness`` (alternativ via ``se_status_brightness``) als "brightness" referenziert. Auf diese Namen beziehen sich nun in weiterer Folge Bedingungen und Aktionen. Im Beispiel wird im Zustand Nacht das Item ``beispiel.raffstore1.hoehe`` auf den Wert 100 gesetzt, sobald @@ -95,7 +94,7 @@ und Aktionen folgen auf den nächsten Seiten. Beispiel se_eval ================ -se_eval ist für Sonderfälle und etwas komplexere Konfiurationen sinnvoll, kann aber +se_eval ist für Sonderfälle und etwas komplexere Konfigurationen sinnvoll, kann aber im ersten Durchlauf ignoriert werden. Es wird daher empfohlen, als Beginner dieses Beispiel einfach zu überspringen ;) diff --git a/stateengine/user_doc/05_bedingungen.rst b/stateengine/user_doc/05_bedingungen.rst index 031914e3a..bcc4bae13 100755 --- a/stateengine/user_doc/05_bedingungen.rst +++ b/stateengine/user_doc/05_bedingungen.rst @@ -10,7 +10,7 @@ Beispiel -------- Im folgenden Beispiel wird der Zustand "Daemmerung" eingenommen, sobald -die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. +die Helligkeit (über se_item_brightness oder se_status_brightness definiert) über 500 Lux liegt. .. code-block:: yaml @@ -19,7 +19,7 @@ die Helligkeit (über se_item_brightness definiert) über 500 Lux liegt. automatik: struct: stateengine.general rules: - se_item_brightness: beispiel.wetterstation.helligkeit + se_status_brightness: beispiel.wetterstation.helligkeit Daemmerung: name: Dämmerung remark: @@ -62,7 +62,7 @@ Der zu vergleichende Wert einer Bedingung kann auf folgende Arten definiert werd - statischer Wert (also z.B. 500 Lux). Wird angegegeben mit ``value:500``, wobei das value: auch weggelassen werden kann. - Item (beispielsweise ein Item namens settings.helligkeitsschwellwert). Wird angegeben mit ``item:settings.helligkeitsschwellwert`` - Eval-Funktion (siehe auch `eval Ausdrücke `_). Wird angegeben mit ``eval:1*2*se_eval.get_relative_itemvalue('..bla')`` -- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` +- Regular Expression (siehe auch ` RegEx Howto `_) - Vergleich mittels re.fullmatch, wobei Groß/Kleinschreibung ignoriert wird. Wird angegeben mit ``regex:StateEngine Plugin:(.*)`` - Template: eine Vorlage, z.B. eine eval Funktion, die immer wieder innerhalb des StateEngine Items eingesetzt werden kann. Angegeben durch ``template:`` @@ -75,7 +75,8 @@ die jeweils mit einem Unterstrich "_" getrennt werden: - ``se_``: eindeutiger Prefix, um dem Plugin zugeordnet zu werden - ````: siehe unten. Beispiel: min = der Wert des muss mindestens dem beim Attribut angegebenen Wert entsprechen. -- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. +- ````: Hier wird entweder das im Regelwerk-Item mittels ``se_item_`` +oder ``se_status_`` deklarierte Item oder eine besondere Bedingung (siehe unten) referenziert. Templates für Bedingungsabfragen @@ -83,7 +84,7 @@ Templates für Bedingungsabfragen Setzt man für mehrere Bedingungsabfragen (z.B. Helligkeit, Temperatur, etc.) immer die gleichen Ausdrücke ein (z.B. eine eval-Funktion), so kann Letzteres als Template -definiert und referenziert werden. Dadurch wird die Handhabung +definiert und referenziert werden. Dadurch wird die Handhabung komplexerer Abfragen deutlich vereinfacht. Diese Templates müssen wie se_item/se_eval auf höchster Ebene des StateEngine Items (also z.B. rules) deklariert werden. diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index a663f0889..95b75479e 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -24,6 +24,12 @@ stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Oh würden sich die Lamellen ständig um wenige Grad(bruchteile) ändern. Wird jedoch mindelta beispielsweise auf den Wert 10 gesetzt, findet eine Änderung erst statt, wenn sich der errechnete Wert um mindestens 10 Grad vom aktuellen Lamellenwert unterscheidet. +Im Beispiel wird auch mittels ``se_status_`` ein gesondertes Item definiert, +das den Wert vom KNX-Aktor empfängt. + +Außerdem ist es möglich, über ``se_repeat_actions`` generell zu definieren, +ob Aktionen für die Stateengine wiederholt ausgeführt werden sollen oder nicht. Diese Konfiguration +kann für einzelne Aktionen individuell über die Angabe ``repeat`` überschrieben werden. Siehe auch :ref:`Aktionen`. Beispiel zu Aktionen -------------------- @@ -43,6 +49,7 @@ Das folgende Beispiel führt je nach Zustand folgende Aktionen aus: rules: se_item_height: raffstore1.hoehe # Definition des zu ändernden Höhe-Items se_item_lamella: raffstore1.lamelle # Definition des zu ändernden Lamellen-Items + se_status_lamella: raffstore1.lamelle.status # Definition des Lamellen Statusitems se_mindelta_lamella: 10 # Mindeständerung von 10 Grad, sonst werden die Lamellen nicht aktualisiert. Daemmerung: <...> From 58dd8e8f0fd3e3d4bccfd5dbf52685462392ceff Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:06:50 +0200 Subject: [PATCH 015/118] stateengine plugin: add status log message --- stateengine/StateEngineAction.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 2cc9e279f..3962336d7 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -463,6 +463,8 @@ def write_to_logger(self): self._log_debug("item from eval: {0}", self.__item) elif self.__item is not None: self._log_debug("item: {0}", self.__item.property.path) + if self.__status is not None: + self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() From f297c2433e5238a569ba6f9e660113091bd054c9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:10:17 +0200 Subject: [PATCH 016/118] stateengine plugin: some bug fixes and minor adjustments --- stateengine/StateEngineAction.py | 2 +- stateengine/StateEngineActions.py | 2 +- stateengine/StateEngineCondition.py | 10 +++++++++- stateengine/StateEngineValue.py | 10 +++++----- stateengine/StateEngineWebif.py | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 3962336d7..a5ba925dd 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -295,7 +295,7 @@ def execute(self, is_repeat: bool, allow_item_repeat: bool, state): try: state.update_name(state.state_item) _key_name = ['{}'.format(state.id), 'name'] - self.update_webif(_key_name, state.name) + self._abitem.update_webif(_key_name, state.name) _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(self._name), 'delay'] self._abitem.update_webif(_key, _delay_info) except Exception: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 229c1464f..e6da5db49 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -390,7 +390,7 @@ def set(self, value): # item_allow_repeat: Is repeating actions generally allowed for the item? # state: state item triggering the action # additional_actions: SeActions-Instance containing actions which should be executed, too - def execute(self, is_repeat: bool, allow_item_repeat: bool, state: str, additional_actions=None): + def execute(self, is_repeat: bool, allow_item_repeat: bool, state, additional_actions=None): actions = [] for name in self.__actions: actions.append((self.__actions[name].get_order(), self.__actions[name])) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index b4f1908ec..dd280d120 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -334,6 +334,9 @@ def __cast_all(self, cast_func): def __change_update_value(self, value, valuetype): def __convert(convert_value, convert_current): + if convert_value is None: + self._log_develop("Ignoring value None for conversion") + return convert_value, convert_current _oldvalue = convert_value try: if isinstance(convert_value, re._pattern_type): @@ -342,14 +345,19 @@ def __convert(convert_value, convert_current): if isinstance(convert_value, re.Pattern): return convert_value, convert_current if isinstance(convert_current, bool): + self.__value.set_cast(StateEngineTools.cast_bool) convert_value = StateEngineTools.cast_bool(convert_value) elif isinstance(convert_current, int): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = int(StateEngineTools.cast_num(convert_value)) elif isinstance(convert_current, float): + self.__value.set_cast(StateEngineTools.cast_num) convert_value = StateEngineTools.cast_num(convert_value) * 1.0 elif isinstance(convert_current, list): + self.__value.set_cast(StateEngineTools.cast_list) convert_value = StateEngineTools.cast_list(convert_value) else: + self.__value = str(convert_value) convert_value = str(convert_value) convert_current = str(convert_current) if not type(_oldvalue) == type(convert_value): @@ -470,7 +478,7 @@ def __check_value(self): for i, _ in enumerate(min_value): min = None if min_value[i] == 'novalue' else min_value[i] max = None if max_value[i] == 'novalue' else max_value[i] - self._log_debug("Checking minvalue {} and maxvalue {}", min, max) + self._log_debug("Checking minvalue {} ({}) and maxvalue {}({}) against current {}({})", min, type(min), max, type(max), current, type(current)) if min is not None and max is not None and min > max: min, max = max, min self._log_warning("Condition {}: min must not be greater than max! " diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 0dfe64255..931037566 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -259,8 +259,8 @@ def set(self, value, name="", reset=True, item=None): self.__template) s = None try: - cond1 = s.isdigit() - cond2 = field_value[i].isdigit() + cond1 = s.lstrip('-').replace('.','',1).isdigit() + cond2 = field_value[i].lstrip('-').replace('.','',1).isdigit() except Exception: cond1 = False cond2 = False @@ -381,9 +381,9 @@ def write_to_logger(self): if isinstance(self.__value, list): for i in self.__value: if i is not None: - self._log_debug("{0}: {1}", self.__name, i) + self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: - self._log_debug("{0}: {1}", self.__name, self.__value) + self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -619,7 +619,7 @@ def __get_eval(self): self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = _newvalue values = _newvalue self._log_decrease_indent() - self._log_debug("Eval result: {0}.", values) + self._log_debug("Eval result: {0} ({1}).", values, type(values)) self._log_increase_indent() except Exception as ex: self._log_decrease_indent() diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index b88959e24..3f5afcd6e 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -52,7 +52,7 @@ def __init__(self, smarthome, abitem): fontname='Helvetica', fontsize='10') self.__nodes = {} self.__scalefactor = 0.1 - self.__textlimit = 145 + self.__textlimit = 105 self.__conditionset_count = 0 def __repr__(self): From 3945a6b365fbc25512c350b2a1c46817e8ded85d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:11:32 +0200 Subject: [PATCH 017/118] stateengine plugin: improve handling of min_delta and it's integration in the webif visu --- stateengine/StateEngineAction.py | 27 +++++++++++++++++++-------- stateengine/StateEngineWebif.py | 14 +++++++++++--- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index a5ba925dd..6f95fe98c 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -370,6 +370,7 @@ def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None self.__status = None + self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") self.__function = "set" @@ -504,9 +505,11 @@ def real_execute(self, actionname: str, namevar: str = "", repeat_text: str = "" else: delta = float(abs(self.__item() - value)) additionaltext = "" + + self.__delta = delta if delta < mindelta: - text = "{0}: Not setting '{1}' to '{2}' because delta '{3:.2}' is lower than mindelta '{4}'" - self._log_debug(text, actionname, self.__item.property.path, value, delta, mindelta) + text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2}' is lower than mindelta '{5}'" + self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) return self._execute_set_add_remove(actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition) @@ -520,13 +523,21 @@ def _execute_set_add_remove(self, actionname, namevar, repeat_text, item, value, def get(self): try: - item = str(self.__item.property.path) + _item = str(self.__item.property.path) except Exception: - item = str(self.__item) - return {'function': str(self.__function), 'item': item, - 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), - 'previousconditionset': str(self.previousconditionset.get()), - 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + _item = str(self.__item) + _mindelta = self.__mindelta.get() + if _mindelta is None: + return {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get())} + else: + return {'function': str(self.__function), 'item': _item, + 'value': str(self.__value.get()), 'conditionset': str(self.conditionset.get()), + 'previousconditionset': str(self.previousconditionset.get()), + 'previousstate_conditionset': str(self.previousstate_conditionset.get()), + 'delta': str(self.__delta), 'mindelta': str(_mindelta)} # Class representing a single "se_setbyattr" action diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 3f5afcd6e..75b6dcd06 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -66,6 +66,8 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr for action in self.__states[state].get(label_type): _repeat = self.__states[state][label_type][action].get('repeat') _delay = self.__states[state][label_type][action].get('delay') or 0 + _delta = self.__states[state][label_type][action].get('delta') or 0 + _mindelta = self.__states[state][label_type][action].get('mindelta') or 0 condition_necessary = 0 condition_met = True condition_count = 0 @@ -112,8 +114,9 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr condition_met = False cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond2 = self.__states[state]['conditionsets'].get(conditionset) is not None - fontcolor = "white" if cond1 and cond2 and (not condition_met or (_repeat is False and originaltype == 'actions_stay'))\ - else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 else "black" + cond_delta = float(_delta) < float(_mindelta) + fontcolor = "white" if cond1 and cond2 and (cond_delta or (not condition_met or (_repeat is False and originaltype == 'actions_stay')))\ + else "#5c5646" if _delay > 0 else "darkred" if _delay < 0 else "#303030" if not condition_met else "black" condition_info = condition_to_meet if condition1 is False\ else previouscondition_to_meet if condition2 is False\ else previousstate_condition_to_meet if condition3 is False\ @@ -121,12 +124,17 @@ def _actionlabel(self, state, label_type, conditionset, previousconditionset, pr additionaltext = " ({} not met)".format(condition_info) if not condition_met\ else " (no repeat)" if _repeat is False and originaltype == 'actions_stay'\ else " (delay: {})".format(_delay) if _delay > 0\ - else " (wrong delay!)" if _delay < 0 else "" + else " (wrong delay!)" if _delay < 0\ + else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ + else "" action1 = self.__states[state][label_type][action].get('function') if action1 == 'set': action2 = self.__states[state][label_type][action].get('item') value_check = self.__states[state][label_type][action].get('value') value_check = '""' if value_check == "" else value_check + is_number = value_check.lstrip('-').replace('.','',1).isdigit() + if is_number and "." in value_check: + value_check = round(float(value_check), 2) action3 = 'to {}'.format(value_check) elif action1 == 'special': action2 = self.__states[state][label_type][action].get('special') From b1440090b0ec7043d43e98faab192a6f1fca7e9b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 3 Aug 2023 14:12:10 +0200 Subject: [PATCH 018/118] stateengine plugin: remove unneccessary code in web interface index.html --- stateengine/webif/templates/index.html | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/stateengine/webif/templates/index.html b/stateengine/webif/templates/index.html index b6b70a42f..6c5fb3be1 100755 --- a/stateengine/webif/templates/index.html +++ b/stateengine/webif/templates/index.html @@ -35,19 +35,8 @@ +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{_('Broker Host')}}{{ p.broker_config.host }}{{_('Broker Port')}}{{ p.broker_config.port }}
{{_('Benutzer')}}{{ p.broker_config.user }}{{_('Passwort')}} + {% if p.broker_config.password %} + {% for letter in p.broker_config.password %}*{% endfor %} + {% endif %} +
{{_('QoS')}}{{ p.broker_config.qos }}{{_('')}}{{ '' }}
+{% endblock headtable %} + + + +{% block buttons %} +{% endblock %} + + +{% set tabcount = 3 %} + + + +{% if p.shelly_items == [] %} + {% set start_tab = 2 %} +{% endif %} + + + +{% set tab1title = "" ~ p.get_shortname() ~ " Items" %} +{% block bodytab1 %} + + + + + + + + + + + + + + {% for item in p.shelly_items %} + + + + + + + + + + + + {% endfor %} +
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('Shelly Device') }}{{ _('Shelly ID') }}{{ _('Relais') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item._path }}{{ item._type }}{{ item() }}{{ p.get_iattr_value(item.conf, 'shelly_type') }}{{ p.get_iattr_value(item.conf, 'shelly_id') }}{% if p.shelly_relay %}{{ p.get_iattr_value(item.conf, 'shelly_relay') }}{% else %}0{% endif %}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
+{% endblock %} + + + +{% set tab2title = "" ~ p.get_shortname() ~ " Devices" %} +{% block bodytab2 %} + + + + + + + + + + + + + {% for device in p.shelly_devices %} + + + + + + + + + + + {% endfor %} +
{{ _('Shelly ID') }}{{ _('Online') }}{{ _('Mac Adresse') }}{{ _('IP Adresse') }}{{ _('Firmware Version') }}{{ _('neue Firmware') }}{{ _('konfiguriert') }}
{{ device }}{{ p.shelly_devices[device].online }}{{ p.shelly_devices[device].mac }}{{ p.shelly_devices[device].ip }}{{ p.shelly_devices[device].fw_ver }}{{ p.shelly_devices[device].new_fw }}{{ p.shelly_devices[device].connected_to_item }}
+{% endblock %} + + + +{% set tab3title = "" ~ " Broker Information" %} +{% block bodytab3 %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if p.broker_monitoring %} + + + + + + + + + + + + {% endif %} +
{{ 'Broker Version' }}{{ p._broker.version }}{{ connection_result }}
{{ 'Active Clients' }}{{ p._broker.active_clients }}
{{ 'Subscriptions' }}{{ p._broker.subscriptions }}
{{ 'Messages stored' }}{{ p._broker.stored_messages }}
{{ 'Retained Messages' }}{{ p._broker.retained_messages }}
 
{{ _('Laufzeit') }}{{ p.broker_uptime() }}
 
+ +{% if p.broker_monitoring %} + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Message Durchsatz') }}{{ _('letzte Minute') }}{{ _('letzte 5 Min.') }}{{ _('letzte 15 Min.') }}
{{ _('Durchschnittlich Messages je Minute empfangen') }}{{ p._broker.msg_rcv_1min }}     {{ p._broker.msg_rcv_5min }}     {{ p._broker.msg_rcv_15min }}
{{ _('Durchschnittlich Messages je Minute gesendet') }}{{ p._broker.msg_snt_1min }}     {{ p._broker.msg_snt_5min }}     {{ p._broker.msg_snt_15min }}
+{% endif %} + +{% endblock %} +msg_rcv_1min + + +{% block bodytab4 %} + + + + + + + + + + + + {% for item in items %} + {% if p.has_iattr(item.conf, 'mqtt_topic_in') or p.has_iattr(item.conf, 'mqtt_topic_out') %} + + + + + + + + + {% endif %} + {% endfor %} +
ItemTypWertTopic InLetztes UpdateLetzter Change
{{ item._path }}{{ item._type }}{{ item() }}{% if p.has_iattr(item.conf, 'mqtt_topic_in') %}{{ p.get_iattr_value(item.conf, 'mqtt_topic_in') }}{% endif %}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
+{% endblock %} + + diff --git a/shelly/user_doc.rst b/shelly/user_doc.rst index a19ce3012..abb1db782 100755 --- a/shelly/user_doc.rst +++ b/shelly/user_doc.rst @@ -9,13 +9,15 @@ shelly .. image:: webif/static/img/plugin_logo.png :alt: plugin logo :width: 300px - :height: 300px + :height: 90px :scale: 50 % :align: left Das Plugin dienst zur Steuerung von Shelly Devices über MQTT. Zur Aktivierung von MQTT für die Shelly Devices bitte die Dokumentation des jeweiligen Devices zu Rate ziehen. +| + Zurzeit werden folgende Shelly Devices mit Gen1 API unterstützt: - Shelly1/pm @@ -52,6 +54,8 @@ sowie der online-Status. Das Plugin kommuniziert über MQTT und benötigt das mqtt Modul, welches die Kommunikation mit dem MQTT Broker durchführt. Dieses Modul muß geladen und konfiguriert sein, damit das Plugin funktioniert. +| + .. toctree:: :hidden: @@ -59,106 +63,6 @@ sowie der online-Status. user_doc/plugin_configuration.rst -Shelly Device in Betrieb nehmen -=============================== - -Um Shelly Plugs mit diesem Plugin zu nutzen, müssen sie in das lokale WLAN eingebunden sein und die MQTT Unterstützung -muss aktiviert sein. - -Einbindung ins WLAN -------------------- - -Shelly in den AP-Modus versetzen - -- in die Steckdose stecken/an Strom anschließen -- Falls die LED nicht rot/blau blinken, den Taster drücken -> Shelly Plug wird in den AP Mode versetzt -- WLAN SSID suchen und verbinden (z.B. bei ShellyPlug-S die SSID shellyplug-s-xxxxxx) -- Im Browser die Seite http://192.168.33.1 aufrufen -- Einstellungen im Shelly vornehmen -> Einstellungen im Shelly-Hauptmenü - -Gen1 Devices einbinden -~~~~~~~~~~~~~~~~~~~~~~ - -- Fläche **Internet & Security** klicken -- **WIFI MODE - CLIENT** aufklappen -- Haken bei **Connect the Shelly device to an existing WiFi Network** setzen -- SSID und Password eingeben -- **SAVE** klicken -- Mit dem Browser unter der neuen IP Adresse (http://shellyplug-s-xxxxxx) im lokalen WLAN verbinden - -Gen2 Devices einbinden -~~~~~~~~~~~~~~~~~~~~~~ - -- In der Navigation links auf **Settings** klicken -- Im Abschnitt 'Network Settings' auf **Wi-Fi** klicken -- Im Abschnitt 'Wi-Fi 1 settings' Haken bei **Enable Wi-Fi Network** setzen -- SSID und Password eingeben -- **SAVE** klicken -- Mit dem Browser unter der neuen IP Adresse (http://shellyplug-s-xxxxxx) im lokalen WLAN verbinden - -| - -Firmware Update durchführen ---------------------------- - -Die Devices werden im allgemeinen mit einer älteren Firmware Version ausgeliefert. Deshalb sollte als erstes ein -Firmware Update durchgeführt werden. - -Update für Gen1 Devices -~~~~~~~~~~~~~~~~~~~~~~~ - -- Fläche **Settings** klicken -- **FIRMWARE UPDATE** aufklappen -- **UPDATE FIRMWARE** klicken - -Update für Gen2 Devices -~~~~~~~~~~~~~~~~~~~~~~~ - -- In der Navigation links auf **Settings** klicken -- Im Abschnitt 'Device Settings' auf **Firmware** klicken -- Den Button für die aktuelle **stable** Firmware klicken - -| - -MQTT konfigurieren ------------------- - -Für Gen1 Devices -~~~~~~~~~~~~~~~~ - -- Fläche **Internet & Security** klicken -- **ADVANCED - DEVELOPER SETTINGS** aufklappen -- Haken bei **Enable action execution via MQTT** setzen -- Falls der MQTT Broker ein Login erfordert, Username und Password eingeben -- Adresse des Brokers in der Form : eingeben (z.B.: 10.0.0.140:1883) -- Max QoS vorzugsweise auf **1** setzen -- **SAVE** klicken - -.. image:: user_doc/assets/gen1_mqtt_settings.jpg - :class: screenshot - -Für Gen2 Devices -~~~~~~~~~~~~~~~~ - -- In der Navigation links auf **Settings** klicken -- Im Abschnitt 'Connectivity' auf **MQTT** klicken -- Den Haken bei **Enable MQTT Network** setzen -- Den 'MQTT PREFIX' auf **shellies/gen2** konfigurieren -- IP-Adresse und Port des MQTT Brokers unter 'SERVER' konfigurieren -- Falls der Broker eine Anmeldung erfordert, 'USERNAME' und 'PASSWORD' konfigurieren -- **SAVE** klicken - -.. image:: user_doc/assets/gen2_mqtt_settings.jpg - :class: screenshot - -.. note:: - - Bei späteren Rekonfigurationen ist im allgemeinen das PASSWORD Feld leer und das Password muss - (bevor **Save Settings** geklickt wird) erneut eingegeben werden. Sonst verbindet sich das Device - nicht dem Broker. - -| - Konfiguration des Plugins ========================= diff --git a/shelly/user_doc/device_installation.rst b/shelly/user_doc/device_installation.rst index bf55d74fd..f35a894a8 100644 --- a/shelly/user_doc/device_installation.rst +++ b/shelly/user_doc/device_installation.rst @@ -76,7 +76,7 @@ Für Gen1 Devices - Max QoS vorzugsweise auf **1** setzen - **SAVE** klicken -.. image:: user_doc/assets/gen1_mqtt_settings.jpg +.. image:: assets/gen1_mqtt_settings.jpg :class: screenshot Für Gen2 Devices @@ -90,7 +90,7 @@ Für Gen2 Devices - Falls der Broker eine Anmeldung erfordert, 'USERNAME' und 'PASSWORD' konfigurieren - **SAVE** klicken -.. image:: user_doc/assets/gen2_mqtt_settings.jpg +.. image:: assets/gen2_mqtt_settings.jpg :class: screenshot .. note:: diff --git a/shelly/user_doc/assets/plugin_cpnfiguration.rst b/shelly/user_doc/plugin_configuration.rst similarity index 100% rename from shelly/user_doc/assets/plugin_cpnfiguration.rst rename to shelly/user_doc/plugin_configuration.rst From 3ced13c7c020af661f7ecf06cc0713ad86b6a075 Mon Sep 17 00:00:00 2001 From: aschwith Date: Thu, 17 Aug 2023 21:13:42 +0200 Subject: [PATCH 043/118] solarforecast: replaced deprecated sh.now() with method of lib shtime --- solarforecast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarforecast/__init__.py b/solarforecast/__init__.py index d0409061d..ddde696e2 100755 --- a/solarforecast/__init__.py +++ b/solarforecast/__init__.py @@ -141,7 +141,7 @@ def poll_backend(self): # Decode Json data: wattHoursToday = None wattHoursTomorrow = None - today = self._sh.now().date() + today = self._sh.shtime.now().date() tomorrow = today + datetime.timedelta(days=1) self.last_update = today From 033d9327905906ade43e2a7d9a47e4fc11d91c59 Mon Sep 17 00:00:00 2001 From: aschwith Date: Thu, 17 Aug 2023 21:46:03 +0200 Subject: [PATCH 044/118] resol: improved user_doc --- resol/user_doc.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/resol/user_doc.rst b/resol/user_doc.rst index 725daed24..2be1058ef 100755 --- a/resol/user_doc.rst +++ b/resol/user_doc.rst @@ -12,7 +12,10 @@ resol :scale: 50 % :align: left -Resol plugin, mit Unterstützung für Resol Solar Datenlogger, Frischwasserwaermetauscher und Regler. +Allgemein +========= + +Resol plugin, mit Unterstützung für Resol Solar Datenlogger, Frischwasserwärmetauscher und Regler. Konfiguration ============= @@ -20,3 +23,65 @@ Konfiguration Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/resol` beschrieben. +Weiterführende Informationen +============================ + +Weitere Informationen zu Resol Parametern und Quellen sind hier zu finden: + +https://github.com/danielwippermann/resol-vbus + +https://danielwippermann.github.io/resol-vbus/#/vsf + + +Resol Protokol +-------------- + +Synch byte beween messages: 0xAA + +Message: + + Byte Content + + 0-1 Destination + + 2-3 Source + + 4 Protocol Version, 0x10 -> "PV1", 0x20 -> "PV2", 0x30 -> "PV3" + + 5-6 Command + + 7-8 Frame count, Example 0x1047-> 104 bytes + + +Beispiele +========= + +.. code-block:: yaml + + solar: + resol_source@solar: '0x7721' + resol_destination@solar: '0x0010' + resol_command@solar: '0x0100' + + sensordefektmaske: + type: num + resol_offset@solar: 36 + resol_bituse@solar: 16 + resol_factor@solar: + - '1.0' + - '256.0' + + temperatur_1: + name: 'Temperature Kollektor' + type: num + resol_offset@solar: 0 + resol_bituse@solar: 16 + resol_factor@solar: + - '0.1' + - '25.6' + resol_isSigned@solar: + - False + - True + + + From 5721732c2975d10802b52d036911c48cefb9f473 Mon Sep 17 00:00:00 2001 From: aschwith Date: Thu, 17 Aug 2023 21:48:55 +0200 Subject: [PATCH 045/118] sonos: improved thread termination; reintroduced old legacy method play_radio --- sonos/__init__.py | 98 +++++++++++++++++++++++++++++++++++++++++++++-- sonos/plugin.yaml | 2 +- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/sonos/__init__.py b/sonos/__init__.py index deb39814b..0654fde61 100755 --- a/sonos/__init__.py +++ b/sonos/__init__.py @@ -273,7 +273,7 @@ def unsubscribe(self): self.logger.dbglow("Preparing to terminate thread") if debug: self.logger.dbghigh(f"unsubscribe(): Preparing to terminate thread for endpoint {self._endpoint}") - self._thread.join(2) + self._thread.join(timeout=4) if debug: self.logger.dbghigh(f"unsubscribe(): Thread joined for endpoint {self._endpoint}") @@ -283,7 +283,7 @@ def unsubscribe(self): self.logger.dbghigh(f"Thread killed for endpoint {self._endpoint}") else: - self.logger.error("unsubscibe(): Error, thread is still alive") + self.logger.warning("unsubscibe(): Error, thread is still alive after termination (join timed-out)") self._thread = None self.logger.info(f"Event {self._endpoint} unsubscribed and thread terminated") if debug: @@ -2298,9 +2298,99 @@ def play_sonos_radio(self, station_name: str, start: bool = True) -> None: return False return True - def _play_radio(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: """ + Old legacy radio select function. The function is not based on the SoCo library. + Plays a radio station by a given radio name. If more than one radio station are found, the first result will be + played. + :param station_name: radio station name + :param start: Start playing after setting the radio stream? Default: True + :return: None + """ + + # ------------------------------------------------------------------------------------------------------------ # + + # This code here is a quick workaround for issue https://github.com/SoCo/SoCo/issues/557 and will be fixed + # if a patch is applied. + + # ------------------------------------------------------------------------------------------------------------ # + + if not self._check_property(): + return False, "Property check failed" + if not self.is_coordinator: + sonos_speaker[self.coordinator].play_tunein(station_name, start) + else: + + data = 'anon' \ + 'Sonos' \ + 'search:station{search}' \ + '0100'.format( + search=station_name) + + headers = { + "SOAPACTION": "http://www.sonos.com/Services/1.1#search", + "USER-AGENT": "Linux UPnP/1.0 Sonos/40.5-49250 (WDCR:Microsoft Windows NT 10.0.16299)", + "CONTENT-TYPE": 'text/xml; charset="utf-8"' + } + + response = requests.post("http://legato.radiotime.com/Radio.asmx", data=data.encode("utf-8"), + headers=headers) + schema = XML.fromstring(response.content) + body = schema.find("{http://schemas.xmlsoap.org/soap/envelope/}Body")[0] + + response = list(xmltodict.parse(XML.tostring(body), process_namespaces=True, + namespaces={'http://www.sonos.com/Services/1.1': None}).values())[0] + + items = [] + # The result to be parsed is in either searchResult or getMetadataResult + if 'searchResult' in response: + response = response['searchResult'] + elif 'getMetadataResult' in response: + response = response['getMetadataResult'] + else: + raise ValueError('"response" should contain either the key ' + '"searchResult" or "getMetadataResult"') + return False, "response should contain either the key 'searchResult' or 'getMetadataResult'" + + for result_type in ('mediaCollection', 'mediaMetadata'): + # Upper case the first letter (used for the class_key) + result_type_proper = result_type[0].upper() + result_type[1:] + raw_items = response.get(result_type, []) + # If there is only 1 result, it is not put in an array + if isinstance(raw_items, OrderedDict): + raw_items = [raw_items] + + for raw_item in raw_items: + # Form the class_key, which is a unique string for this type, + # formed by concatenating the result type with the item type. Turns + # into e.g: MediaMetadataTrack + class_key = result_type_proper + raw_item['itemType'].title() + cls = get_class(class_key) + #from plugins.sonos.soco.music_services.token_store import JsonFileTokenStore + items.append( + cls.from_music_service(MusicService(service_name='TuneIn'), raw_item)) + #cls.from_music_service(MusicService(service_name='TuneIn', token_store=JsonFileTokenStore()), raw_item)) + + if not items: + exit(0) + + item_id = items[0].metadata['id'] + sid = 254 # hard-coded TuneIn service id ? + sn = 0 + meta = to_didl_string(items[0]) + + uri = "x-sonosapi-stream:{0}?sid={1}&flags=8224&sn={2}".format(item_id, sid, sn) + + self.soco.avTransport.SetAVTransportURI([('InstanceID', 0), + ('CurrentURI', uri), ('CurrentURIMetaData', meta)]) + if start: + self.soco.play() + return True, "" + + def _play_radio_dontuse(self, station_name: str, music_service: str = 'TuneIn', start: bool = True) -> tuple: + """ + WARNING: THIS FUNCTION IS NOT WORKING FOR SOME RADIO STATIONS, e.g. Plays a radio station by a given radio name at a given music service. If more than one radio station are found, the first result will be played. :param music_service: music service name Default: TuneIn @@ -2879,7 +2969,7 @@ class Sonos(SmartPlugin): """ Main class of the Plugin. Does all plugin specific stuff """ - PLUGIN_VERSION = "1.8.2" + PLUGIN_VERSION = "1.8.3" def __init__(self, sh): """Initializes the plugin.""" diff --git a/sonos/plugin.yaml b/sonos/plugin.yaml index 3f60f0aa7..4fca62837 100755 --- a/sonos/plugin.yaml +++ b/sonos/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://github.com/smarthomeNG/plugins/blob/master/sonos/README.md support: https://knx-user-forum.de/forum/supportforen/smarthome-py/25151-sonos-anbindung - version: 1.8.2 # Plugin version + version: 1.8.3 # Plugin version sh_minversion: 1.5.1 # minimum shNG version to use this plugin py_minversion: 3.8 # minimum Python version to use for this plugin multi_instance: False # plugin supports multi instance From 4441b781125bc87765c5ec3562706ff800814796 Mon Sep 17 00:00:00 2001 From: lgb-this Date: Sat, 19 Aug 2023 09:22:55 +0200 Subject: [PATCH 046/118] First commit of byd_bat. --- byd_bat/__init__.py | 981 +++++++++++++++++++++++ byd_bat/locale.yaml | 40 + byd_bat/plugin.yaml | 334 ++++++++ byd_bat/requirements.txt | 1 + byd_bat/user_doc.rst | 106 +++ byd_bat/webif/__init__.py | 164 ++++ byd_bat/webif/static/img/diag.JPG | Bin 0 -> 67618 bytes byd_bat/webif/static/img/home.JPG | Bin 0 -> 117340 bytes byd_bat/webif/static/img/plugin_logo.png | Bin 0 -> 44103 bytes byd_bat/webif/static/img/readme.txt | 6 + byd_bat/webif/static/img/temp.JPG | Bin 0 -> 70537 bytes byd_bat/webif/static/img/volt.JPG | Bin 0 -> 121379 bytes byd_bat/webif/templates/index.html | 473 +++++++++++ 13 files changed, 2105 insertions(+) create mode 100644 byd_bat/__init__.py create mode 100644 byd_bat/locale.yaml create mode 100644 byd_bat/plugin.yaml create mode 100644 byd_bat/requirements.txt create mode 100644 byd_bat/user_doc.rst create mode 100644 byd_bat/webif/__init__.py create mode 100644 byd_bat/webif/static/img/diag.JPG create mode 100644 byd_bat/webif/static/img/home.JPG create mode 100644 byd_bat/webif/static/img/plugin_logo.png create mode 100644 byd_bat/webif/static/img/readme.txt create mode 100644 byd_bat/webif/static/img/temp.JPG create mode 100644 byd_bat/webif/static/img/volt.JPG create mode 100644 byd_bat/webif/templates/index.html diff --git a/byd_bat/__init__.py b/byd_bat/__init__.py new file mode 100644 index 000000000..a6a236776 --- /dev/null +++ b/byd_bat/__init__.py @@ -0,0 +1,981 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2023 Matthias Manhart smarthome@beathis.ch +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Monitoring of BYD energy storage systems (HVM, HVS). +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +# ----------------------------------------------------------------------- +# +# History +# +# V0.0.1 230811 - erster Release +# +# V0.0.2 230812 - Korrektur Berechnung Batteriestrom +# +# V0.0.3 - Code mit pycodestyle kontrolliert/angepasst +# - Anpassungen durch 'check_plugin' +# +# ----------------------------------------------------------------------- +# +# Als Basis fuer die Implementierung wurde die folgende Quelle verwendet: +# +# https://github.com/christianh17/ioBroker.bydhvs +# +# Diverse Notizen +# +# - Datenpaket wird mit CRC16/MODBUS am Ende abgeschlossen (2 Byte, LSB,MSB) +# +# ----------------------------------------------------------------------- + +from lib.model.smartplugin import * +from lib.item import Items +from .webif import WebInterface +import cherrypy + +import socket +import time +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + +byd_ip_default = "192.168.16.254" + +scheduler_name = 'mmbyd' + +BUFFER_SIZE = 4096 + +byd_sample_basics = 60 # Abfrage fuer Basisdaten [s] + +byd_timeout_1s = 1.0 +byd_timeout_2s = 2.0 +byd_timeout_8s = 8.0 + +byd_tours_max = 3 +byd_cells_max = 160 +byd_temps_max = 64 + +byd_no_of_col = 8 + +byd_webif_img = "/webif/static/img/" +byd_path_empty = "x" +byd_fname_volt = "bydvt" +byd_fname_temp = "bydtt" +byd_fname_ext = ".png" + +MESSAGE_0 = "010300000066c5e0" +MESSAGE_1 = "01030500001984cc" +MESSAGE_2 = "010300100003040e" + +MESSAGE_3_1 = "0110055000020400018100f853" # Start Messung Turm 1 +MESSAGE_3_2 = "01100550000204000281000853" # Start Messung Turm 2 +MESSAGE_3_3 = "01100550000204000381005993" # Start Messung Turm 3 +MESSAGE_4 = "010305510001d517" +MESSAGE_5 = "01030558004104e5" +MESSAGE_6 = "01030558004104e5" +MESSAGE_7 = "01030558004104e5" +MESSAGE_8 = "01030558004104e5" + +MESSAGE_9 = "01100100000306444542554700176f" # switch to second turn for the last few cells (not tested, perhaps only for tower 1 ?) +MESSAGE_10 = "0110055000020400018100f853" # start measuring remaining cells (like 3a) (not tested, perhaps only for tower 1 ?) +MESSAGE_11 = "010305510001d517" # (like 4) (not tested) +MESSAGE_12 = "01030558004104e5" # (like 5) (not tested) +MESSAGE_13 = "01030558004104e5" # (like 6) (not tested) + +byd_errors = [ + "High Temperature Charging (Cells)", + "Low Temperature Charging (Cells)", + "Over Current Discharging", + "Over Current Charging", + "Main circuit Failure", + "Short Current Alarm", + "Cells Imbalance", + "Current Sensor Failure", + "Battery Over Voltage", + "Battery Under Voltage", + "Cell Over Voltage", + "Cell Under Voltage", + "Voltage Sensor Failure", + "Temperature Sensor Failure", + "High Temperature Discharging (Cells)", + "Low Temperature Discharging (Cells)" +] + +byd_invs = [ + "Fronius HV", + "Goodwe HV", + "Fronius HV", + "Kostal HV", + "Goodwe HV", + "SMA SBS3.7/5.0", + "Kostal HV", + "SMA SBS3.7/5.0", + "Sungrow HV", + "Sungrow HV", + "Kaco HV", + "Kaco HV", + "Ingeteam HV", + "Ingeteam HV", + "SMA SBS 2.5 HV", + "", + "SMA SBS 2.5 HV", + "Fronius HV" +] + +byd_invs_lvs = [ + "Fronius HV", + "Goodwe HV", + "Goodwe HV", + "Kostal HV", + "Selectronic LV", + "SMA SBS3.7/5.0", + "SMA LV", + "Victron LV", + "Suntech LV", + "Sungrow HV", + "Kaco HV", + "Studer LV", + "Solar Edge LV", + "Ingeteam HV", + "Sungrow LV", + "Schneider LV", + "SMA SBS2.5 HV", + "Solar Edge LV", + "Solar Edge LV", + "Solar Edge LV" +] + + +class byd_bat(SmartPlugin): + + """ + Main class of the Plugin. Does all plugin specific stuff and provides + the update functions for the items + + HINT: Please have a look at the SmartPlugin class to see which + class properties and methods (class variables and class functions) + are already available! + """ + + PLUGIN_VERSION = '0.0.2' + + def __init__(self,sh): + """ + Initalizes the plugin. + + If you need the sh object at all, use the method self.get_sh() to get it. There should be almost no need for + a reference to the sh object any more. + + Plugins have to use the new way of getting parameter values: + use the SmartPlugin method get_parameter_value(parameter_name). Anywhere within the Plugin you can get + the configured (and checked) value for a parameter by calling self.get_parameter_value(parameter_name). It + returns the value in the datatype that is defined in the metadata. + """ + + # Call init code of parent class (SmartPlugin) + super().__init__() + + # get the parameters for the plugin (as defined in metadata plugin.yaml): + + if self.get_parameter_value('ip') != '': + self.ip = self.get_parameter_value('ip') + else: + self.log_info("no ip defined => use default '" + byd_ip_default + "'") + self.ip = byd_ip_default + + if self.get_parameter_value('imgpath') != '': + self.bpath = self.get_parameter_value('imgpath') + if self.bpath is None: + self.log_info("path is None") + self.bpath = byd_path_empty + else: + self.log_info("no path defined") + self.bpath = byd_path_empty + + self.log_debug("BYD ip = " + self.ip) + self.log_debug("BYD path = " + self.bpath) + + # cycle time in seconds, only needed, if hardware/interface needs to be + # polled for value changes by adding a scheduler entry in the run method of this plugin + # (maybe you want to make it a plugin parameter?) + self._cycle = byd_sample_basics + + self.last_diag_hour = 99 # erzwingt beim ersten Aufruf das Abfragen der Detaildaten + + self.byd_root_found = False + + self.byd_diag_soc = [] + self.byd_diag_volt_max = [] + self.byd_diag_volt_max_c = [] + self.byd_diag_volt_min = [] + self.byd_diag_volt_min_c = [] + self.byd_diag_temp_max_c = [] + self.byd_diag_temp_min_c = [] + self.byd_volt_cell = [] + self.byd_temp_cell = [] + for x in range(0,byd_tours_max + 1): + self.byd_diag_soc.append(0) + self.byd_diag_volt_max.append(0) + self.byd_diag_volt_max_c.append(0) + self.byd_diag_volt_min.append(0) + self.byd_diag_volt_min_c.append(0) + self.byd_diag_temp_max_c.append(0) + self.byd_diag_temp_min_c.append(0) + a = [] + for xx in range(0,byd_cells_max + 1): + a.append(0) + self.byd_volt_cell.append(a) + a = [] + for xx in range(0,byd_temps_max + 1): + a.append(0) + self.byd_temp_cell.append(a) + + self.last_homedata = self.now_str() + self.last_diagdata = self.now_str() + + # Initialization code goes here + + self.sh = sh + + self.init_webinterface() + + return + + def run(self): + """ + Run method for the plugin + """ + self.scheduler_add(scheduler_name,self.poll_device,cycle=self._cycle) + + self.alive = True + + return + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.debug("Stop method called") + self.scheduler_remove('poll_device') + self.alive = False + + def parse_item(self, item): + """ + Default plugin parse_item method. Is called when the plugin is initialized. + The plugin can, corresponding to its attribute keywords, decide what to do with + the item in future, like adding it to an internal array for future reference + :param item: The item to process. + :return: If the plugin needs to be informed of an items change you should return a call back function + like the function update_item down below. An example when this is needed is the knx plugin + where parse_item returns the update_item function when the attribute knx_send is found. + This means that when the items value is about to be updated, the call back function is called + with the item, caller, source and dest as arguments and in case of the knx plugin the value + can be sent to the knx with a knx write function within the knx plugin. + """ + # todo + # if interesting item for sending values: + # self._itemlist.append(item) + # return self.update_item + if self.get_iattr_value(item.conf,'byd_root'): + self.byd_root = item + self.byd_root_found = True + self.log_debug("BYD root = " + "{0}".format(self.byd_root)) + + def parse_logic(self, logic): + """ + Default plugin parse_logic method + """ + if 'xxx' in logic.conf: + # self.function(logic['name']) + pass + + def update_item(self, item, caller=None, source=None, dest=None): + # Wird aufgerufen, wenn ein Item mit dem Attribut 'mmgarden' geaendert wird + + if self.alive and caller != self.get_shortname(): + # code to execute if the plugin is not stopped + # and only, if the item has not been changed by this plugin: + + return + + def poll_device(self): + # Wird alle 'self._cycle' aufgerufen + + self.log_debug("BYD Start *********************") + + if self.byd_root_found is False: + self.log_debug("BYD not root found - please define root item with structure 'byd_struct'") + return + + # Verbindung herstellen + client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + try: + client.connect((self.ip,8080)) + except: + self.log_info("client.connect failed (" + self.ip + ")") + self.byd_root.info.connection(False) + client.close() + return + + # 1.Befehl senden + client.send(bytes.fromhex(MESSAGE_0)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 0 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_0(data) + + # 2.Befehl senden + client.send(bytes.fromhex(MESSAGE_1)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 1 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_1(data) + + # 3.Befehl senden + client.send(bytes.fromhex(MESSAGE_2)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 2 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_2(data) + + # Speichere die Basisdaten + self.basisdata_save(self.byd_root) + + # Pruefe, ob die Diagnosedaten abgefragt werden sollen + tn = self.now() + if tn.hour == self.last_diag_hour: + self.byd_root.info.connection(True) + self.log_debug("BYD Basic Done ****************") + client.close() + return + + self.last_diag_hour = tn.hour + + # Durchlaufe alle Tuerme + for x in range(1,self.byd_bms_qty + 1): + self.log_debug("Turm " + str(x)) + + # 4.Befehl senden + if x == 1: + client.send(bytes.fromhex(MESSAGE_3_1)) + elif x == 2: + client.send(bytes.fromhex(MESSAGE_3_2)) + elif x == 3: + client.send(bytes.fromhex(MESSAGE_3_3)) + client.settimeout(byd_timeout_2s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 3 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_nop(data,x) + time.sleep(2) + + # 5.Befehl senden + client.send(bytes.fromhex(MESSAGE_4)) + client.settimeout(byd_timeout_8s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 4 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_nop(data,x) + + # 6.Befehl senden + client.send(bytes.fromhex(MESSAGE_5)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 5 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_5(data,x) + + # 7.Befehl senden + client.send(bytes.fromhex(MESSAGE_6)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 6 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_6(data,x) + + # 8.Befehl senden + client.send(bytes.fromhex(MESSAGE_7)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 7 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_7(data,x) + + # 9.Befehl senden + client.send(bytes.fromhex(MESSAGE_8)) + client.settimeout(byd_timeout_1s) + + try: + data = client.recv(BUFFER_SIZE) + except: + self.log_info("client.recv 8 failed") + self.byd_root.info.connection(False) + client.close() + return + self.decode_8(data,x) + + self.diagdata_save(self.byd_root) + self.byd_root.info.connection(True) + + self.log_debug("BYD Diag Done +++++++++++++++++") + client.close() + + return + + def decode_0(self,data): + # Decodieren der Nachricht auf Befehl 'MESSAGE_0'. + + self.log_debug("decode_0: " + data.hex()) + + # Serienummer + self.byd_serial = "" + for x in range(3,22): + self.byd_serial = self.byd_serial + chr(data[x]) + + # Firmware-Versionen + self.byd_bmu_a = "V" + str(data[27]) + "." + str(data[28]) + self.byd_bmu_b = "V" + str(data[29]) + "." + str(data[30]) + if data[33] == 0: + self.byd_bmu = self.byd_bmu_a + "-A" + else: + self.byd_bmu = self.byd_bmu_b + "-B" + self.byd_bms = "V" + str(data[31]) + "." + str(data[32]) + "-" + chr(data[34] + 65) + + # Anzahl Tuerme und Anzahl Module pro Turm + self.byd_bms_qty = data[36] // 0x10 + if (self.byd_bms_qty == 0) or (self.byd_bms_qty > byd_tours_max): + self.byd_bms_qty = 1 + self.byd_modules = data[36] % 0x10 + self.byd_batt_type_snr = data[5] + + # Application + if data[38] == 1: + self.byd_application = "OnGrid" + else: + self.byd_application = "OffGrid" + + self.log_debug("Serial : " + self.byd_serial) + self.log_debug("BMU A : " + self.byd_bmu_a) + self.log_debug("BMU B : " + self.byd_bmu_b) + self.log_debug("BMU : " + self.byd_bmu) + self.log_debug("BMS : " + self.byd_bms) + self.log_debug("BMS QTY : " + str(self.byd_bms_qty)) + self.log_debug("Modules : " + str(self.byd_modules)) + self.log_debug("Application : " + self.byd_application) + return + + def decode_1(self,data): + # Decodieren der Nachricht auf Befehl 'MESSAGE_1'. + + self.log_debug("decode_1: " + data.hex()) + + self.byd_soc = self.buf2int16SI(data,3) + self.byd_soh = self.buf2int16SI(data,9) + + self.byd_volt_bat = self.buf2int16US(data,13) * 1.0 / 100.0 + self.byd_volt_out = self.buf2int16US(data,35) * 1.0 / 100.0 + self.byd_volt_max = self.buf2int16SI(data,5) * 1.0 / 100.0 + self.byd_volt_min = self.buf2int16SI(data,7) * 1.0 / 100.0 + self.byd_volt_diff = self.byd_volt_max - self.byd_volt_min + self.byd_current = self.buf2int16SI(data,11) * 1.0 / 10.0 + self.byd_power = self.byd_volt_out * self.byd_current + if self.byd_power >= 0: + self.byd_power_discharge = self.byd_power + self.byd_power_charge = 0 + else: + self.byd_power_discharge = 0 + self.byd_power_charge = -self.byd_power + + self.byd_temp_bat = self.buf2int16SI(data,19) + self.byd_temp_max = self.buf2int16SI(data,15) + self.byd_temp_min = self.buf2int16SI(data,17) + + self.byd_error_nr = self.buf2int16SI(data,29) + self.byd_error_str = "" + for x in range(0,16): + if (((1 << x) & self.byd_error_nr) != 0): + if len(self.byd_error_str) > 0: + self.byd_error_str = self.byd_error_str + ";" + self.byd_error_str = self.byd_error_str + byd_errors[x] + if len(self.byd_error_str) == 0: + self.byd_error_str = "no error" + + self.byd_param_t = str(data[31]) + "." + str(data[32]) + + self.log_debug("SOC : " + str(self.byd_soc)) + self.log_debug("SOH : " + str(self.byd_soh)) + self.log_debug("Volt Battery : " + str(self.byd_volt_bat)) + self.log_debug("Volt Out : " + str(self.byd_volt_out)) + self.log_debug("Volt max : " + str(self.byd_volt_max)) + self.log_debug("Volt min : " + str(self.byd_volt_min)) + self.log_debug("Volt diff : " + str(self.byd_volt_diff)) + self.log_debug("Current : " + str(self.byd_current)) + self.log_debug("Power : " + str(self.byd_power)) + self.log_debug("Temp Battery : " + str(self.byd_temp_bat)) + self.log_debug("Temp max : " + str(self.byd_temp_max)) + self.log_debug("Temp min : " + str(self.byd_temp_min)) + self.log_debug("Error : " + str(self.byd_error_nr) + " " + self.byd_error_str) + self.log_debug("ParamT : " + self.byd_param_t) + return + + def decode_2(self,data): + # Decodieren der Nachricht auf Befehl 'MESSAGE_2'. + + self.log_debug("decode_2: " + data.hex()) + + self.byd_batt_type = data[5] + if self.byd_batt_type == 0: + # HVL -> unknown specification, so 0 cells and 0 temps + self.byd_batt_str = "HVL" + self.byd_capacity_module = 4.0 + self.byd_volt_n = 0 + self.byd_temp_n = 0 + self.byd_cells_n = 0 + self.byd_temps_n = 0 + elif self.byd_batt_type == 1: + # HVM 16 Cells per module + self.byd_batt_str = "HVM" + self.byd_capacity_module = 2.76 + self.byd_volt_n = 16 + self.byd_temp_n = 8 + self.byd_cells_n = self.byd_modules * self.byd_volt_n + self.byd_temps_n = self.byd_modules * self.byd_temp_n + elif self.byd_batt_type == 2: + # HVS 32 cells per module + self.byd_batt_str = "HVS" + self.byd_capacity_module = 2.56 + self.byd_volt_n = 32 + self.byd_temp_n = 12 + self.byd_cells_n = self.byd_modules * self.byd_volt_n + self.byd_temps_n = self.byd_modules * self.byd_temp_n + else: + if (self.byd_batt_type_snr == 49) or (self.byd_batt_type_snr == 50): + self.byd_batt_str = "LVS" + self.byd_capacity_module = 4.0 + self.byd_volt_n = 7 + self.byd_temp_n = 0 + self.byd_cells_n = self.byd_modules * self.byd_volt_n + self.byd_temps_n = 0 + else: + self.byd_batt_str = "???" + self.byd_capacity_module = 0.0 + self.byd_volt_n = 0 + self.byd_temp_n = 0 + self.byd_cells_n = 0 + self.byd_temps_n = 0 + + self.byd_capacity_total = self.byd_bms_qty * self.byd_modules * self.byd_capacity_module + + self.byd_inv_type = data[3] + if self.byd_batt_str == "LVS": + self.byd_inv_str = byd_invs_lvs[self.byd_inv_type] + else: + self.byd_inv_str = byd_invs[self.byd_inv_type] + + self.log_debug("Inv Type : " + self.byd_inv_str + " (" + str(self.byd_inv_type) + ")") + self.log_debug("Batt Type : " + self.byd_batt_str + " (" + str(self.byd_batt_type) + ")") + self.log_debug("Cells n : " + str(self.byd_cells_n)) + self.log_debug("Temps n : " + str(self.byd_temps_n)) + + if self.byd_cells_n > byd_cells_max: + self.byd_cells_n = byd_cells_max + if self.byd_temps_n > byd_temps_max: + self.byd_temps_n = byd_temps_max + return + + def decode_5(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_5'. + + self.log_debug("decode_5 (" + str(x) + ") : " + data.hex()) + + self.byd_diag_soc[x] = self.buf2int16SI(data,53) * 1.0 / 10.0 + self.byd_diag_volt_max[x] = self.buf2int16SI(data,5) / 1000.0 + self.byd_diag_volt_max_c[x] = data[9] + self.byd_diag_volt_min[x] = self.buf2int16SI(data,7) / 1000.0 + self.byd_diag_volt_min_c[x] = data[10] + self.byd_diag_temp_max_c[x] = data[15] + self.byd_diag_temp_min_c[x] = data[16] + + # starting with byte 101, ending with 131, Cell voltage 1-16 + for xx in range(0,16): + self.byd_volt_cell[x][xx] = self.buf2int16SI(data,101 + (xx * 2)) / 1000.0 + + self.log_debug("SOC : " + str(self.byd_diag_soc[x])) + self.log_debug("Volt max : " + str(self.byd_diag_volt_max[x]) + " c=" + str(self.byd_diag_volt_max_c[x])) + self.log_debug("Volt min : " + str(self.byd_diag_volt_min[x]) + " c=" + str(self.byd_diag_volt_min_c[x])) + self.log_debug("Temp max : " + " c=" + str(self.byd_diag_temp_max_c[x])) + self.log_debug("Temp min : " + " c=" + str(self.byd_diag_temp_min_c[x])) +# for xx in range(0,16): +# self.log_debug("Turm " + str(x) + " Volt " + str(xx) + " : " + str(self.byd_volt_cell[x][xx])) + + return + + def decode_6(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_6'. + + self.log_debug("decode_6 (" + str(x) + ") : " + data.hex()) + + for xx in range(0,64): + self.byd_volt_cell[x][16 + xx] = self.buf2int16SI(data,5 + (xx * 2)) / 1000.0 + +# for xx in range(0,64): +# self.log_debug("Turm " + str(x) + " Volt " + str(16 + xx) + " : " + str(self.byd_volt_cell[x][16 + xx])) + + return + + def decode_7(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_7'. + + self.log_debug("decode_7 (" + str(x) + ") : " + data.hex()) + + # starting with byte 5, ending 101, voltage for cell 81 to 128 + for xx in range(0,48): + self.byd_volt_cell[x][80 + xx] = self.buf2int16SI(data,5 + (xx * 2)) / 1000.0 + + # starting with byte 103, ending 132, temp for cell 1 to 30 + for xx in range(0,30): + self.byd_temp_cell[x][xx] = data[103 + xx] + +# for xx in range(0,48): +# self.log_debug("Turm " + str(x) + " Volt " + str(80 + xx) + " : " + str(self.byd_volt_cell[x][80 + xx])) +# for xx in range(0,30): +# self.log_debug("Turm " + str(x) + " Temp " + str(xx) + " : " + str(self.byd_temp_cell[x][xx])) + + return + + def decode_8(self,data,x): + # Decodieren der Nachricht auf Befehl 'MESSAGE_8'. + + self.log_debug("decode_8 (" + str(x) + ") : " + data.hex()) + + for xx in range(0,34): + self.byd_temp_cell[x][30 + xx] = data[5 + xx] + +# for xx in range(0,34): +# self.log_debug("Turm " + str(x) + " Temp " + str(30 + xx) + " : " + str(self.byd_temp_cell[x][30 + xx])) + + return + + def decode_nop(self,data,x): +# self.log_debug("decode_nop (" + str(x) + ") : " + data.hex()) + return + + def basisdata_save(self,device): + # Speichert die Basisdaten in der sh-Struktur. + + self.log_debug("basisdata_save") + + device.state.current(self.byd_current) + device.state.power(self.byd_power) + device.state.power_charge(self.byd_power_charge) + device.state.power_discharge(self.byd_power_discharge) + device.state.soc(self.byd_soc) + device.state.soh(self.byd_soh) + device.state.tempbatt(self.byd_temp_bat) + device.state.tempmax(self.byd_temp_max) + device.state.tempmin(self.byd_temp_min) + device.state.voltbatt(self.byd_volt_bat) + device.state.voltdiff(self.byd_volt_diff) + device.state.voltmax(self.byd_volt_max) + device.state.voltmin(self.byd_volt_min) + device.state.voltout(self.byd_volt_out) + + device.system.bms(self.byd_bms) + device.system.bmu(self.byd_bmu) + device.system.bmubanka(self.byd_bmu_a) + device.system.bmubankb(self.byd_bmu_b) + device.system.batttype(self.byd_batt_str) + device.system.errornum(self.byd_error_nr) + device.system.errorstr(self.byd_error_str) + device.system.grid(self.byd_application) + device.system.invtype(self.byd_inv_str) + device.system.modules(self.byd_modules) + device.system.bmsqty(self.byd_bms_qty) + device.system.capacity_total(self.byd_capacity_total) + device.system.paramt(self.byd_param_t) + device.system.serial(self.byd_serial) + + self.last_homedata = self.now_str() + + return + + def diagdata_save(self,device): + # Speichert die Diagnosedaten in der sh-Struktur. + + self.log_debug("diagdata_save") + + self.diagdata_save_one(device.diagnosis.tower1,1) + if self.byd_bms_qty > 1: + self.diagdata_save_one(device.diagnosis.tower2,2) + if self.byd_bms_qty > 2: + self.diagdata_save_one(device.diagnosis.tower3,3) + + self.last_diagdata = self.now_str() + + return + + def diagdata_save_one(self,device,x): + + device.soc(self.byd_diag_soc[x]) + device.volt_max.volt(self.byd_diag_volt_max[x]) + device.volt_max.cell(self.byd_diag_volt_max_c[x]) + device.volt_min.volt(self.byd_diag_volt_min[x]) + device.volt_min.cell(self.byd_diag_volt_min_c[x]) + device.temp_max_cell(self.byd_diag_temp_max_c[x]) + device.temp_min_cell(self.byd_diag_temp_min_c[x]) + + self.diag_plot(x) + +# self.log_debug("Turm " + str(x)) +# for xx in range(0,self.byd_cells_n): +# self.log_debug("Volt " + str(xx+1) + " : " + str(self.byd_volt_cell[x][xx])) +# for xx in range(0,self.byd_temps_n): +# self.log_debug("Temp " + str(xx+1) + " : " + str(self.byd_temp_cell[x][xx])) + + return + + def diag_plot(self,x): + + # Heatmap der Spannungen + i = 0 + j = 1 + rows = self.byd_cells_n // byd_no_of_col + d = [] + rt = [] + for r in range(0,rows): + c = [] + for cc in range(0,byd_no_of_col): + c.append(self.byd_volt_cell[x][i]) + i = i + 1 + d.append(c) + rt.append("M" + str(j)) + if ((r + 1) % (self.byd_volt_n // self.byd_modules)) == 0: + j = j + 1 + dd = np.array(d) + + fig,ax = plt.subplots(figsize=(10,4)) # Erzeugt ein Bitmap von 1000x500 Pixel + + im = ax.imshow(dd) + cbar = ax.figure.colorbar(im,ax=ax,shrink=0.5) + cbar.ax.yaxis.set_tick_params(color='white') + cbar.outline.set_edgecolor('white') + plt.setp(plt.getp(cbar.ax.axes,'yticklabels'),color='white') + + ax.set_aspect(0.25) + ax.get_xaxis().set_visible(False) + ax.set_yticks(np.arange(len(rt)),labels=rt) + + ax.spines[:].set_visible(False) + ax.set_xticks(np.arange(dd.shape[1] + 1) - .5,minor=True) + ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True,size=10) + ax.tick_params(which='minor',bottom=False,left=False) + ax.tick_params(axis='y',colors='white') + + textcolors = ("white","black") + threshold = im.norm(dd.max()) / 2. + kw = dict(horizontalalignment="center",verticalalignment="center",size=9) + valfmt = matplotlib.ticker.StrMethodFormatter("{x:.3f}") + + # Loop over data dimensions and create text annotations. + for i in range(0,rows): + for j in range(0,byd_no_of_col): + kw.update(color=textcolors[int(im.norm(dd[i,j]) > threshold)]) + text = ax.text(j,i,valfmt(dd[i,j], None),**kw) + + ax.set_title("Turm " + str(x) + " - Spannungen [V]" + " (" + self.now_str() + ")",size=10,color='white') + + fig.tight_layout() + if len(self.bpath) != byd_path_empty: + fig.savefig(self.bpath + byd_fname_volt + str(x) + byd_fname_ext,format='png',transparent=True) + self.log_debug("save " + self.bpath + byd_fname_temp + str(x) + byd_fname_ext) + fig.savefig(self.get_plugin_dir() + byd_webif_img + byd_fname_volt + str(x) + byd_fname_ext, + format='png',transparent=True) + self.log_debug("save " + self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(x) + byd_fname_ext) + plt.close('all') + + # Heatmap der Temperaturen + i = 0 + j = 1 + rows = self.byd_temps_n // byd_no_of_col + d = [] + rt = [] + for r in range(0,rows): + c = [] + for cc in range(0,byd_no_of_col): + c.append(self.byd_temp_cell[x][i]) + i = i + 1 + d.append(c) + rt.append("M" + str(j)) + if ((r + 1) % (self.byd_temp_n // self.byd_modules)) == 0: + j = j + 1 + dd = np.array(d) + cmap = matplotlib.colors.LinearSegmentedColormap.from_list('',['#f5f242','#ffaf38','#fc270f']) + norm = matplotlib.colors.TwoSlopeNorm(vcenter=dd.min() + (dd.max() - dd.min()) / 2, + vmin=dd.min(),vmax=dd.max()) + + fig,ax = plt.subplots(figsize=(10,2.5)) # Erzeugt ein Bitmap von 1000x400 Pixel + + im = ax.imshow(dd,cmap=cmap,norm=norm) + cbar = ax.figure.colorbar(im,ax=ax,shrink=0.5) + cbar.ax.yaxis.set_tick_params(color='white') + cbar.outline.set_edgecolor('white') + plt.setp(plt.getp(cbar.ax.axes,'yticklabels'),color='white') + + ax.set_aspect(0.28) + ax.get_xaxis().set_visible(False) + ax.set_yticks(np.arange(len(rt)),labels=rt) + + ax.spines[:].set_visible(False) + ax.set_xticks(np.arange(dd.shape[1] + 1) - .5,minor=True) + ax.set_yticks(np.arange(dd.shape[0] + 1) - .5,minor=True,size=10) + ax.tick_params(which='minor',bottom=False,left=False) + ax.tick_params(axis='y',colors='white') + + textcolors = ("black","white") + threshold = im.norm(dd.max()) / 2. + kw = dict(horizontalalignment="center",verticalalignment="center",size=9) + valfmt = matplotlib.ticker.StrMethodFormatter("{x:.0f}") + + # Loop over data dimensions and create text annotations. + for i in range(0,rows): + for j in range(0,byd_no_of_col): + kw.update(color=textcolors[int(im.norm(dd[i,j]) > threshold)]) + text = ax.text(j,i,valfmt(dd[i,j], None),**kw) + + ax.set_title("Turm " + str(x) + " - Temperaturen [°C]" + " (" + self.now_str() + ")",size=10,color='white') + + fig.tight_layout() + if len(self.bpath) != byd_path_empty: + fig.savefig(self.bpath + byd_fname_temp + str(x) + byd_fname_ext,format='png',transparent=True) + self.log_debug("save " + self.bpath + byd_fname_temp + str(x) + byd_fname_ext) + fig.savefig(self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(x) + byd_fname_ext, + format='png',transparent=True) + self.log_debug("save " + self.get_plugin_dir() + byd_webif_img + byd_fname_temp + str(x) + byd_fname_ext) + plt.close('all') + + return + + def buf2int16SI(self,byteArray,pos): # signed + result = byteArray[pos] * 256 + byteArray[pos + 1] + if (result > 32768): + result -= 65536 + return result + + def buf2int16US(self,byteArray,pos): # unsigned + result = byteArray[pos] * 256 + byteArray[pos + 1] + return result + + def now_str(self): + return self.now().strftime("%d.%m.%Y, %H:%M:%S") + + def log_debug(self,s1): + self.logger.debug(s1) + + def log_info(self,s1): + self.logger.warning(s1) + + # webinterface init method + def init_webinterface(self): + + """" + Initialize the web interface for this plugin + + This method is only needed if the plugin is implementing a web interface + """ + try: + self.mod_http = Modules.get_instance().get_module( + 'http') # try/except to handle running in a core version that does not support modules + except: + self.mod_http = None + if self.mod_http is None: + self.logger.error("Not initializing the web interface") + return False + + import sys + if not "SmartPluginWebIf" in list(sys.modules['lib.model.smartplugin'].__dict__): + self.logger.warning("Web interface needs SmartHomeNG v1.5 and up. Not initializing the web interface") + return False + + # set application configuration for cherrypy + webif_dir = self.path_join(self.get_plugin_dir(), 'webif') + config = { + '/': { + 'tools.staticdir.root': webif_dir, + }, + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static' + } + } + + # Register the web interface as a cherrypy app + self.mod_http.register_webif(WebInterface(webif_dir,self), + self.get_shortname(), + config, + self.get_classname(), + self.get_instance_name(), + description='') + + return True diff --git a/byd_bat/locale.yaml b/byd_bat/locale.yaml new file mode 100644 index 000000000..f29ff2617 --- /dev/null +++ b/byd_bat/locale.yaml @@ -0,0 +1,40 @@ +# translations for the web interface +plugin_translations: + # Translations for the plugin specially for the web interface + 'BYD Home': {'de': '=', 'en': 'BYD Home'} + 'BYD Diagnose': {'de': '=', 'en': 'BYD Diagnostics'} + 'BYD Spannungen': {'de': '=', 'en': 'BYD Voltages'} + 'BYD Temperaturen': {'de': '=', 'en': 'BYD Temperatures'} + + 'Laden': {'de': '=', 'en': 'Loading'} + 'Gesamtkapazität': {'de': '=', 'en': 'Total capacity'} + 'Batterieladung': {'de': '=', 'en': 'Battery charge'} + 'Ladeleistung': {'de': '=', 'en': 'Charging power'} + 'Entladeleistung': {'de': '=', 'en': 'Discharge power'} + 'Bilder-Pfad': {'de': '=', 'en': 'Image path'} + 'Basisdaten': {'de': '=', 'en': 'Basic values'} + 'Diagnosedaten': {'de': '=', 'en': 'Diagnostic values'} + + 'Leistung': {'de': '=', 'en': 'Power'} + 'Spannung Ausgang': {'de': '=', 'en': 'Voltage output'} + 'Strom Ausgang': {'de': '=', 'en': 'Current output'} + 'Spannung Batterie': {'de': '=', 'en': 'Voltage battery'} + 'Spannung Batteriezellen max': {'de': '=', 'en': 'Voltage battery cell max'} + 'Spannung Batteriezellen min': {'de': '=', 'en': 'Voltage battery cell min'} + 'Spannung Batteriezellen Differenz': {'de': '=', 'en': 'Voltage battery cell delta'} + 'Temperatur Batterie': {'de': '=', 'en': 'Temperature battery'} + 'Temperatur Batterie max': {'de': '=', 'en': 'Temperature battery max'} + 'Temperatur Batterie min': {'de': '=', 'en': 'Temperature battery min'} + + 'Wechselrichter': {'de': '=', 'en': 'Inverter'} + 'Batterietyp': {'de': '=', 'en': 'Battery type'} + 'Seriennummer': {'de': '=', 'en': 'Serial number'} + 'Türme': {'de': '=', 'en': 'Towers'} + 'Module pro Turm': {'de': '=', 'en': 'Modules per tower'} + 'Parameter': {'de': '=', 'en': 'Parameter'} + 'Fehler': {'de': '=', 'en': 'Error'} + + # Alternative format for translations of longer texts: + 'Hier kommt der Inhalt des Webinterfaces hin.': + de: '=' + en: 'Here goes the content of the web interface.' diff --git a/byd_bat/plugin.yaml b/byd_bat/plugin.yaml new file mode 100644 index 000000000..67944d0de --- /dev/null +++ b/byd_bat/plugin.yaml @@ -0,0 +1,334 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: interface # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Plugin fuer die Anzeige von Daten von BYD Batterien' + en: 'Plugin to display data from BYD batteries' + maintainer: Matthias Manhart + tester: Matthias Manhart + state: develop # change to ready when done with development +# keywords: iot xyz +# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1886748-support-thread-f%C3%BCr-das-byd-batterie-plugin + + version: 0.0.2 # Plugin version (must match the version specified in __init__.py) + sh_minversion: 1.9 # minimum shNG version to use this plugin +# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) +# py_minversion: 3.6 # minimum Python version to use for this plugin +# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) + multi_instance: false # plugin supports multi instance + restartable: true + classname: byd_bat # class containing the plugin + +parameters: + + ip: + type: ip + default: '192.168.16.254' + description: + de: "IP-Adresse der BYD Batterie (Master)" + en: "IP address of the BYD battery (master)" + + imgpath: + type: str + default: '' + description: + de: "Pfad fuer Heatmap-Bilder (z.Bsp. fuer smartvisu)" + en: "Path for heatmap images (e.g. for smartvisu)" + +item_attributes: + + byd_root: # only used internally - do not use for own items ! + type: bool + mandatory: True + description: + de: 'Root-Flag fuer das Plugin' + en: 'Root-Flag for plugin' + +item_structs: + + byd_struct: + + byd_root: true # must stay here - used by the plugin internally - do not remove ! + + info: + + connection: # shows connection status of plugin to battery + type: bool + initial_value: false + enforce_updates: true + visu_acl: ro + + state: + + current: # [A] Charge / Discharge Current + type: num + visu_acl: ro + database: init + + power: # [W] Power (+ discharge / - charge battery) + type: num + visu_acl: ro + database: init + + power_charge: # [W] Power charging + type: num + visu_acl: ro + database: init + + power_discharge: # [W] Charge discharging + type: num + visu_acl: ro + database: init + + soc: # [%] SOC + type: num + visu_acl: ro + database: init + + soh: # [%] SOH + type: num + visu_acl: ro + database: init + + tempbatt: # [°C] Battery Temperature + type: num + visu_acl: ro + database: init + + tempmax: # [°C] Max Cell Temp + type: num + visu_acl: ro + database: init + + tempmin: # [°C] Min Cell Temp + type: num + visu_acl: ro + database: init + + voltbatt: # [V] Battery Voltage + type: num + visu_acl: ro + database: init + + voltdiff: # [V] Max - Min Cell Voltage + type: num + visu_acl: ro + database: init + + voltmax: # [V] Max Cell Voltage + type: num + visu_acl: ro + database: init + + voltmin: # [V] Min Cell Voltage + type: num + visu_acl: ro + database: init + + voltout: # [V] Output Voltage + type: num + visu_acl: ro + database: init + + system: + + bms: # F/W BMS + type: str + cache: true + visu_acl: ro + + bmu: # F/W BMU + type: str + cache: true + visu_acl: ro + + bmubanka: # F/W BMU-BankA + type: str + cache: true + visu_acl: ro + + bmubankb: # F/W BMU-BankB + type: str + cache: true + visu_acl: ro + + batttype: # Battery Type + type: str + cache: true + visu_acl: ro + + errornum: # Error (numeric) + type: num + cache: true + visu_acl: ro + + errorstr: # Error (string) + type: str + cache: true + visu_acl: ro + + grid: # Parameter Table + type: str + cache: true + visu_acl: ro + + invtype: # Inverter Type + type: str + cache: true + visu_acl: ro + + modules: # modules (count) + type: num + cache: true + visu_acl: ro + + bmsqty: # tours (count) + type: num + cache: true + visu_acl: ro + + capacity_total: # Total capacitiy (all modules) [kWh] + type: num + cache: true + visu_acl: ro + + paramt: # F/W BMU + type: str + cache: true + visu_acl: ro + + serial: # Serial number + type: str + cache: true + visu_acl: ro + + diagnosis: + + tower1: + + soc: # [%] SOC + type: num + visu_acl: ro + database: init + + volt_max: + + volt: # max voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of max voltage + type: num + visu_acl: ro + database: init + + volt_min: + + volt: # min voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min voltage + type: num + visu_acl: ro + database: init + + temp_max_cell: # cell number of max temperature + type: num + visu_acl: ro + database: init + + temp_min_cell: # cell number of min temperature + type: num + visu_acl: ro + database: init + + tower2: + + soc: # [%] SOC + type: num + visu_acl: ro + database: init + + volt_max: + + volt: # max voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of max voltage + type: num + visu_acl: ro + database: init + + volt_min: + + volt: # min voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min voltage + type: num + visu_acl: ro + database: init + + temp_max_cell: # cell number of max temperature + type: num + visu_acl: ro + database: init + + temp_min_cell: # cell number of min temperature + type: num + visu_acl: ro + database: init + + tower3: + + soc: # [%] SOC + type: num + visu_acl: ro + database: init + + volt_max: + + volt: # max voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of max voltage + type: num + visu_acl: ro + database: init + + volt_min: + + volt: # min voltage [V] + type: num + visu_acl: ro + database: init + + cell: # cell number of min voltage + type: num + visu_acl: ro + database: init + + temp_max_cell: # cell number of max temperature + type: num + visu_acl: ro + database: init + + temp_min_cell: # cell number of min temperature + type: num + visu_acl: ro + database: init + +logic_parameters: NONE + +plugin_functions: NONE diff --git a/byd_bat/requirements.txt b/byd_bat/requirements.txt new file mode 100644 index 000000000..6ccafc3f9 --- /dev/null +++ b/byd_bat/requirements.txt @@ -0,0 +1 @@ +matplotlib diff --git a/byd_bat/user_doc.rst b/byd_bat/user_doc.rst new file mode 100644 index 000000000..9bddd962d --- /dev/null +++ b/byd_bat/user_doc.rst @@ -0,0 +1,106 @@ +.. index:: Plugins; byd_bat +.. index:: byd_bat + +======= +byd_bat +======= + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + +Anzeigen von Parametern eines BYD Energiespeichers. Die Parameter entsprechen den Daten, die in der Software Be_Connect_Plus_V2.0.2 angezeigt werden. + +Es werden 1-3 Türme unterstützt. + +Die Grunddaten werden alle 60 Sekunden aktualisiert. Die Diagnosedaten werden beim Start des Plugin und dann immer zur vollen Stunde abgerufen. + +Die Spannungen und Temperaturen in den Modulen werden mit Hilfe von Heatmaps dargestellt. Diese werden im Web Interface angezeigt. Zusätzlich können diese Bilder auch in ein weiteres Verzeichnis kopiert werden (z.Bsp. für smartvisu). + +Das Pflugin benoetigt nur ein Item mit der folgenden Deklaration: + +byd: + struct: byd_bat.byd_struct + +Alle verfügbaren Daten werden im Struct 'byd_struct' bereitgestellt. Diverse Parameter besitzen bereits die Eigenschaft 'database: init', so dass die Daten für die Visualisierung bereitgestellt werden. + +Anforderungen +------------- + +Der BYD Energiespeicher muss mit dem LAN verbunden sind. Die IP-Adresse des BYD wird über DHCP zugewiesen und muss ermittelt werden. Diese IP-Adresse muss in der Plugin-Konfiguration gespeichert werden. + +Notwendige Software +~~~~~~~~~~~~~~~~~~~ + +* matplotlib + +Unterstützte Geräte +~~~~~~~~~~~~~~~~~~~ + +Folgende Typen werden unterstützt: + +* HVS (noch nicht getestet) +* HVM (getestet mit HVM 19.3kWh und 2 Türmen) +* HVL (noch nicht getestet) +* LVS (noch nicht getestet) + +Bitte Debug-Daten (level: DEBUG) von noch nicht getesteten BYD Energiespeichern an Plugin-Autor senden. Beim Start von smarthomeng werden die Diagnosedaten sofort ermittelt. + +Konfiguration +------------- + +plugin.yaml +~~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +items.yaml +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +logic.yaml +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + + +Funktionen +~~~~~~~~~~ + +Bitte die Dokumentation lesen, die aus den Metadaten der plugin.yaml erzeugt wurde. + +Web Interface +------------- + +Ein Web Interface ist implementiert und zeigt die eingelesenen Daten an. + +Beispiele +--------- + +Oben rechts werden die wichtigsten Daten zum BYD Energiespeicher angezeigt. + +Im Tab "BYD Home" sind die Grunddaten des Energiespeichers dargestellt: + +.. image:: assets/home.JPG + :class: screenshot + +Im Tab "BYD Diagnose" werden Diagnosedaten angezeigt: + +.. image:: assets/diag.JPG + :class: screenshot + +Im Tab "BYD Spannungen" werden die Spannungen der Module als Heatmap angezeigt: + +.. image:: assets/volt.JPG + :class: screenshot + +Im Tab "BYD Temperaturen" werden die Temperaturen der Module als Heatmap angezeigt: + +.. image:: assets/temp.JPG + :class: screenshot diff --git a/byd_bat/webif/__init__.py b/byd_bat/webif/__init__.py new file mode 100644 index 000000000..0bdd656ac --- /dev/null +++ b/byd_bat/webif/__init__.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2023 Matthias Manhart smarthome@beathis.ch +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# This file implements the web interface for the byd_bat plugin. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +import datetime +import time +import os +import json + +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf + + +# ------------------------------------------ +# Webinterface of the plugin +# ------------------------------------------ + +import cherrypy +import csv +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + + self.tplenv = self.init_template_environment() + + @cherrypy.expose + def index(self, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + pagelength = self.plugin.get_parameter_value('webif_pagelength') + tmpl = self.tplenv.get_template('index.html') + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + return tmpl.render(p=self.plugin, + webif_pagelength=pagelength, + items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), + item_count=0) + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + # get the new data + data = {} + data['bydip'] = self.plugin.ip + data['imppath'] = self.plugin.bpath + data['last_homedata'] = self.plugin.last_homedata + data['last_diagdata'] = self.plugin.last_diagdata + + data['current'] = f'{self.plugin.byd_current:.1f}' + " A" + data['power'] = f'{self.plugin.byd_power:.1f}' + " W" + data['power_charge'] = f'{self.plugin.byd_power_charge:.1f}' + " W" + data['power_discharge'] = f'{self.plugin.byd_power_discharge:.1f}' + " W" + data['soc'] = f'{self.plugin.byd_soc:.1f}' + " %" + data['soh'] = f'{self.plugin.byd_soh:.1f}' + " %" + data['tempbatt'] = f'{self.plugin.byd_temp_bat:.1f}' + " °C" + data['tempmax'] = f'{self.plugin.byd_temp_max:.1f}' + " °C" + data['tempmin'] = f'{self.plugin.byd_temp_min:.1f}' + " °C" + data['voltbatt'] = f'{self.plugin.byd_volt_bat:.1f}' + " V" + data['voltdiff'] = f'{self.plugin.byd_volt_diff:.3f}' + " V" + data['voltmax'] = f'{self.plugin.byd_volt_max:.3f}' + " V" + data['voltmin'] = f'{self.plugin.byd_volt_min:.3f}' + " V" + data['voltout'] = f'{self.plugin.byd_volt_out:.1f}' + " V" + + data['bms'] = self.plugin.byd_bms + data['bmu'] = self.plugin.byd_bmu + data['bmubanka'] = self.plugin.byd_bmu_a + data['bmubankb'] = self.plugin.byd_bmu_b + data['batttype'] = self.plugin.byd_batt_str + data['errorstr'] = self.plugin.byd_error_str + " (" + str(self.plugin.byd_error_nr) + ")" + data['grid'] = self.plugin.byd_application + data['invtype'] = self.plugin.byd_inv_str + data['modules'] = str(self.plugin.byd_modules) + data['bmsqty'] = str(self.plugin.byd_bms_qty) + data['capacity_total'] = f'{self.plugin.byd_capacity_total:.2f}' + " kWh" + data['paramt'] = self.plugin.byd_param_t + data['serial'] = self.plugin.byd_serial + + data['t1_soc'] = f'{self.plugin.byd_diag_soc[1]:.1f}' + " %" + data['t1_volt_max'] = f'{self.plugin.byd_diag_volt_max[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[1]) + ")" + data['t1_volt_min'] = f'{self.plugin.byd_diag_volt_min[1]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[1]) + ")" + data['t1_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[1]) + data['t1_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[1]) + if self.plugin.byd_bms_qty > 1: + data['t2_soc'] = f'{self.plugin.byd_diag_soc[2]:.1f}' + " %" + data['t2_volt_max'] = f'{self.plugin.byd_diag_volt_max[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[2]) + ")" + data['t2_volt_min'] = f'{self.plugin.byd_diag_volt_min[2]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[2]) + ")" + data['t2_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[2]) + data['t2_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[2]) + else: + data['t2_soc'] = "-" + data['t2_soc'] = "-" + data['t2_volt_max'] = "-" + data['t2_volt_min'] = "-" + data['t2_temp_max_cell'] = "-" + data['t2_temp_min_cell'] = "-" + if self.plugin.byd_bms_qty > 2: + data['t3_soc'] = f'{self.plugin.byd_diag_soc[3]:.1f}' + " %" + data['t3_soc'] = f'{self.plugin.byd_diag_soc[3]:.1f}' + " %" + data['t3_volt_max'] = f'{self.plugin.byd_diag_volt_max[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_max_c[3]) + ")" + data['t3_volt_min'] = f'{self.plugin.byd_diag_volt_min[3]:.3f}' + " V (" + str(self.plugin.byd_diag_volt_min_c[3]) + ")" + data['t3_temp_max_cell'] = str(self.plugin.byd_diag_temp_max_c[3]) + data['t3_temp_min_cell'] = str(self.plugin.byd_diag_temp_min_c[3]) + else: + data['t3_soc'] = "-" + data['t3_soc'] = "-" + data['t3_soc'] = "-" + data['t3_volt_max'] = "-" + data['t3_volt_min'] = "-" + data['t3_temp_max_cell'] = "-" + data['t3_temp_min_cell'] = "-" + + # return it as json the the web page + try: + return json.dumps(data) + except Exception as e: + self.logger.error("get_data_html exception: {}".format(e)) diff --git a/byd_bat/webif/static/img/diag.JPG b/byd_bat/webif/static/img/diag.JPG new file mode 100644 index 0000000000000000000000000000000000000000..9c12b92dbd31d2ad5e13aec4857650aa56d0d96d GIT binary patch literal 67618 zcmeFZ1ymi&vNyhQOK=PBkl^kXf(IwKJ8aw?0>RxS1PJaD+}%QOcX#(qkbFBi_uQLv z&$;h=_y6Ae*7~oP*{hkJnV#;hUsqRGSJm)s_S-4|RZ2`!3;+QE0nh{g0N>V7A4Ofw zO#lEH8Ndqw000kwhTsK2f^%r#3#lM;05muc0nTfsefWL`kO2Uwllue!IPkqE;A;VJ z0ubzj|Ge3e7XrT!_}_?tjJ~6znYq3LiHyFrnZCUv7z3DSs2@57 zCRleUiiZAszBM-HM;?lU2>^(V{YU;E`UQOYcMN~&7ylbM=@-qv5cq|_F9d!e@C$)| zAi%=J!pg(M#ly@@!py?M#>KebUrM zO=51uPp!%>!z5!XVr*tE;cjoNA{X2cLn1iHVQ# zpUUwueh1^Hj{f)d{6B@30H2W|kKy;e+t~lsypplqe{Hla%?18hz5b0-@Ppqn;3mNQ zhk$^<$M|#We}?179WeWW-}^hWg5M#z00%^PHBB7LSCHmGK1&^$Xe;-z$NDgNH{%K*U5s!hAu1 zOYq{~KEAaB(BL7KpktsQ$N-RN5Kw3k-?{}HK2@M4U0}b|PLU@6n1EA4h&`FtvVKEf+;mGVTS>DEcgeQMl*@2}vc1po& zVDE>3h>e5$43CnEn&$ZnHg*n9E^Z!?SE6F#5|UC%%5PLu)zmc%jf_o9&CD$v9G#q9 zT;1IL-vtB)1&4&j#>FQjCMBn&W@YE(=H(X@7JaI!uBol7Z)oi7>h9_7>mT?$J~25p zJu^EuzqY=yxwXBsySIOKesOtqeFM7v`kgKa0Mu_}{hhLZq6-a77bG+^6g1p-x*#B3 zzzGTs8ite^7F}2YPTvlLjO8sn=F6Cml^qD=tcs^t2KHl!*c5DQlxN>b`$5@%jxfLf zPn7+guz%1s4|oCv0UkUkG=L!Bt5_&aCglG}dY&c9AkReCBAyH234L2`m8g&L`s0X2 zM!6JS=b9*{Jj8#PSR@tcY)1H*a=rmxn9i?X+1#zQb^20eeZ14sDg6fUtXK;#rR-Qg$dX+J%d;_%N-_t-^ydbXW*_zHVObBZTUR}?FbW;M;y7R6h&xFDPsp4Y`qrTF z@}k*1PVFubhfl&wrzJj9jJT**HmBvNE{qH`1>r{bVN^e==K}C3vaM#qP^+*zN(guBhd1~U@8dt{askU2&^d*&)&0N zmdLHOEfNe)_e+&&BISr}VjR;7Zaec|2& zmg43yI43+&g(ZX9_Pph&+UB8|ULUZ+b_yxZzP_QUt4NDooXV^^RC>Q5J&EP#Rz#fB1l14{ zEg5O{JHcQ`+u?I8xFWR21@PWJJ&*KON>eaD^Y6Mz7a$@CiHhGZ#9BkS8}wUyMap61 zG`udfah3z;BkF*ifzC(nvyPUf0koC&*6OVc@G9x5@U*1fbt}311_)`zZ>gBHyc9$g z!7e0h83@U#jSo(z8DwRe8-+*@;v#|VrGcu;s*m98n^y^1mBCjy3ktT%OTR*9&=2?q z7)O@-Qu*!)02lfQit7psF4UXRjEH#bvgKv3=;Vx|-4^^@G<+j(T=i;U`wQDSel9kM z6$P#9xQ&sgz*udn_k|^;=GJvefq@d&uaud5QpCjI`P`?Td%tjHf!9m)GB=P8Gju}p za8vcvpJQRkSQY~~arNVp`SR@3rs@W#V|t;_sA`*F7`ixItL4nzvU~$LmT@wcNkBmp zy;GGI`aaWsODc>ZPGJ#vgzTk?oSg0nE!nreX%ta@V(>c?xAL%4SW(6MbsQSoVTP7u z3k5QC_H3b&+7(THOyW+@t}hSNIUbomc;hbt>#oo8a!o#U@fVre%*TX#z;Sm8Fpr2~ zLd+;=8R;-g9(j{!xcaCB&h#gCVo-L|s9G(AATO(}!H2XHq^+8!R+WH?wg>w(7=jF+ zC<@ze+2qOgk#d_H&4AQ8x@v#WRXWh|}d#V_a9BCnS(UL@FYEcve3Zc zDkb2M!&y)SGAAc}X|h5Ae@aF3;WsMtjuYnBCSSM2kB{!hh%VCX`+Rxh2HLrx&$OC} zSXy);bbv{T&ffr*f#;gab)h0#$2hQk?xZ#px5O+DDV;AdpMR zw=}=taufJ5Ikdw`iaH2zoFNylEm0m{ws7DlZwQSqYRS})4`v0x3z!<#XRHAjwN`Ol zGkTD6bTrLJm(|op_^jBHQsWGeT&EK3#kLl|Xd*TxHH_er5K>|dTHsxOv@!pvIk#+H z$BEaxO*z?3e=X0!>W3;Jjg1sQWJ*oVXHf2{q|q>4y^z?d<~?Ta#-0fLr*kCbDORFUaWeS(juyveAL4&V)?h3eI~Y$Y09Z}jt! zc1b)(@)0;Sp-I?89PY9|Et0(J%9YkHSdm;SbVBlYPmK0jguQYC;PF=(O>- zuiv{2PGkKh?h%E`nP+NH_8v>4&ZWa zn<(Gg%9>+3Wx!ByAH0zriI_ z;!IB?3v^xTQ>i&vrVU`sZEBd}G}&Y`j1PS?)wtRXU8RdkAG-zX2F=Gi?#N+9Sm@2G z8*;XYHR<4cYBvK)^>XVFm*`U@G;P(0iG*^e%0YAWg5~LbBOo58vkqF6N~pO{wLH1o zR>QD`Ht>S{-V=|m*T2DLQO--W}^3k)a&bF8EYBF zFg;{_SheMXs(}&UWl=G8!+Ig5E)T^L;(_K&Ho~Dq)S$M>0j58pj=&b8=#6TPrsgA{ zCGOhB;~SuZwfv)%Pw%I_BzwVGD}7`GrHDhjnX5c}{k14;HO;-__2GM#YKqP0xSQTi zKFVWP@HJc0Y)bgA$8PPrNZajtx$mDRx(np{l#3Pt)3tm|6-oVA(=b*91EgZNR-~=A zIdQydS|7z!Lb624bobllhQ+d;9u!ARo)jNY)p_%bT>NH7Nn-q7^ii5o1A1=EZ3f`YJLxWLT2amCVMYo=hU+{owlS+(2CEpw9t z>b`S$Sk zc$Z@2_&U`R>0%Y(pno$|_*WMSb6ro~Y4J`kjy}l=YuId?5{6uc3^yOlGUf)UkT-9T zg;nn5lzKc+enMmvF8E%)YAQp0^u)4s;`KD2oY_48R!W+LGAs2N;}h6JjL|1=BCMwm zx-#(j_-)oo$>Ww(x1&Tz1BgHTeLq^Nx)Q-v-|(T-vS< z`k6}5nsZFEV9v@uUSuOtQy@{bz@WjY{ZMw2ti->-WEL$!{n z3i57$1CDPNY`1Is0N`mD!P|{JnD>5fkr=bootj%c!z@`{7@*4-y@(dMxHH~J{8I&) z$SO8)=GOu*Mr|&iB6kj1gWs7>9!S!#dD9t!Uvm)y$>_aRoudAjPZ#C!?OF|H%SHsIcLp*`Q-5b?I^zrp1EZh}@XT0=S z;hCrs|Q&w#Hxvm2AcwbUPYocqz(~+Xz3ttae{B7p_je(%}pDA@hPW%$#jfS##N|7*=oBWeGb z^Cn&+UzV4#uwE>{ia(4Xw92@OYnTN~WOWt1yBfw6ckA9y|#eU-gdOP>|p z;iw;sBw_exuKuqNUIRU~X!=YCPeNdEPS^(j)?#4a7r7O2isL~)*-@zqatgh#s;WQZ z3emau73Lu^+h_f$pm;jcdoK=YZGj1emb=#%4WzAZPa@Gc$pYl>jYses>egndsEils6v6mpbTgJI8~Xr%dylgyh%VV?9I+ZD!C4r zbl2YYb%}RlDV(SIxa9n2k|lddDycbk5UOUnQxyYZo?;Ts7*ibz$49Pyvu1!>;g;iWOP3C0r#q@Lj z(2fy#^V5{rg`$#CQVHZ<1i72Pa|y&9M=A&#ME(*Ic>RW$bFezv<6^wXRaK_Bwt2dS zc0um&tVjo8qjI^#015A-%dFIieQqSe(ExW@&`)*lK?JPPvz)y4SJB*U&bBMo))Kvd zYUK1Rwsq}4bSwS(-z6!seA&$Yu-Ek$B9PgX#x8Z<&*YVGVIoL#+&L@JmhQr1DEs2{ zlw(n;gR-L4c1oL()&@qg)M- zfL7B?YOF3>1SW`$D{F$_Y|qye{13*<4B{}Ku(%s-Ot2;AX8BV2dIlXRPRe_AWoRTu zcmCa$3U-1OTKHYgmtVVlUE1J#+lx}ZrfQb=;Y0V-*wEn_b=$*b#)jAkI#|bDk$hwy zSJ3c~qY$sUFy*Z3PX7kD8TX06=aW1EYXH=s8A(y1i2=C(Jr7M7(i%sbbn8q%pIk6r zmKMcDzfK&yOmI3}m^-_# zTDWat$yXcsY5|=nRzmt|fd?mdN1v1>F}f$K!-mLOvJ-CV;Y$kj?y*~jEx+J5bw&fj z&TkJcGm*deO4>EOXV5U1h+FWWA?t;&;E*i*-nXWl?&HPYW>?#TOY5u`s4?z8(Q7qr4Y$ zkP2K0Vty~$6h7GOq@6M>hJst9Lw9Bs6y)e7QI?%wR%)ZqK2;NcQQ#&e_N9$V1XU_M zJ=s*ZB{tdmL#`u-#2ewkF;oH(#x z{ymR)jj3Fvo+7z26rbR;B~A8U&WipbSDwjSJ>16#Bn9ZMDpcl7@=;y|Int zDI_@+b0f4E0yBm3m5k6{BGsDZae0P&masXl8$lI-CmOIrnd}QI$p&E`xFlls$uuC|;C?jVqpAk%9jbv!Im|?$5RPoNL>cjiw~b1{-95Dr?0j zoI6aP%`&p3)4QyQTr$fCk)9u#*w>v(RQR-e-lQsjS=6GbjA(g}LQM=|5LI^UJRR)O z^JOb2Dd1CDyDMA^T+p5{NnB^8WA1aqXA9;uj)&^U*QD@q{EC-f2)gKUHw|QPx+vDr zo?WO6ugUGUe@4;kS<|K0KyWnDCI#3vE9YqirPtxj_{DWam^$dMKckS2BVD0yK*G1Y zB}Ux%a{G3ew%iBX)rlnCbmp=qbXnoaIDUU;_YRjB_gXz4kt-WSsi^TcK&M>hH-NE^ zg^$(V%G}+3Tg^k6Ag#-d%g(dwA!(~}bDShqmD<$O_#14uyVuNG20szO2(9{d;xrT9P&QmdVDB%hHG$a8EW& z9KtsMabNMkw$SGJuJvvnRlh@aIRbt^U7e+5^rvLo&y%brR$^$MW)m|n+Qlrc*VK#z z*SgDoBv2X2S<;lrZ&~C-2Rfg+l?R@0Aofvk?gt#+=743eN3(k=d zRGK8cU?Af}t9E``!K`jX2NbKgZZ%4e4$Yx9lV~P{?Txh3GRhg z1VH`A9>3UvWm&}FBu}Hy#VG*+v&v1vz8zf`s0WZ-B}y)H^0cM9Y;pH;kmIQ1K_E5{ zoVVdOdh#|pJG!A?f06-Z=f}+2eI#pbnAsY0YGJ`g+N##Z5MLb+POA`Hl5LgNPxY>9YeN z?AzKpLgB-#Nt^qrx*AsaSLd`$@J%7yykhQOrwEnS-{hs7Uc5e2olSR~ zJ9DlO$Av6}FkC#^-&PvolhwMx__05L5`SsZ1mqzX3taG+xH5DcfW1-tR zRzp87MT?OjK81W>t#f5wwjo2_bj+gGTnA55aDw98>qG!HREB~Oe*Ay5_WktC9xrLI zcvxbXV&uFG!IQLTPlpU=9=k*)ybfaLlf9A!_H6~X%Em6Rd1~>4#o9{_SY$8PqPK4; zkh2Re^T?LLPIOWU@BCX;Ij!C#aGvW&UI;;y9up9+$oe9@3gU__pigh;5)-ihyutDk z_l8Zssm{97mrmrd{H*YK^fy3}h8JnTAB@~8-Yq2yAP=FIsS`!jx~kwovQ7wlxJ-&C zk8BVhMeR9s(df1(OG?@4@zuw^0ji%VPOm>My<%yyFCZlJHJl|dsi}VIb7${mAXw!_ zJeL|ayYppf+v=6o1FXO!^N4IUMCFh0AB8FFlOxhPD^{7uQ8xy3Zlw{m=7NFSoI$5~ z?s;x@_rJMM%Q)9nqrL&wbj(y5qpoePqvjc25$R@ZS>38veJIUnnjqYT`t+O8YJ+$k zc_d5$M19A~dvyN|Q2UUb->-?!ac9W;ed>gY8NZ@C#8NhjwFP9tfM=3TNUG@2P~jbN zA@T<|!5FJz8B6SGQ);BZrFvLCNM)!4ZJCFM+NAtNUEMDgnwq^viDJ7&u#%Zbn5wk7 zo{F@|HYVlQ0nO&24O|tE`FLzjiG>TGt+zrhTVO zl#>p~L@+BMe(E|Xer7rEx2xFF?Tk)m!!1)tYCH_qdQN;%EF*!xlm6Ep}?PS)E3RbnUKb2kS#DVQonjn{co;mR*yRymTwKM}) zcp0+h$&;DtG12zhxQ=D`8LMXL91V+W_o0BL#JsAMjnhkekp@j}VEbAM%6junSyjg) zqN_Kd>;Caw*LhwYU+Oz&EB+LWYAaYw(U_(u+ChUexgK7aT6CUsB_QeUgKQAh*=39H z;o;?UT_XHtLTlo(!8p3c^lN0Xj<^Y;`r(?OcQu;yylbnqHB)6DgS2C}-!x3%ElYEK z2}Vi5t9qwZ%)|}#ROe0<2Z@-~)nEKfpw!91!EQ}kitr74Pkc>c&=cDnkF;ZqtlLkq zwl1bN_L5|^>?0^Eny&tRl|Ukdwwdx8uXA&rcN|$}t1@M(H$lfN5%q6wlMA#QM@HEc zY%5pYbv~520^fkV%0|{&bk3Itr4#43k)#zbUL9=%o*{EQTy0e_XGnclCscKsK$_RQ zllaG=DK)u@gecbKb|L;0meZH-{Ob5Uk%J%Nkxi-sWl zN?oyiXSDrPndJ=L{uu4{Q(iIK&(EY{5k~lCXtO+-#vP+jMN60VHF_HgH1h`CYCJ~x z+PR09;R+8jEEQ}CPv?<t&2P1%L)Z8{jq4OxX+=|q6u4(Zk@?%}j{f&sjVHP;B}1R}vqMb}lkxkyydnNYugkN6Ze zn4p_{zewPMDG!SiP>pgJ1!=xAhlv-oFm10+icbu*8D$Gh?aeirD<9-6h zpv#?FMX-er<@n-l|K(R2sP^h1>Rz6unb6Ye?DeY&(=i=diyHlS zOk-Kq1rSanb{4R;b>+^m8t3*n6&5vn)^chu#y%REO&i-9NAGMg^9f?rOkhWwFlA;`>vM7rHBsWc?W|tLKYc7qf8NsM zFFZ|eb?nJ%hs5{wm5PCSh1eJYB9?tufMr8FU%2Y%C3s3)l;(Hs5qGU1LqFnV96c?N zV7IqS7K}Fmxb;0ls>?fOKGLuQ;T_8`t_Qp$B619sPIxaBQp%By#o_=f#_IpJZW8(G z_(w%UGeK8ghEgCOtF;y=rPHgW==9~|Z5g(nExFD1ytUfMaN(|6gn5t9WZ^RPd+G@` zBpaBeg+aqYH}_;7t`<{*$482Itmry&a$dXe0NRX9nygjN%F;c&z=T!eXI|yflWYf) zS3vP<=>=ooPmedz1<-E2i-))bs)jZ>7ODdLNouD-Z>=49y;w!lmo+))4ntTD9k*o(q%%=fC5%&V~0>$iKIF4R?)|gAsrJYDrWFE=2`Hn_ZAe)y8@iSLb zm8R3yikJ=?hlf0$3jOMc_n$4Z`Vb*M3nMb{w3QO)j+@%R(smpHw*bW<% z&}OyiN34ujF3n1mhmk!_Uo>1Kym-bh87)1>L*u+9`nq-8=HJBK9gbEJB8~o-*%)d72_4s=bg9h zv0~g>s<~rAL@wFzjv46MD6~$^>Ojty=kpHW70ZMs^G=M?uv+pQs>0aLPu608 zULbXuYPyCP9b|o{QZr4rme_;cbF6`*cFiZMOiU$mj=BSt(Q=;V?t}@ktxAM)26Qz& zYK$zmaw`z|_+G)vPVWX?Y@3X7_JGBoJRK>4+80i#sroda<*3}>F`1nuaGO^m!LM`7 zvNQa-V)rqW{|E&UoYY*FFJJcB@cTkn`|ZzOJ~GYqIA_h+aba#q=Gk8;I(Omv%WNM_ zy(4@vr&@@+IF&%--vHuMbS8M%sWz0=_Bo(mjxt_nB@?Nxd68?r z?`);4pP@6=+;to|)O_Xo-g5x@^^;H(3W;NhJ}oa=oe8nju~vf8)(i@1gUBR&ee(XN zCaXakTZ2c#?zOx#9zSd}B!6)0|BJ{qZhk#Om5kHJmjww=)5517Odnrf__X^(m(V`7 zk(e}7*9GMnC!{5xHztse1b`&{z0mVp}6 z*&IncLuNBcQja=Q-irTObr1;AaFFccq%V-qpT~1!Fdm3;P z>`m<&*=gmVR6Rj&Wx1$1x5Osy5yJh1Tj4w{pm(E=yN3ww)CBDIoF`1IdUB1fsT-j{ zA0J>A{2@02pXAK1fq-aHRoC?6g1T!EIMl;UrZ<;>`Admd4;m~|wITUQ+GhF^4HVp3 z%hgeDMB(f{XPmBN&i)LKyVkG+MP;wp_)e34WroFWob*EZK#^DDRoz0cKrgsKaV-v) zcYVn2ru;7mQM#@h;jW`*H&ej-5ysrsq#-@$0V)<8fM>H^T84WHTTA18YW_mGrdPfH zp^4344OD*n^`9(TvuS0;D`*^9JHV1%| zjYVmf0+t01m(Z%b>vD+m_p)Q-UES{9m03MNTdJs=>4h9#H7Bun!*me;^S52D5%z{=|S z!wb#gpqOax+4s`uK3gSjeNGl9mp<l2-y{x5Mloac$v=hw>mfQv`vX9^4!a-33 z;v!qz$_3Ai%i_*V*N-FMf)s(pKj=Mh#V`)skY+K?l8u`&O{Cga0!U%pez<(x4T5RsfYXduqkP&p zA@nC#z*13iBbVj|^xN)2#bK#qYPd;c9(kCP;PL`>S1lmV>xrCs{%)6X5zGM5ZWKmrH^yu0sI|Ee)c(&&I5=q7T5r3Kf+1N)>)H3>LgsZgmjNDSW+*NL-l)n20xEeV^JAUA~Gs&p{0dv)~=GVUg zl)@Its;&2+^8|4KgLQJmr_U}E|O)AtF)9;`f(`r>T$`#<+>>_ z!)kJ&n;T#mZnz~J3D=Orns};)v~fZR1ODR55q#Lo{x7c*gR6}|!Nd|vPsd;`?C zB`D1}2_EVq@AKp*?05xEYevyJFWsTmF0O8H_1apr1vssMB%A5=OCB-~S1iN6oV%RN z4d5=lO;3-9DBKc%=(#UA&wgBgd^7AC)k}OD1EWF5wW@vy500W)TE)N3^5feOS+!Fsme7=Iv(;xj%cKeMwCrrS~zw=zhyQ*Yi zH_@E`vo!6{iwa<<)6F63g_DnN zS%9aR46+Er_Cl6+_f)>@&A$yOk+RRC?&pG8AgYB%Vy>wTLBm9znsI{f?6HDY0Lf1(Z$E{Ps_k z3*q6|aBY@1naJ;d>R?e|K#7m_o}|~q2cLXv6WywPR5!(N0;4iHxEi^_eU(&!B)fur zC3t%J=mENS7&YP*n-4GmwLh@PmXj{g!kon;X3eLXJFB`7XlWRe6|^pO~l2LFp{$+NGNwr}G;?|3a{aG*P$ucJ{oMmV@JHhu2G7 zW6unt^~W$q{di5Sm0-rErZ_^8_W^lFN|Uy>E+C)!ff8a}bk)}D6F)++#!fI>+tb*wE~b531!wpjbEkDgh<$anzjDc*HFaH0)<52J($NOhLI5>~2 zr2Y*MUA=Njyh$57me<8#u~Qoli)b@#oXr{Bj|f}5?(_}!`$g=;F4eza!h8i@3{Qo z(wu2&;T$&;E-q4r)0Ln5D2NO?p_++;b?Osw&%Y)}2wEpbwIDW%Jy11-KR5E4b%8rB>{FPox3W>HP10_d zKBU(lXRp6Hx(Z*_)rDEvAw(AjGFy6`nLyIN`*&v^+OxJ1Scq;368I2!({p{eUd$ z#SONb65*zn2l8>A@pgW#jiw6gQrsQaoRf+kqPpeVb0K+x6xR?$e1zPsN^eR$?~P%` zd=ksZ+4J6obbc>>AH{%n?=wxk+A015{s@w5(mHn3YI${MNS+9vZ67V~b1IfY1j;vTq&f8h)FA_0F zf^ZoiP7BNoyEwDE1m}H>@#2?FuTzLjQv#O4eNbeZcer=h7sfq z+;cJ82hL<8*}>mT?(>+QEeds#Kg_|=_`m`lv35{(rAtB92kV$;jU&cRrfhac9TIx- z44A|Y^!mT+^V|cC$jbbh ztxZM`C)%(O`>{k7d#pyn27uk@O$q3p*NQsOn{wBA`h3Vddxd^D!~JD zdDz_{PWR=Q?7D#GSX+hn%xTjj+Hi)&`=gZu!Ht8@br;^?xFdm+!96vE!Uf57&8H5p zYl6h5z#bwJBtBHixl}9{%aZZ_ZT_J^7%VX%epr>(tnaMP&^oUPR=q8j>A@$^YnTZn z3L={USn=1MO&l0E^$X!j_c}9H#P1BJy+5Z?-;+1D=r$6pZoC#jnub_Hk{5AC(uHflZEU&b9U+xt@xZgXekVpu&Fj?1ZdF+WK2X zH=zn0PSYuIPc+!leDkkS9Uk;Lj3`LZmD5j5Jlv>Vjo-GTya++i%k^XA?$Cj!2>q(@ z-9;e45B7uNf=WMHxVsOh6E}>uj^y*%N|Pou_jb~dR9dd;U0k3jXe9<%*0o}tJ}rr& zFz0AvvmE0V2UH8K1(DoEihA*i;y~?a4r-pDsCi0_c~V~omy{aS`cQE{3uor=>_phX z7ejd5-N^;p{!i=f=}+Lnu;MbpreC|j8a$dQamLZu=iV;)verc=pXltB&MsT<@J%>amkH1N@u6!Tym~C9+R) z@|k*R^^yuwQx>dez1Ws1iL-V`R3`NGkZE6;xqr`mU8v}xO_qjvvg?6xF z=J7nCaIdW{VFcJE0QW(n9ro>y1mi zqQs}~=6BsQFW|22)fasonr`&*GlpwvGEoR8N}W*9U0syxKd9+~tUK8548w_Q76!JP zpk^yWkuyFw$puyU0QcsjLNqu;N|&q0U~%;!Au#nK7pZ(!-^?hyh76_2`H%ij0?gNc zveR)#xIDg$cwC=0HV5WEQk6cg&!D)i(;h)-b2M(fZ+&k^qbc(et=`^M6>5TOMA!PV z{eW$S*-6c_Mlt7{)sO!me|{l3aG`0`q@jg1LJ`%c`Nz5~f|qtY=q1A_{I>lPQ&Apr z+ypE!GSi<@nH>n>;2cNg(nIC`=Ir^G!CpT(y#L)YEE2I=HgJ*227(E!hcWJ)57#Rj zN(w@#53!75U^%dGZ+1ta69JCBs{_rD7I^BNawHXLna_OPiF2}|6=5?Occ__M4S(0C zbKB$0af7a;&0@3wsNEi|+ye??B+!16tifrBq10k7;AQRsEFY5I>zn+n;ian8U2%MfLO`%E`3ijvPih}gT`BTM3a|iRX&E2nuK8XH)>v9`s5YGKZw-#ccTY+ooHF=8})#E{j7G1%4<7)rpOky~azJ$O*&w zKoYtv`Hp!9f0HIhti{`;eH9#UGx29i^A=Y$HO$X-Mrg3l@{_fNEz5WzV&@KFC6AP= zSJ3y|OJ-0PWZ0B6Nn*{ck8*1rMbZT3-pDSaaP`6ZGX4dhP3t7BA7Q9B-th-4R0opt zxYeG&suDv+>K)FMXpTyeB#2Uw&UdYnm}1=cRGYd+oyw7j)likXC|ew!{3ZBf`y$F` zIcTDIi2Of85bateUO$cVT75fmTRevB;)p7Q%1DqYQd~Y3&CgrW9)dh{?k|V!y=$0+ zE-F>fLpGE}WbR{sRHG;Wb`avml_Y$Fzv|6#vv&5J1?=#?+lKy&b`uTB@v+?Gs6rTAxSraKL|#iMI7BfN1**k}x4q{#3J!C_v;-L!qR~zwSc_a_A_xOsAOi z?w%12C-0Ay>9EVF&nFbu04)5O&q@fX*HHqU9;j~QN3YmgBYUY6&9cSR{R#%iI3Ox# z3G~ugRht>tdF8M{789BihUv7xiXQZ}m-X{ovO*M2O3S`%zl((b&WT>&K_-;IGnt)1=-W+ea})|OvXWbMd)p}Uo+YMM?_?dXL3f?VAf8P={JI&!%h!r}L+ z`FBzB-mPHHd=O!hk^bn_c?t8gE#(Ue>)-~y%8qt|GnM}tkz_u@$)r9%;NZe$D~z5L ztFHATD>5gpo#;yS_4MVEsSHnGk{i}gvWr|b=`DiTi*AgLXK*nc`v{m2hJWfmIBM=X zVIAI@YK(EMHE`L6Oj6Hkpv|Hd3K>60M@*b+|EcP=NK8kY2Z3b8Kvmb2_DJw`KD90!hTxOeuS=?AJ*u~3}EVYBs=8E)l-X!4Bs4Ishl23z_15)r)yzkvBpsJZDKo4vF- ziwH&YeHgPYT^+O@ha|Mqx-Z+~U#uKMjc3R)#w8kioIDYY7MxbWR-D@^;2^DPdmQ)` z=Jx(xdr^SaUr^g3*V8BvWqW49I)WPff7pA=s5rZAU9boQLV^T$Nw6dYcZVcc2pZgj zL*ZIzA;BF2Bsc*Agy8OO!QI`0SD}R!{eFA&-e-T==j`40-ag&ucAp;%7=yRgJ8Q1F z)|&FHXXYoHR(x6gSk1BZHeZ>a_GW2Mx4EE)V|5@N{{h940QQP+yFWDTllk8$Y6bDY08f@hr&Io%5Q!};YIlG(=W z>|!z+vKLSey>IYDvg>S?164-Bu(pXC^DN6ZMP0|Tw0EFCxDXoc#{Ce<8!l{NZus}= zCBt6df#g{gP5=_G2;7Mgy0JbGJ_NNuq!3jVULBBYr&W?@nG`@&yEJ>c?sqvIXXSw` zDonNFJ9(P}r#>jiFr`#$VrnBlFGkhTkbDTYmRnsR%h1v((h zR}Ss+m3vN#dGA2_Q)kz`nGMagwI}pynx9tP=@1%Wr$x-GRG;3+{#fdBUi8YxNX!V` zg}$AIO?Nagw_l~!RKM37rmOhGhmXz?gnVo-s_csaRk6GuRRmFbZt>OcPjJZ(ka6vL z*hRommAuFY@aKE(MlLVEn5%8v!`$0tcCA~cvE=jE_B+ECQV1Dz;*WTW@j!uI#m7jB zAEOCm0;YaCeJV9sdHZUu+tV*=VVBn}(RC0v5pGCivABVe^ooL?pEg=QT72rmM9~w! zyc4^%ikkFISG;H*S&VN2a|<}1547qtKC;llh8n1&ZMd%@kr^!Y{Vs0w$IZ(Ad0&|E zNtnKg_YIowoC{tAR|TifOLHx+la0KMKa*wZK08;=i{a-^;?tA;Ef^ zViLpHc^LvK18uk#hczCbw9hrR;5O=f>oUXM$}P}c(2?GvJ;B?=DoR~3!Py``6@2|r z+JT&_u06j`bfPj!OF4{n3pH?v)BD-I;-rE(u%KRZ_P4=pZQXAAF!@}!=fCX5>EJ|z zKul}wbO(Bbdi}Bu?LgF*C64sT5p8`de1ta}>uGg>5<3}% zqSk1ymw7+FeO@|OYcX~+4LG>f7u)p|{Gp{vw10EnUCeHAYaO8Tp*HVNyA`SZhJNbv zf*cxp^DcREYf!Z9$Jip8&S&m|3)N>Y_Xi_z6mvy!Ddv_T@v5WvOXrhCEODw0o@D2n ziLc~PNgrS&wHXI9p~mVS3KOA$D27aFmkkPXZ9AOULiRgHk;zsbF?b&{{Akg)I%t&nUo~0Gvs*k8$`{E-0h0 z?cJyer!!$mK}oWKMw7;41dn>r6c`J&C%2H=4Z8Oqn73v z!sw}pR1I-^RmepC1VsK1@Ns|l3=P}vKESww_!Qj5>tRhZr*M=rOinye4O9lISJq>r zlcrPEi|f|9n#WJW^#qSpVDobbcc$YH)6H$xpH-Az5SoAaYz(4|(MT`SUl$7yW^-|2 zn4Ate4J~GHJeFtp`0lWCqm02(!E>_V07H7>aJ_z_vXG)7Wa6v5gLRzX^C5A16C6eA z(?h!LMT0K=r%kZnAfE6e{GYVQ;n?CIKV0%FulvYMW8bu}Gm5Jb8s3vt@8$&i40mcL zX05ZlWufbI^YBiAyn4RBHDxO!qx<>QAgg0muW&8K5k8lG37h{7i{Ptx1Xw!OP?43v z^Fc_YqmI=kgv^Yes!wEhamX)wNq5-6EoWv}q*MRRSig0|SWk>A2OWbWO53`hWGjq{ zqh+l4R`9Hl>CISFDUb0t ztuz^|N5d6)spuS$h{f@!CO{I@6|u*V;&mmhB=!3Gs+4!Nac8z(ukX(nKKPO-vx+8* zLt{ruZyw)8?wY$MM}v-_=(1UB(%%9vYV14CDcWljhY!Vk2-aZ2NTgr!<*3wX&_XD7 z)O!u{1o`F8^*`mdh0S(H{PZZGb;Na0fwl{L^$wcrzot>VX2?mmX{0e#WF_eniggGS zpx^LFW<|#cF!fg?f|PxLt94iJK$=ZI<^Qa=yu1Us=D?(tkKxB28`H*WMU{KZLgAtN z`v)t@I{^!_gl~(uZ~{Zfj1R2_{|&?sq&Vzc^J|nO1$&7Uw6hJGEviSzG71MuuA>-%k385`$4;M zE|$iixO;VEl<^}M+=Fr*bF3BIPGEA8z?(8GsSEW2X4*YpP0IJEbh2=%NYfSYqduDb z*sk0*mvdeyCuGg^VaBt|6sAoK%H&o!YwV9*YBIQDNQvP!)&AAX#6(v--it82x_M6= ztjLV|Mpn@Ma(Oq6WY(no$~>iU>%)lAmz-Z#G*0NSYoGLPzIH@e7oH%u3*yG6{Dr= zlTGr9!bF09`)I{$*}eWML%Ya)qKG zGX>cm*&YD5dTr$8BZ$K{1smS1nwq^tI)1O9N$RKEINZ_8Az{~JuBwC&(78qq*iwKC zI-d8u!bNlat}aS3a@w%nH0}(b)ON*R58GMAHr#WRFywTUva(^43y>OD`8iS-skcxO zqxEH>fgr+BEA++!^$T!-9r!RIbmw)tFu(9%nsQ_saQuAph*vNbrSCKkEM-R_=a6X$ z4>>LL{3QPtskrv+=hg#_1{2T_L^pcjk(Sv5VX?K_=cjUuyn|J8GRE$Cf$FnAbzX2s6*2Or z%4i0u2&Tp{&lr|hRi|4P6=j9#_ztAq>J8QNqAoZ(xyi z_PiB1W!?cOJS-Jh=!3wHTlol5Vi-JZ3#e zY!@c2_c7|rj|up@tKa}HqCEY*xu)hUm-_=J2GDkKZp2Ubfy{p9>?ArfP`7l8ooD4} zWk0sS={1$$*j!%+Ms?7A8Gw%N1v-xB)dcox(?;2ux0l3Vapq+sJHQ0P#QU9=q0x8@kYl(u=2mX8Z!HCrfC4OC%|sD

ak)W!hUsT}OV40M{TA$&NbE6T?y^T2yq+Qo z!?)xTl@SM$*OzlnJ#U9$8XZo#)g{oi(L5`X`c|3EO8@0se4yqNMXyeouaqWDq3RS7 zgTp7+1jh+jx4SQQ2g1ZDD2()!wJd&lxV1>;fWG;l3FC>o7zb^f!n*G84Z~a}OjrM< zB9#^HTJ;1k7489D-+XQHas|pbsld*B3D2p)Icw5+xWFB#!un>9@c4lKn8c-iokD4S zlXM-GQdMrX37g06Ky|e8Y{HG?+OSH3ca2@)#+-k>hy=9@D7<37mPM4BP2IfC*Rve< z#FqeP?ZrN?77(jT9KW9tgG!B#t0IG%7T-id}!~o0U`S&}J6=P@~YgS|5d6^|?lEdyiJoS1jyyVd@lDVYwz{B8g z3;LTn2b^rJrwL}guf%!P^~PHnwSeU|H^+RqEUomnX2y$7jrBD*IY z$&6VZc5J8KqPxGkjkpG_qBbAA&~PAYLyPbrkXa%hNwc0j)e}f;-5Q~IBWAuqvj{&v zMp>tNR!bweo21c9kASY|fjh}JuWBdnN7T?*%m6%rot?3rS&$Q4)4smGLL@JowSI{+ z<@Gqv-W_QFR!rX4@`8Eofwky(J^fR61|OzAfmS-Dia8M;(PUOp9^$z15N5mN> ziu8QC``uheVK&==jbiz)?TC?I><) zG{&W~5+{86oRo5Xt6PUTpjTLqV^I{7$ulZpVX~FcUwuu_t5fttQV-DOK4K z;8!(a4>Cuc5!p2<2amsMXW{{tDnf|T;V|JO**g9;>^asSJd9zxV@v0YnN2leYs9pl zH^p~lXrpi`ALCAFFt1QRY;Wki?OgEX(dN>>#z{U6(s=Lm?1jXNKYy3Y8BtYHQ>v7Z z%u_$I*`J+$5@z%jhHmt6ijEejpI~0q=Uk9(Eg%OA_O_C8bHt;Qtn!Uh6bI1+zRuOx zY+GBV(6z$v+2D4a7Mcy3sw8f;#i+N%KN*kLvmCd6$ETFS>_oI<>>R`}FJSsmF2ZY8 z31wIbl+SvKO?_LWeJxv72Npw|bLr517W5v7PRiW*^$c~e@wxf;vN4+X}NAe<@?EUr(F%p>1j$tS+ zy+bRRR2_b)8q(h_w56(_l20X==09-S3~z8wGIL0^#UN|FDH!`TXR=PPQymvzYomZ5 zL~g8e9E+~DUV7L+_Z!GfFIs!_4Nec^&DhwPer&034fXk^mp2#_MHCXro_c^0pN`JZ zAmOxv8_auU+rc-t|5oawt-%Yf?G2B79E#;4*K=xo*j5rG$-~SqcXr{MtGt~lrEMK4 zzaa`G{)@rt&|EF6_q)U%rfE$F-EXEjbiVenDkyBBGR5GEenJ`A0vJe>y(b%uuu

lb{GB3Kr`o%Uzz#^YBTloy5U!0}i;nW9=VT8XGa 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
c&!z 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 K$SnJFaOX|V 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%0A(Fwq3cX*?&ne_cMpE&tiiS>{;*8N-~y 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