From 26506e80eea06b68438bbb0bc1e08e269da5f1d3 Mon Sep 17 00:00:00 2001 From: gruberth Date: Tue, 9 Apr 2024 22:06:05 +0200 Subject: [PATCH 001/121] husky2: fixed loop caused by caller name check --- husky2/__init__.py | 44 ++++++++++++++++++++++---------------------- husky2/plugin.yaml | 2 +- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/husky2/__init__.py b/husky2/__init__.py index 5b5330b69..007273101 100755 --- a/husky2/__init__.py +++ b/husky2/__init__.py @@ -71,7 +71,7 @@ class properties and methods (class variables and class functions) are already available! """ - PLUGIN_VERSION = '2.1.1' + PLUGIN_VERSION = '2.1.2' ITEM_INFO = "husky_info" ITEM_CONTROL = "husky_control" @@ -440,7 +440,7 @@ def update_item(self, item, caller=None, source=None, dest=None): :param dest: if given it represents the dest """ - if self.alive and caller != self.get_shortname(): + if self.alive and not self.get_fullname() in caller: # code to execute if the plugin is not stopped # and only, if the item has not been changed by this this plugin: item_value = "{0}".format(item()) @@ -459,7 +459,7 @@ def update_item(self, item, caller=None, source=None, dest=None): # if control item is a bool type reset it to False after sending cmd if item.property.type == 'bool': - item(0, self.get_shortname()) + item(0, self.get_fullname()) def sendCmd(self, cmd, value=-1): self.writeToStatusItem(self.translate("Sending command") + ": " + cmd) @@ -469,7 +469,7 @@ def writeToStatusItem(self, txt): self.message = txt if 'message' in self._items_state: for item in self._items_state['message']: - item(txt, self.get_shortname()) + item(txt, self.get_fullname()) def poll_device(self): self.logger.debug("Poll new status") @@ -517,19 +517,19 @@ def data_callback(self, status): data['attributes']['positions'][i]['longitude']]) if 'gpspoints' in self._items_state: for item in self._items_state['gpspoints']: - item(self.mowerGpspoints.get_list()[::-1], self.get_shortname()) + item(self.mowerGpspoints.get_list()[::-1], self.get_fullname()) self.mowerLongitude.push(data['attributes']['positions'][0]['longitude']) if 'longitude' in self._items_state: for item in self._items_state['longitude']: - item(self.mowerLongitude.get_last(), self.get_shortname()) + item(self.mowerLongitude.get_last(), self.get_fullname()) self.mowerLatitude.push(data['attributes']['positions'][0]['latitude']) if 'latitude' in self._items_state: for item in self._items_state['latitude']: - item(self.mowerLatitude.get_last(), self.get_shortname()) + item(self.mowerLatitude.get_last(), self.get_fullname()) self.mowerBatterypercent.push(data['attributes']['battery']['batteryPercent']) if 'batterypercent' in self._items_state: for item in self._items_state['batterypercent']: - item(self.mowerBatterypercent.get_last(), self.get_shortname()) + item(self.mowerBatterypercent.get_last(), self.get_fullname()) errorcode = data['attributes']['mower']['errorCode'] if errorcode in self.MOWERERROR: self.mowerErrormsg.push(self.translate(self.MOWERERROR[errorcode]['msg'])) @@ -537,39 +537,39 @@ def data_callback(self, status): self.mowerErrormsg.push(self.translate('Unknown error code') + ": " + str(errorcode)) if 'errormessage' in self._items_state: for item in self._items_state['errormessage']: - item(self.mowerErrormsg.get_last(), self.get_shortname()) + item(self.mowerErrormsg.get_last(), self.get_fullname()) self.mowerActivity.push(data['attributes']['mower']['activity']) if 'activity' in self._items_state: for item in self._items_state['activity']: - item(self.translate(self.mowerActivity.get_last()), self.get_shortname()) + item(self.translate(self.mowerActivity.get_last()), self.get_fullname()) if 'state' in self._items_state: for item in self._items_state['state']: - item(self.translate(data['attributes']['mower']['state']), self.get_shortname()) + item(self.translate(data['attributes']['mower']['state']), self.get_fullname()) if 'mode' in self._items_state: for item in self._items_state['mode']: - item(self.translate(data['attributes']['mower']['mode']), self.get_shortname()) + item(self.translate(data['attributes']['mower']['mode']), self.get_fullname()) self.mowerConnection.push(data['attributes']['metadata']['connected']) if 'connection' in self._items_state: for item in self._items_state['connection']: - item(self.mowerConnection.get_last(), self.get_shortname()) + item(self.mowerConnection.get_last(), self.get_fullname()) # settings if 'cuttingHeight' in data['attributes']: if 'cuttingheight' in self._items_control: for item in self._items_control['cuttingheight']: - item(data['attributes']['cuttingHeight'], self.get_shortname()) + item(data['attributes']['cuttingHeight'], self.get_fullname()) else: if 'cuttingheight' in self._items_control: for item in self._items_control['cuttingheight']: - item(data['attributes']['settings']['cuttingHeight'], self.get_shortname()) + item(data['attributes']['settings']['cuttingHeight'], self.get_fullname()) if 'headlight' in data['attributes']: if 'headlight' in self._items_control: for item in self._items_control['headlight']: - item(data['attributes']['headlight']['mode'], self.get_shortname()) + item(data['attributes']['headlight']['mode'], self.get_fullname()) else: if 'headlight' in self._items_control: for item in self._items_control['headlight']: - item(data['attributes']['settings']['headlight']['mode'], self.get_shortname()) + item(data['attributes']['settings']['headlight']['mode'], self.get_fullname()) def token_callback(self, token): """ @@ -622,16 +622,16 @@ async def initWorker(self): if 'name' in self._items_info: for item in self._items_info['name']: - item(self.mowerName, self.get_shortname()) + item(self.mowerName, self.get_fullname()) if 'id' in self._items_info: for item in self._items_info['id']: - item(self.mowerId, self.get_shortname()) + item(self.mowerId, self.get_fullname()) if 'serial' in self._items_info: for item in self._items_info['serial']: - item(self.mowerSerial, self.get_shortname()) + item(self.mowerSerial, self.get_fullname()) if 'model' in self._items_info: for item in self._items_info['model']: - item(self.mowerModel, self.get_shortname()) + item(self.mowerModel, self.get_fullname()) self.data_callback(status_all) return @@ -801,7 +801,7 @@ def init_webinterface(self): # Register the web interface as a cherrypy app self.mod_http.register_webif(WebInterface(webif_dir, self), - self.get_shortname(), + self.get_fullname(), config, self.get_classname(), self.get_instance_name(), description='') diff --git a/husky2/plugin.yaml b/husky2/plugin.yaml index bed222c30..4ac0a1390 100755 --- a/husky2/plugin.yaml +++ b/husky2/plugin.yaml @@ -12,7 +12,7 @@ plugin: documentation: https://smarthomeng.github.io/smarthome/plugins/husky2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1764058-support-thread - version: 2.1.1 # Plugin version (must match the version specified in __init__.py) + version: 2.1.2 # Plugin version (must match the version specified in __init__.py) sh_minversion: 1.8 # minimum shNG version to use this plugin multi_instance: false # plugin supports multi instance restartable: unknown From ea4ef26190dae5010f61ed34c9f3ebb7423b6eb7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 29 May 2024 11:12:09 +0200 Subject: [PATCH 002/121] denon plugin: fix initial maxvolume check (it's not available) --- denon/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denon/commands.py b/denon/commands.py index 27afbd0fa..ac400af84 100755 --- a/denon/commands.py +++ b/denon/commands.py @@ -106,7 +106,7 @@ 'volume': {'read': True, 'write': True, 'read_cmd': 'MV?', 'write_cmd': 'MV{VALUE}', 'item_type': 'num', 'dev_datatype': 'DenonVol', 'reply_pattern': r'MV(\d{2,3})', 'cmd_settings': {'force_min': 0.0, 'valid_max': 98.0}, 'item_attrs': {'initial': True}}, 'volumeup': {'read': False, 'write': True, 'item_type': 'bool', 'write_cmd': 'MVUP', 'dev_datatype': 'raw'}, 'volumedown': {'read': False, 'write': True, 'write_cmd': 'MVDOWN', 'item_type': 'bool', 'dev_datatype': 'raw'}, - 'volumemax': {'opcode': '{VALUE}', 'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'MVMAX (\d{2,3})', 'item_attrs': {'initial': True}}, + 'volumemax': {'opcode': '{VALUE}', 'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'MVMAX (\d{2,3})'}, 'input': {'read': True, 'write': True, 'read_cmd': 'SI?', 'write_cmd': 'SI{VALUE}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': 'SI{LOOKUP}', 'lookup': 'INPUT', 'item_attrs': {'item_template': 'input', 'initial': True}}, 'listeningmode': {'read': True, 'write': True, 'cmd_settings': {'valid_list_ci': ['MOVIE', 'MUSIC', 'GAME', 'DIRECT', 'PURE DIRECT', 'STEREO', 'AUTO', 'DOLBY DIGITAL', 'DOLBY SURROUND', 'DTS SURROUND', 'NEURAL:X', 'AURO3D', 'AURO2DSURR', 'MCH STEREO', 'ROCK ARENA', 'JAZZ CLUB', 'MONO MOVIE', 'MATRIX', 'VIDEO GAME', 'VIRTUAL', 'LEFT', 'RIGHT']}, 'read_cmd': 'MS?', 'write_cmd': 'MS{RAW_VALUE_UPPER}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'\s?MS(.*)', 'item_attrs': {'initial': True}}, 'sleep': {'read': True, 'write': True, 'item_type': 'num', 'read_cmd': 'SLP?', 'write_cmd': 'SLP{VALUE}', 'dev_datatype': 'convert0', 'reply_pattern': r'SLP(\d{3}|OFF)', 'cmd_settings': {'force_min': 0, 'force_max': 120}, 'item_attrs': {'initial': True}}, From f8a4763faef44a465505b1e0d9d1ad1be27b1703 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 29 May 2024 11:54:04 +0200 Subject: [PATCH 003/121] denon plugin: add readafterwrite parameter --- denon/plugin.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 3ab797598..329f27207 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -226,6 +226,13 @@ item_attributes: de: Konfiguriert ein Intervall in Sekunden für regelmäßiges Lesen en: Configures a interval in seconds for cyclic read actions + denon_readafterwrite: + type: num + + description: + de: Konfiguriert eine Verzögerung, nach der der Lesebefehl im Anschluss an einen Schreibbefehl gesendet werden soll. + en: Configures a delay after that the read command related to a send command will get sent. + denon_read_initial: type: bool From 00daeb072882fcb1142a115d0809afebdabe66cc Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 29 May 2024 11:54:27 +0200 Subject: [PATCH 004/121] denon plugin: add resend options on plugin.yaml --- denon/plugin.yaml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 329f27207..444b8552a 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -163,6 +163,45 @@ parameters: de: Anzahl Verbindungsversuche en: number of connect retries + send_retries: + type: num + default: 0 + + description: + de: Anzahl Sendeversuche + en: number of sending retries + description_long: + de: 'Anzahl Sendeversuche\n + Kommt keine (passende) Antwort nach dem Senden + eines Commands zurück, wird das Kommando nochmals + gesendet, sofern der Wert über 0 liegt. + ' + en: 'number of sending retries\n + If no (suiting) answer is received after sending + a command the command is resent as long as this + value is more than 0. + ' + + sendretry_cycle: + type: num + valid_min: 1 + default: 1 + + description: + de: Pause zwischen Durchgängen von Sendeversuchen + en: wait time between sending retry rounds + description_long: + de: 'Pause zwischen Durchgängen von Sendeversuchen\n + Sind Send Retries aktiv, wird ein Scheduler erstellt, + der im angegebenen Sekundentakt Kommandos erneut sendet, + zu denen keine (passenden) Antworten erhalten wurden. + ' + en: 'wait time between sending retry rounds\n + If send retries are active, a scheduler gets added + that resends commands in the given cycle value (in seconds) + where no (suiting) answer got received. + ' + connect_cycle: type: num default: 3 From 24b321e28a18e57ddd1a3588267e134a2ee4b165 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 29 May 2024 11:56:22 +0200 Subject: [PATCH 005/121] denon plugin: query most relevant commands after powering on a zone --- denon/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/denon/__init__.py b/denon/__init__.py index 56b0f7818..db66fb6ab 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -89,6 +89,25 @@ 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): + zone = 0 + if command == 'zone1.control.power': + zone = 1 + elif command == 'zone2.control.power': + zone = 2 + elif command == 'zone3.control.power': + zone = 3 + if zone > 0 and value is True: + self.logger.debug(f"Device is turned on by command {command}. Requesting current state of zone {zone}.") + time.sleep(1) + self.send_command(f'zone{zone}.control.mute') + self.send_command(f'zone{zone}.control.sleep') + self.send_command(f'zone{zone}.control.standby') + if zone == 1 and value is True: + self.send_command(f'zone{zone}.control.input') + self.send_command(f'zone{zone}.control.volume') + self.send_command(f'zone{zone}.control.listeningmode') + def on_data_received(self, by, data, command=None): commands = None From a66d84db3d35486633589ab8e8a5cefab76b323a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 29 May 2024 11:57:10 +0200 Subject: [PATCH 006/121] denon plugin: add resend function --- denon/__init__.py | 48 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index db66fb6ab..041edf1f2 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -24,6 +24,8 @@ import builtins import os import sys +import threading +import time if __name__ == '__main__': builtins.SDP_standalone = True @@ -56,12 +58,20 @@ class denon(SmartDevicePlugin): PLUGIN_VERSION = '1.0.1' def on_connect(self, by=None): + self.logger.debug(f"Connected.. Cycle for retry: {self._sendretry_cycle}") + if self._send_retries >= 1: + self.scheduler_add('resend', self._resend, cycle=self._sendretry_cycle) self.logger.debug("Checking for custom input names.") self.send_command('general.custom_inputnames') + self._read_initial_values(force=True) def _set_device_defaults(self): - + self._use_callbacks = True self._custom_inputnames = {} + self._sending = {} + self._sending_lock = threading.Lock() + self._send_retries = self.get_parameter_value('send_retries') + self._sendretry_cycle = int(self.get_parameter_value('sendretry_cycle')) # set our own preferences concerning connections if PLUGIN_ATTR_NET_HOST in self._parameters and self._parameters[PLUGIN_ATTR_NET_HOST]: @@ -81,8 +91,31 @@ def _set_device_defaults(self): # command and discard it... so break the "return result"-chain and don't # return anything def _send(self, data_dict): + if data_dict.get('returnvalue') is not None: + self._sending.update({data_dict['command']: data_dict}) self._connection.send(data_dict) + def _resend(self): + if not self.alive or self.suspended: + return + self._sending_lock.acquire(True, 2) + _remove_commands = [] + for command in list(self._sending.keys()): + _retry = self._sending[command].get("retry") or 0 + _sent = True + if _retry is not None and _retry < self._send_retries: + self.logger.debug(f'Re-sending {command}, retry {_retry}.') + _sent = self.send_command(command, self._sending[command].get("returnvalue"), return_result=True, retry=_retry + 1) + elif _retry is not None and _retry >= self._send_retries: + _sent = False + if _sent is False: + _remove_commands.append(command) + self.logger.info(f"Giving up re-sending {command} after {_retry} retries.") + for command in _remove_commands: + self._sending.pop(command) + if self._sending_lock.locked(): + self._sending_lock.release() + def _transform_send_data(self, data=None, **kwargs): if isinstance(data, dict): data['limit_response'] = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] @@ -151,7 +184,18 @@ def on_data_received(self, by, data, command=None): except OSError as e: self.logger.warning(f'received data "{data}" for command {command}, error {e} occurred while converting. Discarding data.') else: - self.logger.debug(f'received data "{data}" for command {command} converted to value {value}') + self.logger.debug(f'received data "{data}" for command {command} converted to value {value}.') + if command in self._sending: + self._sending_lock.acquire(True, 2) + _retry = self._sending[command].get("retry") or 0 + _compare = self._sending[command].get('returnvalue') + if self._sending[command].get('returntype')(value) == _compare: + self._sending.pop(command) + self.logger.debug(f'Correct answer for {command}, removing from send. Sending {self._sending}') + elif _retry is not None and _retry <= self._send_retries: + self.logger.debug(f'Should send again {self._sending}...') + if self._sending_lock.locked(): + self._sending_lock.release() self._dispatch_callback(command, value, by) self._process_additional_data(base_command, data, value, custom, by) From c3a44948dd8ca98d28331a0f1d20cc628cbd4f99 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 31 May 2024 20:35:12 +0200 Subject: [PATCH 007/121] denon plugin: fixes, updated plugin.yaml --- denon/__init__.py | 2 +- denon/commands.py | 2 +- denon/plugin.yaml | 5900 ++++++++++++++++++++++----------------------- 3 files changed, 2948 insertions(+), 2956 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index 041edf1f2..c722cdc34 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -208,4 +208,4 @@ def _check_for_custominputs(self, command, data): self._custom_inputnames[src] = name if __name__ == '__main__': - s = Standalone(lms, sys.argv[0]) + s = Standalone(denon, sys.argv[0]) diff --git a/denon/commands.py b/denon/commands.py index ac400af84..d256af4ac 100755 --- a/denon/commands.py +++ b/denon/commands.py @@ -424,7 +424,7 @@ 'on_change': [".custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value]",], 'custom_name': { 'type': 'str', - 'on_change': ".. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value]" + 'on_change': "sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None" } } } diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 444b8552a..1eabf58fe 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -234,7 +234,6 @@ parameters: de: Item-Pfad für das Standby-Item en: item path for standby switch item - item_attributes: denon_command: @@ -311,224 +310,224 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: info + denon_read_group_trigger@instance: info fullmodel: type: str - denon_command: info.fullmodel - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.fullmodel + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info - denon_read_initial: true + denon_read_initial@instance: true model: type: str - denon_command: info.model - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.model + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info - denon_read_initial: true + denon_read_initial@instance: true serialnumber: type: num - denon_command: info.serialnumber - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.serialnumber + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info main: type: str - denon_command: info.main - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.main + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info mainfbl: type: num - denon_command: info.mainfbl - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.mainfbl + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info dsp1: type: num - denon_command: info.dsp1 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp1 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info dsp2: type: num - denon_command: info.dsp2 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp2 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info dsp3: type: num - denon_command: info.dsp3 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp3 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info dsp4: type: num - denon_command: info.dsp4 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp4 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info apld: type: num - denon_command: info.apld - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.apld + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info vpld: type: num - denon_command: info.vpld - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.vpld + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info guidat: type: num - denon_command: info.guidat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.guidat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info heosversion: type: str - denon_command: info.heosversion - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosversion + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info heosbuild: type: num - denon_command: info.heosbuild - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosbuild + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info heosmod: type: num - denon_command: info.heosmod - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosmod + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info heoscnf: type: str - denon_command: info.heoscnf - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heoscnf + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info heoslanguage: type: str - denon_command: info.heoslanguage - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heoslanguage + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info mac: type: str - denon_command: info.mac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.mac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info wifimac: type: str - denon_command: info.wifimac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.wifimac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info btmac: type: str - denon_command: info.btmac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.btmac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info audyif: type: num - denon_command: info.audyif - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.audyif + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info productid: type: num - denon_command: info.productid - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.productid + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info packageid: type: num - denon_command: info.packageid - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.packageid + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info cmp: type: str - denon_command: info.cmp - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.cmp + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info region: type: str - denon_command: info.region - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.region + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - info - denon_read_initial: true + denon_read_initial@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: general + denon_read_group_trigger@instance: general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general cache: true @@ -543,94 +542,94 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - general display: type: str - denon_command: general.display - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.display + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true allzonestereo: type: bool - denon_command: general.allzonestereo - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.allzonestereo + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - general tuner: @@ -638,86 +637,86 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: tuner + denon_read_group_trigger@instance: tuner title: type: str - denon_command: tuner.title - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: tuner.title + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - tuner - denon_read_initial: true + denon_read_initial@instance: true album: type: str - denon_command: tuner.album - denon_read: true - denon_write: false + denon_command@instance: tuner.album + denon_read@instance: true + denon_write@instance: false artist: type: str - denon_command: tuner.artist - denon_read: true - denon_write: false + denon_command@instance: tuner.artist + denon_read@instance: true + denon_write@instance: false preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner hd: @@ -725,215 +724,214 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: tuner.hd + denon_read_group_trigger@instance: tuner.hd channel: type: num - denon_command: tuner.hd.channel - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.channel + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - tuner.hd - denon_read_initial: true + denon_read_initial@instance: true channelup: type: bool - denon_command: tuner.hd.channelup - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.channelup + denon_read@instance: false + denon_write@instance: true channeldown: type: bool - denon_command: tuner.hd.channeldown - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.channeldown + denon_read@instance: false + denon_write@instance: true multicastchannel: type: num - denon_command: tuner.hd.multicastchannel - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.multicastchannel + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - tuner.hd presetmemory: type: num - denon_command: tuner.hd.presetmemory - denon_read: true - denon_write: true + denon_command@instance: tuner.hd.presetmemory + denon_read@instance: true + denon_write@instance: true preset: type: num - denon_command: tuner.hd.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - tuner.hd presetup: type: bool - denon_command: tuner.hd.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.hd.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.presetdown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.hd.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - tuner - tuner.hd - denon_read_initial: true + denon_read_initial@instance: true zone1: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1 + denon_read_group_trigger@instance: zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.control + denon_read_group_trigger@instance: zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings + denon_read_group_trigger@instance: zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings.sound + denon_read_group_trigger@instance: zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings.sound.channel_level + denon_read_group_trigger@instance: zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -941,10 +939,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -952,10 +950,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -963,10 +961,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -974,10 +972,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -985,10 +983,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -996,10 +994,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1007,10 +1005,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1018,10 +1016,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1029,10 +1027,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1040,10 +1038,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1051,10 +1049,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1062,10 +1060,10 @@ item_structs: subwoofer2: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer2 - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer2 + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1076,14 +1074,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings.sound.tone_control + denon_read_group_trigger@instance: zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1091,10 +1089,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1102,22 +1100,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1125,29 +1123,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings.sound.general + denon_read_group_trigger@instance: zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1155,10 +1153,10 @@ item_structs: dynamic_eq: type: bool - denon_command: zone1.settings.sound.general.dynamic_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamic_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1166,10 +1164,10 @@ item_structs: multeq: type: bool - denon_command: zone1.settings.sound.general.multeq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.multeq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1177,10 +1175,10 @@ item_structs: dynamic_vol: type: bool - denon_command: zone1.settings.sound.general.dynamic_vol - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamic_vol + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1188,10 +1186,10 @@ item_structs: speakersetup: type: str - denon_command: zone1.settings.sound.general.speakersetup - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.speakersetup + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1199,10 +1197,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1210,10 +1208,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1221,10 +1219,10 @@ item_structs: dialogtoggle: type: bool - denon_command: zone1.settings.sound.general.dialogtoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogtoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1232,10 +1230,10 @@ item_structs: dialog: type: num - denon_command: zone1.settings.sound.general.dialog - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialog + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1243,22 +1241,22 @@ item_structs: dialogup: type: bool - denon_command: zone1.settings.sound.general.dialogup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogup + denon_read@instance: false + denon_write@instance: true dialogdown: type: bool - denon_command: zone1.settings.sound.general.dialogdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogdown + denon_read@instance: false + denon_write@instance: true dialogenhance: type: num - denon_command: zone1.settings.sound.general.dialogenhance - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogenhance + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1266,10 +1264,10 @@ item_structs: subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1277,10 +1275,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1288,22 +1286,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1311,22 +1309,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true digitalinput: type: str - denon_command: zone1.settings.sound.general.digitalinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.digitalinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1334,10 +1332,10 @@ item_structs: audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -1348,74 +1346,74 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone1.settings.video + denon_read_group_trigger@instance: zone1.settings.video aspectratio: type: str - denon_command: zone1.settings.video.aspectratio - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.aspectratio + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video hdmimonitor: type: num - denon_command: zone1.settings.video.hdmimonitor - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmimonitor + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video hdmiresolution: type: str - denon_command: zone1.settings.video.hdmiresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmiresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video videoprocessingmode: type: str - denon_command: zone1.settings.video.videoprocessingmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoprocessingmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video videoresolution: type: str - denon_command: zone1.settings.video.videoresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video pictureenhancer: type: num - denon_command: zone1.settings.video.pictureenhancer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.pictureenhancer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video videoinput: type: str - denon_command: zone1.settings.video.videoinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone1 - zone1.settings - zone1.settings.video @@ -1425,60 +1423,60 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2 + denon_read_group_trigger@instance: zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.control + denon_read_group_trigger@instance: zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control on_change: @@ -1486,23 +1484,23 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.control @@ -1511,28 +1509,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.settings + denon_read_group_trigger@instance: zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.settings.sound + denon_read_group_trigger@instance: zone2.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.settings.sound.channel_level + denon_read_group_trigger@instance: zone2.settings.sound.channel_level front_left: type: num - denon_command: zone2.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1540,10 +1538,10 @@ item_structs: front_right: type: num - denon_command: zone2.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1554,14 +1552,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.settings.sound.tone_control + denon_read_group_trigger@instance: zone2.settings.sound.tone_control treble: type: num - denon_command: zone2.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1569,22 +1567,22 @@ item_structs: trebleup: type: bool - denon_command: zone2.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone2.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone2.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1592,40 +1590,40 @@ item_structs: bassup: type: bool - denon_command: zone2.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone2.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: zone2.settings.sound.general + denon_read_group_trigger@instance: zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound - zone2.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone2.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1636,78 +1634,78 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3 + denon_read_group_trigger@instance: zone3 control: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.control + denon_read_group_trigger@instance: zone3.control power: type: bool - denon_command: zone3.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control mute: type: bool - denon_command: zone3.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control volume: type: num - denon_command: zone3.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control volumeup: type: bool - denon_command: zone3.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone3.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumedown + denon_read@instance: false + denon_write@instance: true sleep: type: num - denon_command: zone3.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control standby: type: num - denon_command: zone3.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control input: type: str - denon_command: zone3.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.control on_change: @@ -1715,35 +1713,35 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None settings: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.settings + denon_read_group_trigger@instance: zone3.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.settings.sound + denon_read_group_trigger@instance: zone3.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.settings.sound.channel_level + denon_read_group_trigger@instance: zone3.settings.sound.channel_level front_left: type: num - denon_command: zone3.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1751,10 +1749,10 @@ item_structs: front_right: type: num - denon_command: zone3.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1765,14 +1763,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.settings.sound.tone_control + denon_read_group_trigger@instance: zone3.settings.sound.tone_control treble: type: num - denon_command: zone3.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1780,22 +1778,22 @@ item_structs: trebleup: type: bool - denon_command: zone3.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone3.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone3.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1803,29 +1801,29 @@ item_structs: bassup: type: bool - denon_command: zone3.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone3.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: zone3.settings.sound.general + denon_read_group_trigger@instance: zone3.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone3.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1836,21 +1834,21 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL + denon_read_group_trigger@instance: ALL general: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.general + denon_read_group_trigger@instance: ALL.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general cache: true @@ -1866,84 +1864,84 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.general @@ -1952,68 +1950,68 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.tuner + denon_read_group_trigger@instance: ALL.tuner preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.tuner @@ -2022,148 +2020,147 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1 + denon_read_group_trigger@instance: ALL.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.control + denon_read_group_trigger@instance: ALL.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.settings + denon_read_group_trigger@instance: ALL.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.settings.sound + denon_read_group_trigger@instance: ALL.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: ALL.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2172,10 +2169,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2184,10 +2181,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2196,10 +2193,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2208,10 +2205,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2220,10 +2217,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2232,10 +2229,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2244,10 +2241,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2256,10 +2253,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2268,10 +2265,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2280,10 +2277,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2292,10 +2289,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2307,14 +2304,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: ALL.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2323,10 +2320,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2335,22 +2332,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2359,29 +2356,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone1.settings.sound.general + denon_read_group_trigger@instance: ALL.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2390,10 +2387,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2402,10 +2399,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2414,10 +2411,10 @@ item_structs: subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2426,10 +2423,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2438,22 +2435,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2462,22 +2459,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2489,63 +2486,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone2 + denon_read_group_trigger@instance: ALL.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone2.control + denon_read_group_trigger@instance: ALL.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control @@ -2554,24 +2551,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control @@ -2581,28 +2578,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone2.settings + denon_read_group_trigger@instance: ALL.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone2.settings.sound + denon_read_group_trigger@instance: ALL.zone2.settings.sound general: read: type: bool enforce_updates: true - denon_read_group_trigger: ALL.zone2.settings.sound.general + denon_read_group_trigger@instance: ALL.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.settings @@ -2614,256 +2611,256 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H + denon_read_group_trigger@instance: AVR-X6300H info: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.info + denon_read_group_trigger@instance: AVR-X6300H.info fullmodel: type: str - denon_command: info.fullmodel - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.fullmodel + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info - denon_read_initial: true + denon_read_initial@instance: true model: type: str - denon_command: info.model - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.model + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info - denon_read_initial: true + denon_read_initial@instance: true serialnumber: type: num - denon_command: info.serialnumber - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.serialnumber + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info main: type: str - denon_command: info.main - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.main + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info mainfbl: type: num - denon_command: info.mainfbl - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.mainfbl + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info dsp1: type: num - denon_command: info.dsp1 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp1 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info dsp2: type: num - denon_command: info.dsp2 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp2 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info dsp3: type: num - denon_command: info.dsp3 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp3 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info dsp4: type: num - denon_command: info.dsp4 - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.dsp4 + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info apld: type: num - denon_command: info.apld - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.apld + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info vpld: type: num - denon_command: info.vpld - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.vpld + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info guidat: type: num - denon_command: info.guidat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.guidat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info heosversion: type: str - denon_command: info.heosversion - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosversion + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info heosbuild: type: num - denon_command: info.heosbuild - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosbuild + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info heosmod: type: num - denon_command: info.heosmod - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heosmod + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info heoscnf: type: str - denon_command: info.heoscnf - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heoscnf + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info heoslanguage: type: str - denon_command: info.heoslanguage - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.heoslanguage + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info mac: type: str - denon_command: info.mac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.mac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info wifimac: type: str - denon_command: info.wifimac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.wifimac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info btmac: type: str - denon_command: info.btmac - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.btmac + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info audyif: type: num - denon_command: info.audyif - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.audyif + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info productid: type: num - denon_command: info.productid - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.productid + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info packageid: type: num - denon_command: info.packageid - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.packageid + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info cmp: type: str - denon_command: info.cmp - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.cmp + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info region: type: str - denon_command: info.region - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: info.region + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.info - denon_read_initial: true + denon_read_initial@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.general + denon_read_group_trigger@instance: AVR-X6300H.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general cache: true @@ -2879,84 +2876,84 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general @@ -2965,68 +2962,68 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.tuner + denon_read_group_trigger@instance: AVR-X6300H.tuner preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner @@ -3035,227 +3032,226 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.tuner.hd + denon_read_group_trigger@instance: AVR-X6300H.tuner.hd channel: type: num - denon_command: tuner.hd.channel - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.channel + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - AVR-X6300H.tuner.hd - denon_read_initial: true + denon_read_initial@instance: true channelup: type: bool - denon_command: tuner.hd.channelup - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.channelup + denon_read@instance: false + denon_write@instance: true channeldown: type: bool - denon_command: tuner.hd.channeldown - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.channeldown + denon_read@instance: false + denon_write@instance: true multicastchannel: type: num - denon_command: tuner.hd.multicastchannel - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.multicastchannel + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - AVR-X6300H.tuner.hd presetmemory: type: num - denon_command: tuner.hd.presetmemory - denon_read: true - denon_write: true + denon_command@instance: tuner.hd.presetmemory + denon_read@instance: true + denon_write@instance: true preset: type: num - denon_command: tuner.hd.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - AVR-X6300H.tuner.hd presetup: type: bool - denon_command: tuner.hd.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.hd.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.hd.presetdown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.hd.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.hd.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.tuner - AVR-X6300H.tuner.hd - denon_read_initial: true + denon_read_initial@instance: true zone1: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1 + denon_read_group_trigger@instance: AVR-X6300H.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.control + denon_read_group_trigger@instance: AVR-X6300H.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings.sound + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3264,10 +3260,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3276,10 +3272,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3288,10 +3284,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3300,10 +3296,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3312,10 +3308,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3324,10 +3320,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3336,10 +3332,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3348,10 +3344,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3360,10 +3356,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3372,10 +3368,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3384,10 +3380,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3396,10 +3392,10 @@ item_structs: subwoofer2: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer2 - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer2 + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3411,14 +3407,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3427,10 +3423,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3439,22 +3435,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3463,29 +3459,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings.sound.general + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3494,10 +3490,10 @@ item_structs: speakersetup: type: str - denon_command: zone1.settings.sound.general.speakersetup - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.speakersetup + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3506,10 +3502,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3518,10 +3514,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3530,10 +3526,10 @@ item_structs: dialogenhance: type: num - denon_command: zone1.settings.sound.general.dialogenhance - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogenhance + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3542,10 +3538,10 @@ item_structs: subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3554,10 +3550,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3566,22 +3562,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3590,22 +3586,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3617,14 +3613,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone1.settings.video + denon_read_group_trigger@instance: AVR-X6300H.zone1.settings.video aspectratio: type: str - denon_command: zone1.settings.video.aspectratio - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.aspectratio + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3632,10 +3628,10 @@ item_structs: hdmimonitor: type: num - denon_command: zone1.settings.video.hdmimonitor - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmimonitor + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3643,10 +3639,10 @@ item_structs: hdmiresolution: type: str - denon_command: zone1.settings.video.hdmiresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmiresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3654,10 +3650,10 @@ item_structs: videoprocessingmode: type: str - denon_command: zone1.settings.video.videoprocessingmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoprocessingmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3665,10 +3661,10 @@ item_structs: videoresolution: type: str - denon_command: zone1.settings.video.videoresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3676,10 +3672,10 @@ item_structs: pictureenhancer: type: num - denon_command: zone1.settings.video.pictureenhancer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.pictureenhancer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3687,10 +3683,10 @@ item_structs: videoinput: type: str - denon_command: zone1.settings.video.videoinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone1 - AVR-X6300H.zone1.settings @@ -3701,63 +3697,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2 + denon_read_group_trigger@instance: AVR-X6300H.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.control + denon_read_group_trigger@instance: AVR-X6300H.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control @@ -3766,24 +3762,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.control @@ -3793,28 +3789,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.settings + denon_read_group_trigger@instance: AVR-X6300H.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.settings.sound + denon_read_group_trigger@instance: AVR-X6300H.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X6300H.zone2.settings.sound.channel_level front_left: type: num - denon_command: zone2.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings @@ -3823,10 +3819,10 @@ item_structs: front_right: type: num - denon_command: zone2.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings @@ -3838,14 +3834,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X6300H.zone2.settings.sound.tone_control treble: type: num - denon_command: zone2.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings @@ -3854,22 +3850,22 @@ item_structs: trebleup: type: bool - denon_command: zone2.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone2.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone2.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings @@ -3878,41 +3874,41 @@ item_structs: bassup: type: bool - denon_command: zone2.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone2.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone2.settings.sound.general + denon_read_group_trigger@instance: AVR-X6300H.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings - AVR-X6300H.zone2.settings.sound - AVR-X6300H.zone2.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone2.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone2 - AVR-X6300H.zone2.settings @@ -3924,83 +3920,83 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3 + denon_read_group_trigger@instance: AVR-X6300H.zone3 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.control + denon_read_group_trigger@instance: AVR-X6300H.zone3.control power: type: bool - denon_command: zone3.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control mute: type: bool - denon_command: zone3.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control volume: type: num - denon_command: zone3.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control volumeup: type: bool - denon_command: zone3.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone3.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumedown + denon_read@instance: false + denon_write@instance: true sleep: type: num - denon_command: zone3.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control standby: type: num - denon_command: zone3.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control input: type: str - denon_command: zone3.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.control @@ -4009,35 +4005,35 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.settings + denon_read_group_trigger@instance: AVR-X6300H.zone3.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.settings.sound + denon_read_group_trigger@instance: AVR-X6300H.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X6300H.zone3.settings.sound.channel_level front_left: type: num - denon_command: zone3.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.settings @@ -4046,10 +4042,10 @@ item_structs: front_right: type: num - denon_command: zone3.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.settings @@ -4061,14 +4057,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X6300H.zone3.settings.sound.tone_control treble: type: num - denon_command: zone3.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.settings @@ -4077,22 +4073,22 @@ item_structs: trebleup: type: bool - denon_command: zone3.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone3.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone3.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.settings @@ -4101,29 +4097,29 @@ item_structs: bassup: type: bool - denon_command: zone3.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone3.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X6300H.zone3.settings.sound.general + denon_read_group_trigger@instance: AVR-X6300H.zone3.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone3.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X6300H - AVR-X6300H.zone3 - AVR-X6300H.zone3.settings @@ -4135,21 +4131,21 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H + denon_read_group_trigger@instance: AVR-X4300H general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.general + denon_read_group_trigger@instance: AVR-X4300H.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general cache: true @@ -4165,84 +4161,84 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general @@ -4251,68 +4247,68 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.tuner + denon_read_group_trigger@instance: AVR-X4300H.tuner preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.tuner @@ -4321,148 +4317,147 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1 + denon_read_group_trigger@instance: AVR-X4300H.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.control + denon_read_group_trigger@instance: AVR-X4300H.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings.sound + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4471,10 +4466,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4483,10 +4478,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4495,10 +4490,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4507,10 +4502,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4519,10 +4514,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4531,10 +4526,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4543,10 +4538,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4555,10 +4550,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4567,10 +4562,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4579,10 +4574,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4591,10 +4586,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4603,10 +4598,10 @@ item_structs: subwoofer2: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer2 - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer2 + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4618,14 +4613,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4634,10 +4629,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4646,22 +4641,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4670,29 +4665,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings.sound.general + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4701,10 +4696,10 @@ item_structs: speakersetup: type: str - denon_command: zone1.settings.sound.general.speakersetup - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.speakersetup + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4713,10 +4708,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4725,10 +4720,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4737,10 +4732,10 @@ item_structs: dialogtoggle: type: bool - denon_command: zone1.settings.sound.general.dialogtoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogtoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4749,10 +4744,10 @@ item_structs: dialog: type: num - denon_command: zone1.settings.sound.general.dialog - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialog + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4761,22 +4756,22 @@ item_structs: dialogup: type: bool - denon_command: zone1.settings.sound.general.dialogup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogup + denon_read@instance: false + denon_write@instance: true dialogdown: type: bool - denon_command: zone1.settings.sound.general.dialogdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogdown + denon_read@instance: false + denon_write@instance: true subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4785,10 +4780,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4797,22 +4792,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4821,22 +4816,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4848,14 +4843,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone1.settings.video + denon_read_group_trigger@instance: AVR-X4300H.zone1.settings.video aspectratio: type: str - denon_command: zone1.settings.video.aspectratio - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.aspectratio + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4863,10 +4858,10 @@ item_structs: hdmimonitor: type: num - denon_command: zone1.settings.video.hdmimonitor - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmimonitor + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4874,10 +4869,10 @@ item_structs: hdmiresolution: type: str - denon_command: zone1.settings.video.hdmiresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmiresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4885,10 +4880,10 @@ item_structs: videoprocessingmode: type: str - denon_command: zone1.settings.video.videoprocessingmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoprocessingmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4896,10 +4891,10 @@ item_structs: videoresolution: type: str - denon_command: zone1.settings.video.videoresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4907,10 +4902,10 @@ item_structs: pictureenhancer: type: num - denon_command: zone1.settings.video.pictureenhancer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.pictureenhancer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4918,10 +4913,10 @@ item_structs: videoinput: type: str - denon_command: zone1.settings.video.videoinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone1 - AVR-X4300H.zone1.settings @@ -4932,63 +4927,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2 + denon_read_group_trigger@instance: AVR-X4300H.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.control + denon_read_group_trigger@instance: AVR-X4300H.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control @@ -4997,24 +4992,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.control @@ -5024,28 +5019,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.settings + denon_read_group_trigger@instance: AVR-X4300H.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.settings.sound + denon_read_group_trigger@instance: AVR-X4300H.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X4300H.zone2.settings.sound.channel_level front_left: type: num - denon_command: zone2.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings @@ -5054,10 +5049,10 @@ item_structs: front_right: type: num - denon_command: zone2.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings @@ -5069,14 +5064,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X4300H.zone2.settings.sound.tone_control treble: type: num - denon_command: zone2.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings @@ -5085,22 +5080,22 @@ item_structs: trebleup: type: bool - denon_command: zone2.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone2.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone2.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings @@ -5109,41 +5104,41 @@ item_structs: bassup: type: bool - denon_command: zone2.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone2.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone2.settings.sound.general + denon_read_group_trigger@instance: AVR-X4300H.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings - AVR-X4300H.zone2.settings.sound - AVR-X4300H.zone2.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone2.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone2 - AVR-X4300H.zone2.settings @@ -5155,83 +5150,83 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3 + denon_read_group_trigger@instance: AVR-X4300H.zone3 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.control + denon_read_group_trigger@instance: AVR-X4300H.zone3.control power: type: bool - denon_command: zone3.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control mute: type: bool - denon_command: zone3.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control volume: type: num - denon_command: zone3.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control volumeup: type: bool - denon_command: zone3.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone3.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone3.control.volumedown + denon_read@instance: false + denon_write@instance: true sleep: type: num - denon_command: zone3.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control standby: type: num - denon_command: zone3.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control input: type: str - denon_command: zone3.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.control @@ -5240,35 +5235,35 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.settings + denon_read_group_trigger@instance: AVR-X4300H.zone3.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.settings.sound + denon_read_group_trigger@instance: AVR-X4300H.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X4300H.zone3.settings.sound.channel_level front_left: type: num - denon_command: zone3.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.settings @@ -5277,10 +5272,10 @@ item_structs: front_right: type: num - denon_command: zone3.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.settings @@ -5292,14 +5287,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X4300H.zone3.settings.sound.tone_control treble: type: num - denon_command: zone3.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.settings @@ -5308,22 +5303,22 @@ item_structs: trebleup: type: bool - denon_command: zone3.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone3.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone3.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.settings @@ -5332,29 +5327,29 @@ item_structs: bassup: type: bool - denon_command: zone3.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone3.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone3.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X4300H.zone3.settings.sound.general + denon_read_group_trigger@instance: AVR-X4300H.zone3.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone3.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone3.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X4300H - AVR-X4300H.zone3 - AVR-X4300H.zone3.settings @@ -5366,21 +5361,21 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W + denon_read_group_trigger@instance: AVR-X3300W general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.general + denon_read_group_trigger@instance: AVR-X3300W.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general cache: true @@ -5396,93 +5391,93 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general display: type: str - denon_command: general.display - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.display + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general @@ -5491,90 +5486,90 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.tuner + denon_read_group_trigger@instance: AVR-X3300W.tuner title: type: str - denon_command: tuner.title - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: tuner.title + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.tuner - denon_read_initial: true + denon_read_initial@instance: true album: type: str - denon_command: tuner.album - denon_read: true - denon_write: false + denon_command@instance: tuner.album + denon_read@instance: true + denon_write@instance: false artist: type: str - denon_command: tuner.artist - denon_read: true - denon_write: false + denon_command@instance: tuner.artist + denon_read@instance: true + denon_write@instance: false preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.tuner @@ -5583,148 +5578,147 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1 + denon_read_group_trigger@instance: AVR-X3300W.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.control + denon_read_group_trigger@instance: AVR-X3300W.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings.sound + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5733,10 +5727,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5745,10 +5739,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5757,10 +5751,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5769,10 +5763,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5781,10 +5775,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5793,10 +5787,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5805,10 +5799,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5817,10 +5811,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5829,10 +5823,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5841,10 +5835,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5853,10 +5847,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5865,10 +5859,10 @@ item_structs: subwoofer2: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer2 - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer2 + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5880,14 +5874,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5896,10 +5890,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5908,22 +5902,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5932,29 +5926,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings.sound.general + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5963,10 +5957,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5975,10 +5969,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5987,10 +5981,10 @@ item_structs: dialogtoggle: type: bool - denon_command: zone1.settings.sound.general.dialogtoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogtoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -5999,10 +5993,10 @@ item_structs: dialog: type: num - denon_command: zone1.settings.sound.general.dialog - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialog + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6011,22 +6005,22 @@ item_structs: dialogup: type: bool - denon_command: zone1.settings.sound.general.dialogup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogup + denon_read@instance: false + denon_write@instance: true dialogdown: type: bool - denon_command: zone1.settings.sound.general.dialogdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogdown + denon_read@instance: false + denon_write@instance: true subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6035,10 +6029,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6047,22 +6041,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6071,22 +6065,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6098,14 +6092,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone1.settings.video + denon_read_group_trigger@instance: AVR-X3300W.zone1.settings.video aspectratio: type: str - denon_command: zone1.settings.video.aspectratio - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.aspectratio + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6113,10 +6107,10 @@ item_structs: hdmiresolution: type: str - denon_command: zone1.settings.video.hdmiresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmiresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6124,10 +6118,10 @@ item_structs: videoprocessingmode: type: str - denon_command: zone1.settings.video.videoprocessingmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoprocessingmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6135,10 +6129,10 @@ item_structs: videoresolution: type: str - denon_command: zone1.settings.video.videoresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6146,10 +6140,10 @@ item_structs: pictureenhancer: type: num - denon_command: zone1.settings.video.pictureenhancer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.pictureenhancer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6157,10 +6151,10 @@ item_structs: videoinput: type: str - denon_command: zone1.settings.video.videoinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone1 - AVR-X3300W.zone1.settings @@ -6171,63 +6165,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2 + denon_read_group_trigger@instance: AVR-X3300W.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.control + denon_read_group_trigger@instance: AVR-X3300W.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control @@ -6236,24 +6230,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.control @@ -6263,28 +6257,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.settings + denon_read_group_trigger@instance: AVR-X3300W.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.settings.sound + denon_read_group_trigger@instance: AVR-X3300W.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X3300W.zone2.settings.sound.channel_level front_left: type: num - denon_command: zone2.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings @@ -6293,10 +6287,10 @@ item_structs: front_right: type: num - denon_command: zone2.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings @@ -6308,14 +6302,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X3300W.zone2.settings.sound.tone_control treble: type: num - denon_command: zone2.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings @@ -6324,22 +6318,22 @@ item_structs: trebleup: type: bool - denon_command: zone2.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone2.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone2.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings @@ -6348,41 +6342,41 @@ item_structs: bassup: type: bool - denon_command: zone2.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone2.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone2.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X3300W.zone2.settings.sound.general + denon_read_group_trigger@instance: AVR-X3300W.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings - AVR-X3300W.zone2.settings.sound - AVR-X3300W.zone2.settings.sound.general - HPF: + hpf: type: bool - denon_command: zone2.settings.sound.general.HPF - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.HPF + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X3300W - AVR-X3300W.zone2 - AVR-X3300W.zone2.settings @@ -6394,21 +6388,21 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W + denon_read_group_trigger@instance: AVR-X2300W general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.general + denon_read_group_trigger@instance: AVR-X2300W.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general cache: true @@ -6424,93 +6418,93 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general display: type: str - denon_command: general.display - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.display + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general @@ -6519,90 +6513,90 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.tuner + denon_read_group_trigger@instance: AVR-X2300W.tuner title: type: str - denon_command: tuner.title - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: tuner.title + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.tuner - denon_read_initial: true + denon_read_initial@instance: true album: type: str - denon_command: tuner.album - denon_read: true - denon_write: false + denon_command@instance: tuner.album + denon_read@instance: true + denon_write@instance: false artist: type: str - denon_command: tuner.artist - denon_read: true - denon_write: false + denon_command@instance: tuner.artist + denon_read@instance: true + denon_write@instance: false preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.tuner @@ -6611,148 +6605,147 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1 + denon_read_group_trigger@instance: AVR-X2300W.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.control + denon_read_group_trigger@instance: AVR-X2300W.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings.sound + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6761,10 +6754,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6773,10 +6766,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6785,10 +6778,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6797,10 +6790,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6809,10 +6802,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6821,10 +6814,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6833,10 +6826,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6845,10 +6838,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6857,10 +6850,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6869,10 +6862,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6881,10 +6874,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6896,14 +6889,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6912,10 +6905,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6924,22 +6917,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6948,29 +6941,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings.sound.general + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6979,10 +6972,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -6991,10 +6984,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7003,10 +6996,10 @@ item_structs: dialogtoggle: type: bool - denon_command: zone1.settings.sound.general.dialogtoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogtoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7015,10 +7008,10 @@ item_structs: dialog: type: num - denon_command: zone1.settings.sound.general.dialog - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialog + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7027,22 +7020,22 @@ item_structs: dialogup: type: bool - denon_command: zone1.settings.sound.general.dialogup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogup + denon_read@instance: false + denon_write@instance: true dialogdown: type: bool - denon_command: zone1.settings.sound.general.dialogdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogdown + denon_read@instance: false + denon_write@instance: true subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7051,10 +7044,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7063,22 +7056,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7087,22 +7080,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7114,14 +7107,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone1.settings.video + denon_read_group_trigger@instance: AVR-X2300W.zone1.settings.video aspectratio: type: str - denon_command: zone1.settings.video.aspectratio - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.aspectratio + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7129,10 +7122,10 @@ item_structs: hdmimonitor: type: num - denon_command: zone1.settings.video.hdmimonitor - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmimonitor + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7140,10 +7133,10 @@ item_structs: hdmiresolution: type: str - denon_command: zone1.settings.video.hdmiresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.hdmiresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7151,10 +7144,10 @@ item_structs: videoprocessingmode: type: str - denon_command: zone1.settings.video.videoprocessingmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoprocessingmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7162,10 +7155,10 @@ item_structs: videoresolution: type: str - denon_command: zone1.settings.video.videoresolution - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoresolution + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7173,10 +7166,10 @@ item_structs: pictureenhancer: type: num - denon_command: zone1.settings.video.pictureenhancer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.pictureenhancer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7184,10 +7177,10 @@ item_structs: videoinput: type: str - denon_command: zone1.settings.video.videoinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.video.videoinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone1 - AVR-X2300W.zone1.settings @@ -7198,63 +7191,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2 + denon_read_group_trigger@instance: AVR-X2300W.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2.control + denon_read_group_trigger@instance: AVR-X2300W.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control @@ -7263,24 +7256,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.control @@ -7290,28 +7283,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2.settings + denon_read_group_trigger@instance: AVR-X2300W.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2.settings.sound + denon_read_group_trigger@instance: AVR-X2300W.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X2300W.zone2.settings.sound.channel_level front_left: type: num - denon_command: zone2.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.settings @@ -7320,10 +7313,10 @@ item_structs: front_right: type: num - denon_command: zone2.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.settings @@ -7335,14 +7328,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X2300W.zone2.settings.sound.general + denon_read_group_trigger@instance: AVR-X2300W.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X2300W - AVR-X2300W.zone2 - AVR-X2300W.zone2.settings @@ -7354,21 +7347,21 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W + denon_read_group_trigger@instance: AVR-X1300W general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.general + denon_read_group_trigger@instance: AVR-X1300W.general custom_inputnames: type: dict - denon_command: general.custom_inputnames - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.custom_inputnames + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general cache: true @@ -7384,93 +7377,93 @@ item_structs: power: type: bool - denon_command: general.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general setupmenu: type: bool - denon_command: general.setupmenu - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.setupmenu + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general display: type: str - denon_command: general.display - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.display + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general soundmode: type: str - denon_command: general.soundmode - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.soundmode + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general - denon_read_initial: true + denon_read_initial@instance: true inputsignal: type: str - denon_command: general.inputsignal - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputsignal + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general - denon_read_initial: true + denon_read_initial@instance: true inputrate: type: num - denon_command: general.inputrate - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputrate + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general - denon_read_initial: true + denon_read_initial@instance: true inputformat: type: str - denon_command: general.inputformat - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputformat + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general - denon_read_initial: true + denon_read_initial@instance: true inputresolution: type: str - denon_command: general.inputresolution - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: general.inputresolution + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general - denon_read_initial: true + denon_read_initial@instance: true outputresolution: type: str - denon_command: general.outputresolution - denon_read: true - denon_write: false + denon_command@instance: general.outputresolution + denon_read@instance: true + denon_write@instance: false ecomode: type: str - denon_command: general.ecomode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: general.ecomode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general @@ -7479,90 +7472,90 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.tuner + denon_read_group_trigger@instance: AVR-X1300W.tuner title: type: str - denon_command: tuner.title - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: tuner.title + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.tuner - denon_read_initial: true + denon_read_initial@instance: true album: type: str - denon_command: tuner.album - denon_read: true - denon_write: false + denon_command@instance: tuner.album + denon_read@instance: true + denon_write@instance: false artist: type: str - denon_command: tuner.artist - denon_read: true - denon_write: false + denon_command@instance: tuner.artist + denon_read@instance: true + denon_write@instance: false preset: type: num - denon_command: tuner.preset - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.preset + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.tuner - denon_read_initial: true + denon_read_initial@instance: true presetup: type: bool - denon_command: tuner.presetup - denon_read: false - denon_write: true + denon_command@instance: tuner.presetup + denon_read@instance: false + denon_write@instance: true presetdown: type: bool - denon_command: tuner.presetdown - denon_read: false - denon_write: true + denon_command@instance: tuner.presetdown + denon_read@instance: false + denon_write@instance: true frequency: type: num - denon_command: tuner.frequency - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.frequency + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.tuner - denon_read_initial: true + denon_read_initial@instance: true frequencyup: type: bool - denon_command: tuner.frequencyup - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencyup + denon_read@instance: false + denon_write@instance: true frequencydown: type: bool - denon_command: tuner.frequencydown - denon_read: false - denon_write: true + denon_command@instance: tuner.frequencydown + denon_read@instance: false + denon_write@instance: true band: type: str - denon_command: tuner.band - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.band + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.tuner - denon_read_initial: true + denon_read_initial@instance: true tuningmode: type: str - denon_command: tuner.tuningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: tuner.tuningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.tuner @@ -7571,148 +7564,147 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1 + denon_read_group_trigger@instance: AVR-X1300W.zone1 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.control + denon_read_group_trigger@instance: AVR-X1300W.zone1.control power: type: bool - denon_command: zone1.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true mute: type: bool - denon_command: zone1.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volume: type: num - denon_command: zone1.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true volumeup: type: bool - denon_command: zone1.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone1.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone1.control.volumedown + denon_read@instance: false + denon_write@instance: true volumemax: type: num - denon_command: zone1.control.volumemax - denon_read: true - denon_write: false - denon_read_group: + denon_command@instance: zone1.control.volumemax + denon_read@instance: true + denon_write@instance: false + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true input: type: str - denon_command: zone1.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true on_change: - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None listeningmode: type: str - denon_command: zone1.control.listeningmode - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.listeningmode + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true sleep: type: num - denon_command: zone1.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true standby: type: num - denon_command: zone1.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.control - denon_read_initial: true + denon_read_initial@instance: true settings: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.settings + denon_read_group_trigger@instance: AVR-X1300W.zone1.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.settings.sound + denon_read_group_trigger@instance: AVR-X1300W.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.settings.sound.channel_level + denon_read_group_trigger@instance: AVR-X1300W.zone1.settings.sound.channel_level front_left: type: num - denon_command: zone1.settings.sound.channel_level.front_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7721,10 +7713,10 @@ item_structs: front_right: type: num - denon_command: zone1.settings.sound.channel_level.front_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7733,10 +7725,10 @@ item_structs: front_height_left: type: num - denon_command: zone1.settings.sound.channel_level.front_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7745,10 +7737,10 @@ item_structs: front_height_right: type: num - denon_command: zone1.settings.sound.channel_level.front_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7757,10 +7749,10 @@ item_structs: front_center: type: num - denon_command: zone1.settings.sound.channel_level.front_center - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.front_center + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7769,10 +7761,10 @@ item_structs: surround_left: type: num - denon_command: zone1.settings.sound.channel_level.surround_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7781,10 +7773,10 @@ item_structs: surround_right: type: num - denon_command: zone1.settings.sound.channel_level.surround_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surround_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7793,10 +7785,10 @@ item_structs: surroundback_left: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7805,10 +7797,10 @@ item_structs: surroundback_right: type: num - denon_command: zone1.settings.sound.channel_level.surroundback_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.surroundback_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7817,10 +7809,10 @@ item_structs: rear_height_left: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_left - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_left + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7829,10 +7821,10 @@ item_structs: rear_height_right: type: num - denon_command: zone1.settings.sound.channel_level.rear_height_right - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.rear_height_right + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7841,10 +7833,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.channel_level.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.channel_level.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7856,14 +7848,14 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.settings.sound.tone_control + denon_read_group_trigger@instance: AVR-X1300W.zone1.settings.sound.tone_control tone: type: bool - denon_command: zone1.settings.sound.tone_control.tone - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.tone + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7872,10 +7864,10 @@ item_structs: treble: type: num - denon_command: zone1.settings.sound.tone_control.treble - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.treble + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7884,22 +7876,22 @@ item_structs: trebleup: type: bool - denon_command: zone1.settings.sound.tone_control.trebleup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebleup + denon_read@instance: false + denon_write@instance: true trebledown: type: bool - denon_command: zone1.settings.sound.tone_control.trebledown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.trebledown + denon_read@instance: false + denon_write@instance: true bass: type: num - denon_command: zone1.settings.sound.tone_control.bass - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.tone_control.bass + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7908,29 +7900,29 @@ item_structs: bassup: type: bool - denon_command: zone1.settings.sound.tone_control.bassup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassup + denon_read@instance: false + denon_write@instance: true bassdown: type: bool - denon_command: zone1.settings.sound.tone_control.bassdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.tone_control.bassdown + denon_read@instance: false + denon_write@instance: true general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone1.settings.sound.general + denon_read_group_trigger@instance: AVR-X1300W.zone1.settings.sound.general cinema_eq: type: bool - denon_command: zone1.settings.sound.general.cinema_eq - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.cinema_eq + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7939,10 +7931,10 @@ item_structs: hdmiaudioout: type: str - denon_command: zone1.settings.sound.general.hdmiaudioout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.hdmiaudioout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7951,10 +7943,10 @@ item_structs: dynamicrange: type: num - denon_command: zone1.settings.sound.general.dynamicrange - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dynamicrange + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7963,10 +7955,10 @@ item_structs: dialogtoggle: type: bool - denon_command: zone1.settings.sound.general.dialogtoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialogtoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7975,10 +7967,10 @@ item_structs: dialog: type: num - denon_command: zone1.settings.sound.general.dialog - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.dialog + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -7987,22 +7979,22 @@ item_structs: dialogup: type: bool - denon_command: zone1.settings.sound.general.dialogup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogup + denon_read@instance: false + denon_write@instance: true dialogdown: type: bool - denon_command: zone1.settings.sound.general.dialogdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.dialogdown + denon_read@instance: false + denon_write@instance: true subwoofertoggle: type: bool - denon_command: zone1.settings.sound.general.subwoofertoggle - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofertoggle + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -8011,10 +8003,10 @@ item_structs: subwoofer: type: num - denon_command: zone1.settings.sound.general.subwoofer - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.subwoofer + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -8023,22 +8015,22 @@ item_structs: subwooferup: type: bool - denon_command: zone1.settings.sound.general.subwooferup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferup + denon_read@instance: false + denon_write@instance: true subwooferdown: type: bool - denon_command: zone1.settings.sound.general.subwooferdown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.subwooferdown + denon_read@instance: false + denon_write@instance: true lfe: type: num - denon_command: zone1.settings.sound.general.lfe - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.lfe + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -8047,22 +8039,22 @@ item_structs: lfeup: type: bool - denon_command: zone1.settings.sound.general.lfeup - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfeup + denon_read@instance: false + denon_write@instance: true lfedown: type: bool - denon_command: zone1.settings.sound.general.lfedown - denon_read: false - denon_write: true + denon_command@instance: zone1.settings.sound.general.lfedown + denon_read@instance: false + denon_write@instance: true audioinput: type: str - denon_command: zone1.settings.sound.general.audioinput - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone1.settings.sound.general.audioinput + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone1 - AVR-X1300W.zone1.settings @@ -8074,63 +8066,63 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone2 + denon_read_group_trigger@instance: AVR-X1300W.zone2 control: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone2.control + denon_read_group_trigger@instance: AVR-X1300W.zone2.control power: type: bool - denon_command: zone2.control.power - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.power + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control mute: type: bool - denon_command: zone2.control.mute - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.mute + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control volume: type: num - denon_command: zone2.control.volume - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.volume + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control volumeup: type: bool - denon_command: zone2.control.volumeup - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumeup + denon_read@instance: false + denon_write@instance: true volumedown: type: bool - denon_command: zone2.control.volumedown - denon_read: false - denon_write: true + denon_command@instance: zone2.control.volumedown + denon_read@instance: false + denon_write@instance: true input: type: str - denon_command: zone2.control.input - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.input + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control @@ -8139,24 +8131,24 @@ item_structs: custom_name: type: str - on_change: .. = '' if sh......general.custom_inputnames.reverse() == {} else sh......general.custom_inputnames.reverse()[value] + on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None sleep: type: num - denon_command: zone2.control.sleep - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.sleep + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control standby: type: num - denon_command: zone2.control.standby - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.control.standby + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.control @@ -8166,28 +8158,28 @@ item_structs: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone2.settings + denon_read_group_trigger@instance: AVR-X1300W.zone2.settings sound: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone2.settings.sound + denon_read_group_trigger@instance: AVR-X1300W.zone2.settings.sound general: read: type: bool enforce_updates: true - denon_read_group_trigger: AVR-X1300W.zone2.settings.sound.general + denon_read_group_trigger@instance: AVR-X1300W.zone2.settings.sound.general hdmiout: type: str - denon_command: zone2.settings.sound.general.hdmiout - denon_read: true - denon_write: true - denon_read_group: + denon_command@instance: zone2.settings.sound.general.hdmiout + denon_read@instance: true + denon_write@instance: true + denon_read_group@instance: - AVR-X1300W - AVR-X1300W.zone2 - AVR-X1300W.zone2.settings From fdc0d48a8290f23b898518204bde12c9120c91b7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 1 Jun 2024 21:23:15 +0200 Subject: [PATCH 008/121] denon plugin: add on_suspend/resume functions --- denon/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/denon/__init__.py b/denon/__init__.py index c722cdc34..8d3fadd58 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -65,6 +65,17 @@ def on_connect(self, by=None): self.send_command('general.custom_inputnames') self._read_initial_values(force=True) + def _on_suspend(self): + for scheduler in self.scheduler_get_all(): + self.scheduler_remove(scheduler) + + def _on_resume(self): + if self.scheduler_get('resend'): + self.scheduler_remove('resend') + self.logger.debug(f"Resuming.. Cycle for retry: {self._sendretry_cycle}") + if self._send_retries >= 1: + self.scheduler_add('resend', self._resend, cycle=self._sendretry_cycle) + def _set_device_defaults(self): self._use_callbacks = True self._custom_inputnames = {} From 58d9567ab60b0dbd4937a519456baab65d1249ae Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 1 Jun 2024 22:28:41 +0200 Subject: [PATCH 009/121] denon plugin: implement delay for initial value read --- denon/__init__.py | 18 +++++++++++++----- denon/plugin.yaml | 8 ++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index 8d3fadd58..905db1c62 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -26,6 +26,8 @@ import sys import threading import time +import datetime +from lib.shtime import Shtime if __name__ == '__main__': builtins.SDP_standalone = True @@ -63,7 +65,13 @@ def on_connect(self, by=None): self.scheduler_add('resend', self._resend, cycle=self._sendretry_cycle) self.logger.debug("Checking for custom input names.") self.send_command('general.custom_inputnames') - self._read_initial_values(force=True) + if self.scheduler_get('read_initial_values'): + return + elif self._initial_value_read_delay > 0: + self.logger.dbghigh(f"On connect reading initial values after {self._initial_value_read_delay} seconds.") + self.scheduler_add('read_initial_values', self._read_initial_values, value={'force': True}, next=self.shtime.now() + datetime.timedelta(seconds=self._initial_value_read_delay)) + else: + self._read_initial_values(True) def _on_suspend(self): for scheduler in self.scheduler_get_all(): @@ -101,10 +109,10 @@ def _set_device_defaults(self): # the sent command. Getting it as return value would assign it to the wrong # command and discard it... so break the "return result"-chain and don't # return anything - def _send(self, data_dict): - if data_dict.get('returnvalue') is not None: - self._sending.update({data_dict['command']: data_dict}) - self._connection.send(data_dict) + def _send(self, data_dict, resend_info=None): + if resend_info.get('returnvalue') is not None: + self._sending.update({resend_info.get('command'): resend_info}) + return self._connection.send(data_dict) def _resend(self): if not self.alive or self.suspended: diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 1eabf58fe..dd8670950 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -202,6 +202,14 @@ parameters: where no (suiting) answer got received. ' + delay_initial_read: + type: num + default: 0 + + description: + de: Warte nach Verbindungsaufbau mit dem Abfragen von Werten + en: Wait after connection with querying values + connect_cycle: type: num default: 3 From fabe5fd43a45516c54835337c8beac0f1d021d0c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 3 Jul 2024 10:20:57 +0200 Subject: [PATCH 010/121] denon plugin: fix resend_info --- denon/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/denon/__init__.py b/denon/__init__.py index 905db1c62..6ace0cb1c 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -109,7 +109,7 @@ def _set_device_defaults(self): # the sent command. Getting it as return value would assign it to the wrong # command and discard it... so break the "return result"-chain and don't # return anything - def _send(self, data_dict, resend_info=None): + def _send(self, data_dict, resend_info={}): if resend_info.get('returnvalue') is not None: self._sending.update({resend_info.get('command'): resend_info}) return self._connection.send(data_dict) From 3616a3847a14eec0236256d7a55e68c9f024fa76 Mon Sep 17 00:00:00 2001 From: ivande Date: Thu, 4 Jul 2024 23:39:54 +0200 Subject: [PATCH 011/121] telegram Plugin: Plugin controllable with stop/run/pause_item --- telegram/__init__.py | 241 +++++++++++++++++++++++++++--------------- telegram/plugin.yaml | 14 ++- telegram/user_doc.rst | 6 ++ 3 files changed, 171 insertions(+), 90 deletions(-) diff --git a/telegram/__init__.py b/telegram/__init__.py index c3ec5507e..6c0dded7b 100755 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -2,7 +2,7 @@ # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### # Copyright 2017 Markus Garscha http://knx-user-forum.de/ -# 2018-2023 Ivan De Filippis +# 2018-2024 Ivan De Filippis # 2018-2021 Bernd Meiners Bernd.Meiners@mail.de ######################################################################### # @@ -68,7 +68,7 @@ class Telegram(SmartPlugin): - PLUGIN_VERSION = "2.0.1" + PLUGIN_VERSION = '2.0.3' _items = [] # all items using attribute ``telegram_message`` _items_info = {} # dict used whith the info-command: key = attribute_value, val= item_list telegram_info @@ -77,6 +77,7 @@ class Telegram(SmartPlugin): _chat_ids_item = {} # an item with a dict of chat_id and write access _waitAnswer = None # wait a specific answer Yes/No - or num (change_item) _queue = None # queue for the messages to be sent + _cHandlers = [] # CommandHandler from parse_item def __init__(self, sh): """ @@ -100,8 +101,9 @@ def __init__(self, sh): self.logger.error(f"{self.get_fullname()}: Unable to import Python package 'python-telegram-bot' [{REQUIRED_PACKAGE_IMPORTED}]") return - self._loop = asyncio.new_event_loop() # new_event is required for multi-instance - asyncio.set_event_loop(self._loop) + #self._loop = asyncio.new_event_loop() # new_event is required for multi-instance + #self.log_event_loop_details() + #asyncio.set_event_loop(self._loop) self.alive = False self._name = self.get_parameter_value('name') @@ -116,27 +118,12 @@ def __init__(self, sh): self._resend_delay = self.get_parameter_value('resend_delay') self._resend_attemps = self.get_parameter_value('resend_attemps') + self._pause_item = None + self._pause_item_path = self.get_parameter_value('pause_item') + self._bot = None self._queue = Queue() - self._application = Application.builder().token(self._token).build() - - if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug("adding command handlers to application") - - self._application.add_error_handler(self.eHandler) - self._application.add_handler(CommandHandler('time', self.cHandler_time)) - self._application.add_handler(CommandHandler('help', self.cHandler_help)) - self._application.add_handler(CommandHandler('hide', self.cHandler_hide)) - self._application.add_handler(CommandHandler('list', self.cHandler_list)) - self._application.add_handler(CommandHandler('info', self.cHandler_info)) - self._application.add_handler(CommandHandler('start', self.cHandler_start)) - self._application.add_handler(CommandHandler('lo', self.cHandler_lo)) - self._application.add_handler(CommandHandler('tr', self.cHandler_tr)) - self._application.add_handler(CommandHandler('control', self.cHandler_control)) - # Filters.text includes also commands, starting with ``/`` so it is needed to exclude them. - self._application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.mHandler)) - self.init_webinterface() if not self.init_webinterface(WebInterface): self.logger.error("Unable to start Webinterface") @@ -162,22 +149,56 @@ def run(self): This is called when the plugins thread is about to run """ if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug("Run method called") + self.logger.debug(f"Plugin '{self.get_fullname()}': run method called") - self.logics = Logics.get_instance() # Returns the instance of the Logics class, to be used to access the logics-api + if self.alive: + return + if self._pause_item is not None and bool(self._pause_item()): + self.logger.info(f'plugin not startet - pause_item is True') + return + + self._loop = asyncio.new_event_loop() # new_event is required for multi-instance + asyncio.set_event_loop(self._loop) + + self._application = Application.builder().token(self._token).build() + + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug("adding command handlers to application") + + self._application.add_error_handler(self.eHandler) + self._application.add_handler(CommandHandler('time', self.cHandler_time)) + self._application.add_handler(CommandHandler('help', self.cHandler_help)) + self._application.add_handler(CommandHandler('hide', self.cHandler_hide)) + self._application.add_handler(CommandHandler('list', self.cHandler_list)) + self._application.add_handler(CommandHandler('info', self.cHandler_info)) + self._application.add_handler(CommandHandler('start', self.cHandler_start)) + self._application.add_handler(CommandHandler('lo', self.cHandler_lo)) + self._application.add_handler(CommandHandler('tr', self.cHandler_tr)) + self._application.add_handler(CommandHandler('control', self.cHandler_control)) + # Filters.text includes also commands, starting with ``/`` so it is needed to exclude them. + self._application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.mHandler)) + # add CommandHandlers from parse_item + for cHandler in self._cHandlers: + self._application.add_handler(cHandler) + self.alive = True + self.logics = Logics.get_instance() # Returns the instance of the Logics class, to be used to access the logics-api + self._loop.run_until_complete(self.run_coros()) if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug(f"Run method ended") + self.logger.debug(f"Plugin '{self.get_fullname()}': run method finished ") def stop(self): """ This is called when the plugins thread is about to stop """ if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug("stop telegram plugin") + self.logger.debug(f"Plugin '{self.get_fullname()}': stop method called") + + if self.scheduler_get('telegram_change_item_timeout'): + self.scheduler_remove('telegram_change_item_timeout') try: if self._bye_msg: @@ -188,8 +209,18 @@ def stop(self): except Exception as e: self.logger.error(f"could not send bye message [{e}]") - time.sleep(1) - self.alive = False # Clears the infiniti loop in sendQueue + if not self._queue.empty(): + time.sleep(5) + if not self._queue.empty(): + try: + self.alive = False # Clears the infiniti loop in sendQueue + self.remove_all_events() + self.logger.debug(f"Events removed.") + except Exception as e: + self.logger.error(f"An error occurred while removing the events: [{e}]") + + self.alive = False + try: asyncio.gather(self._taskConn, self._taskQueue) self.disconnect() @@ -204,8 +235,20 @@ def stop(self): self.logger.error(f"An error occurred while stopping the plugin [{e}]") if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug("stop telegram plugin finished") + self.logger.debug(f"Plugin '{self.get_fullname()}': stop method finished") + + def remove_all_events(self): + while not self._queue.empty(): + self._queue.get_nowait() # Entfernt das Event aus der Queue + self._queue.task_done() # Markiert die Aufgabe als erledigt + self.logger.debug("all events removed") + def _start_loop(self): + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + self.logger.debug("Starting event loop") + self._loop.run_forever() + async def run_coros(self): """ This method run multiple coroutines concurrently using asyncio @@ -220,37 +263,41 @@ async def connect(self): """ if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("connect method called") - try: - await self._application.initialize() - await self._application.start() - self._updater = self._application.updater - - q = await self._updater.start_polling(timeout=self._long_polling_timeout, error_callback=self.error_handler) + while self.alive: + try: + await self._application.initialize() + await self._application.start() + self._updater = self._application.updater - if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug(f"started polling the updater, Queue is {q}") + q = await self._updater.start_polling(timeout=self._long_polling_timeout, error_callback=self.error_handler) - self._bot = self._updater.bot - self.logger.info(f"Telegram bot is listening: {await self._updater.bot.getMe()}") - if self._welcome_msg: if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug(f"sent welcome message {self._welcome_msg}") - cids = [key for key, value in self._chat_ids_item().items() if value == 1] - self.msg_broadcast(self._welcome_msg, chat_id=cids) + self.logger.debug(f"started polling the updater, Queue is {q}") - except TelegramError as e: - # catch Unauthorized errors due to an invalid token - self.logger.error(f"Unable to start up Telegram conversation. Maybe an invalid token? {e}") - return False + self._bot = self._updater.bot + self.logger.info(f"Telegram bot is listening: {await self._updater.bot.getMe()}") + if self._welcome_msg: + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"sent welcome message {self._welcome_msg}") + cids = [key for key, value in self._chat_ids_item().items() if value == 1] + self.msg_broadcast(self._welcome_msg, chat_id=cids) + + # If no exception occurred, break the loop and exit the function + break + + except TelegramError as e: + self.logger.error(f"Unable to start up Telegram conversation. {e}") + await asyncio.sleep(60) # Wait for 60 seconds before retrying - while + if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug("connect method end") - def error_handler(self, update, context): + def error_handler(self, error): """ Just logs an error in case of a problem """ try: - self.logger.warning(f'Update {update} caused error {context.error}') + self.logger.error(f'Update caused error {error}') except Exception: pass @@ -330,6 +377,14 @@ def parse_item(self, item): Default plugin parse_item method. Is called when the plugin is initialized. :param item: The item to process. """ + + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item + self.add_item(item, updating=True) + return self.update_item + if self.has_iattr(item.conf, ITEM_ATTR_CHAT_IDS): if self._chat_ids_item: self.logger.warning(f"Item: {item.property.path} declares chat_id for telegram plugin which are already defined, aborting!") @@ -360,7 +415,8 @@ def parse_item(self, item): if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"Register new command '{key}', add item '{item}' and register a handler") # add a handler for each info-attribute - self._application.add_handler(CommandHandler(key, self.cHandler_info_attr)) + self._cHandlers.append(CommandHandler(key, self.cHandler_info_attr)) + #self._application.add_handler(CommandHandler(key, self.cHandler_info_attr)) return self.update_item else: self.logger.error(f"Command '{key}' chosen for item '{item}' is invalid for telegram botfather") @@ -411,7 +467,8 @@ def parse_item(self, item): if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"Register new command '{key}', add item '{item}' and register a handler") # add a handler for each control-attribute - self._application.add_handler(CommandHandler(key, self.cHandler_control_attr)) + #self._application.add_handler(CommandHandler(key, self.cHandler_control_attr)) + self._cHandlers.append(CommandHandler(key, self.cHandler_control_attr)) return self.update_item return None @@ -433,6 +490,21 @@ def update_item(self, item, caller=None, source=None, dest=None): """ Called each time an item changed in SmartHomeNG """ + + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + + if not self.alive: + self.logger.info('Plugin is not alive, data will not be written') + return + if caller != self.get_fullname(): self.logger.info(f"update item: {item.property.path}") @@ -527,7 +599,7 @@ async def async_msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_ for cid in self.get_chat_id_list(chat_id): try: - response = await self._bot.send_message(chat_id=cid, text=msg, reply_markup=reply_markup, parse_mode=parse_mode) + response = await self._bot.sendMessage(chat_id=cid, text=msg, reply_markup=reply_markup, parse_mode=parse_mode) if response: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"Message sent:[{msg}] to Chat_ID:[{cid}] Bot:[{self._bot.bot}] response:[{response}]") @@ -630,7 +702,7 @@ def has_access_right(self, user_id): if user_id in self._chat_ids_item(): return True else: - self._bot.send_message(chat_id=user_id, text=self._no_access_msg) + self._bot.sendMessage(chat_id=user_id, text=self._no_access_msg) return False @@ -642,7 +714,7 @@ def has_write_access_right(self, user_id): if user_id in self._chat_ids_item(): return self._chat_ids_item()[user_id] else: - self._bot.send_message(chat_id=user_id, text=self._no_write_access_msg) + self._bot.sendMessage(chat_id=user_id, text=self._no_write_access_msg) return False @@ -685,7 +757,7 @@ def has_write_access_right(self, user_id): it contains the following objects: args - bot context.bot is the target for send_message() function + bot context.bot is the target for sendMessage() function bot_data chat_data dispatcher @@ -707,7 +779,7 @@ async def eHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> Just logs an error in case of a problem """ try: - self.logger.warning(f'Update {update} caused error {context.error}') + self.logger.error(f'Update {update} caused error {context.error}') except Exception: pass @@ -740,26 +812,26 @@ async def mHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> if dicCtl['type'] == 'onoff': item = dicCtl['item'] msg = f"{dicCtl['name']} \n change to:On(True)" - self._bot.sendMessage(chat_id=update.message.chat.id, text=msg) + await self._bot.sendMessage(chat_id=update.message.chat.id, text=msg) item(True) self._waitAnswer = None - self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) + await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) elif text == 'Off': if dicCtl['type'] == 'onoff': item = dicCtl['item'] msg = f"{dicCtl['name']} \n change to:Off(False)" - self._bot.sendMessage(chat_id=update.message.chat.id, text=msg) + await self._bot.sendMessage(chat_id=update.message.chat.id, text=msg) item(False) self._waitAnswer = None - self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) + await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) elif text == 'Yes': if self.scheduler_get('telegram_change_item_timeout'): self.scheduler_remove('telegram_change_item_timeout') dicCtlCopy = dicCtl.copy() dicCtlCopy['question'] = '' - self.change_item(update, context, dicCtlCopy['name'], dicCtlCopy) + await self.change_item(update, context, dicCtlCopy['name'], dicCtlCopy) self._waitAnswer = None - self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) + await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) elif dicCtl['type'] == 'num': if type(text) == int or float: if self.logger.isEnabledFor(logging.DEBUG): @@ -787,7 +859,7 @@ async def mHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> msg = f"{dicCtl['name']} \n out off range" await context.bot.sendMessage(chat_id=update.message.chat.id, text=msg) else: - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) self._waitAnswer = None except Exception as e: if self.logger.isEnabledFor(logging.DEBUG): @@ -800,7 +872,7 @@ async def cHandler_time(self, update: Update, context: ContextTypes.DEFAULT_TYPE if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"/time: return server time for update={update}, chat_id={update.message.chat.id} and context={dir(context)}") if self.has_access_right(update.message.chat.id): - await context.bot.send_message(chat_id=update.message.chat.id, text=str(datetime.datetime.now())) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=str(datetime.datetime.now())) async def cHandler_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -809,7 +881,7 @@ async def cHandler_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"/help: show available commands as keyboard for update={update}, chat_id={update.message.chat.id} and context={dir(context)}") if self.has_access_right(update.message.chat.id): - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("choose"), reply_markup={"keyboard": [["/hide","/start"], ["/time","/list"], ["/lo","/info"], ["/control", "/tr "]]}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("choose"), reply_markup={"keyboard": [["/hide","/start"], ["/time","/list"], ["/lo","/info"], ["/control", "/tr "]]}) async def cHandler_hide(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -819,7 +891,7 @@ async def cHandler_hide(self, update: Update, context: ContextTypes.DEFAULT_TYPE self.logger.debug(f"/hide: hide keyboard for bot={context.bot} and chat_id={update.message.chat.id}") if self.has_access_right(update.message.chat.id): hide_keyboard = {'hide_keyboard': True} - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("I'll hide the keyboard"), reply_markup=hide_keyboard) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("I'll hide the keyboard"), reply_markup=hide_keyboard) async def cHandler_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -828,8 +900,7 @@ async def cHandler_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"/list: show registered items and value for chat_id={update.message.chat.id}") if self.has_access_right(update.message.chat.id): - await context.bot.send_message(chat_id=update.message.chat.id, text=self.list_items()) - #self.list_items(update.message.chat.id) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items()) async def cHandler_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -839,9 +910,9 @@ async def cHandler_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE self.logger.debug(f"/info: show item-menu with registered items with specific attribute for chat_id={update.message.chat.id}") if self.has_access_right(update.message.chat.id): if len(self._items_info) > 0: - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Infos from the items:"), reply_markup={"keyboard": self.create_info_reply_markup()}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Infos from the items:"), reply_markup={"keyboard": self.create_info_reply_markup()}) else: - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_info!"), reply_markup={"keyboard": self.create_info_reply_markup()}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_info!"), reply_markup={"keyboard": self.create_info_reply_markup()}) async def cHandler_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -866,7 +937,7 @@ async def cHandler_start(self, update: Update, context: ContextTypes.DEFAULT_TYP else: self.logger.warning('No chat_ids defined') - await context.bot.send_message(chat_id=update.message.chat.id, text=text) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=text) async def cHandler_info_attr(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -882,7 +953,6 @@ async def cHandler_info_attr(self, update: Update, context: ContextTypes.DEFAULT if c_key in self._items_info: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"info-command: {c_key}") - #self.list_items_info(update.message.chat.id, c_key) await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items_info(c_key)) else: await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("unknown command %s") % c_key) @@ -901,7 +971,6 @@ async def cHandler_lo(self, update: Update, context: ContextTypes.DEFAULT_TYPE) for logic in sorted(self.logics.return_defined_logics()): # list with the names of all logics that are currently loaded data = [] info = self.logics.get_logic_info(logic) - # self.logger.debug(f"logic_info: {info}") if len(info) == 0 or not info['enabled']: data.append("disabled") if 'next_exec' in info: @@ -936,11 +1005,10 @@ async def cHandler_control(self, update: Update, context: ContextTypes.DEFAULT_T self.logger.debug(f"/control: show item-menu with registered items with specific attribute for chat_id={update.message.chat.id}") if self.has_write_access_right(update.message.chat.id): if len(self._items_control) > 0: - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) - await context.bot.send_message(chat_id=update.message.chat.id, text=self.list_items_control()) - #self.list_items_control(update.message.chat.id) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items_control()) else: - await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_control!"), reply_markup={"keyboard": self.create_control_reply_markup()}) + await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_control!"), reply_markup={"keyboard": self.create_control_reply_markup()}) async def cHandler_control_attr(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ @@ -1035,6 +1103,7 @@ def list_items_control(self): """ Show registered items and value with specific attribute ITEM_ATTR_CONTROL """ + text = "" for key, value in sorted(self._items_control.items()): # {'type':type,'item':item} item = value['item'] if item.type(): @@ -1135,16 +1204,14 @@ async def change_item(self, update, context, name, dicCtl): text = self.translate("no items found with the attribute %s") % ITEM_ATTR_CONTROL await context.bot.sendMessage(chat_id=chat_id, text=text) - async def telegram_change_item_timeout(self, **kwargs): + def telegram_change_item_timeout(self, **kwargs): update = None - context = None if 'update' in kwargs: update = kwargs['update'] - if 'context' in kwargs: - context = kwargs['context'] - if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug(f"Answer control_item timeout update:{update} context:{context}") - if self._waitAnswer is not None: - self._waitAnswer = None - # self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) - await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()}) + if self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"Answer control_item timeout update:{update}") + if self._waitAnswer is not None: + self._waitAnswer = None + self.msg_broadcast(msg=self.translate("Control/Change item-values:"), chat_id=update.message.chat.id, reply_markup={"keyboard": self.create_control_reply_markup()} ) + elif self.logger.isEnabledFor(logging.DEBUG): + self.logger.debug(f"telegram_change_item_timeout: update argument missing") diff --git a/telegram/plugin.yaml b/telegram/plugin.yaml index 09d9dfc1a..5bf572ecc 100755 --- a/telegram/plugin.yaml +++ b/telegram/plugin.yaml @@ -11,10 +11,10 @@ plugin: keywords: telegram chat messenger photo support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1548691-support-thread-für-das-telegram-plugin - version: 2.0.1 # Plugin version - sh_minversion: '1.9.5' # minimum shNG version to use this plugin + version: 2.0.3 # Plugin version + sh_minversion: '1.10' # 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_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: True # plugin supports multi instance restartable: True @@ -92,6 +92,14 @@ parameters: de: 'Telegram Threads zur leichteren Unterscheidung in der Thread Liste umbenennen' en: 'Rename Telegram threads for easier distinction in thread list' + pause_item: + type: str + default: '' + description: + de: 'Item, um die Ausführung des Plugins zu steuern' + en: 'item for controlling plugin execution' + + item_attributes: # Definition of item attributes defined by this plugin telegram_message: diff --git a/telegram/user_doc.rst b/telegram/user_doc.rst index e1b8890a8..25dfdd892 100755 --- a/telegram/user_doc.rst +++ b/telegram/user_doc.rst @@ -599,6 +599,12 @@ dargestellt und die entsprechenden Aktionen ausgeführt. if msg != '': telegram_plugin.msg_broadcast(msg, message_chat_id, reply_markup, parse_mode) +Changelog +--------- +V2.0.3 Plugin mit stop/run/pause_item steuerbar +V2.0.2 Fehler beim Kommando ``/control`` behoben +V2.0.0 Umbau auf neues Telegram Paket (V20.2+) mit async + Web Interface ============= From 8b847c3803e4121a7b44efe14867263a01e59773 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 7 Jul 2024 12:33:06 +0200 Subject: [PATCH 012/121] denon plugin: code fix --- denon/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/denon/__init__.py b/denon/__init__.py index 6ace0cb1c..633c4c258 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -109,7 +109,9 @@ def _set_device_defaults(self): # the sent command. Getting it as return value would assign it to the wrong # command and discard it... so break the "return result"-chain and don't # return anything - def _send(self, data_dict, resend_info={}): + def _send(self, data_dict, resend_info=None): + if resend_info is None: + resend_info = {} if resend_info.get('returnvalue') is not None: self._sending.update({resend_info.get('command'): resend_info}) return self._connection.send(data_dict) From 45c1352fd54026b566f22365dd65ef183c0d2ad9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 11 Aug 2024 21:49:16 +0200 Subject: [PATCH 013/121] denon plugin: add resume_initial_read and protocol (for resend) --- denon/plugin.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index dd8670950..513e1eeaa 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -139,6 +139,16 @@ parameters: de: Klasse für Verarbeitung von Kommandos en: class for command processing + protocol: + type: str + default: '' + valid list: + - '' + - 'resend' + description: + de: 'Protokoll' + en: 'Protocol' + autoreconnect: type: bool default: true @@ -210,6 +220,13 @@ parameters: de: Warte nach Verbindungsaufbau mit dem Abfragen von Werten en: Wait after connection with querying values + resume_initial_read: + type: bool + defaul: false + description: + de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' + en: 'Repeat initial read on resume' + connect_cycle: type: num default: 3 From 83dd11fa624714c9c07476d8f9a54422e8bcd76e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 11 Aug 2024 22:15:26 +0200 Subject: [PATCH 014/121] denon plugin: revert resend changes as it is now included as a protocol on a global level --- denon/__init__.py | 70 ----------------------------------------------- 1 file changed, 70 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index 633c4c258..414052ef9 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -26,8 +26,6 @@ import sys import threading import time -import datetime -from lib.shtime import Shtime if __name__ == '__main__': builtins.SDP_standalone = True @@ -60,37 +58,12 @@ class denon(SmartDevicePlugin): PLUGIN_VERSION = '1.0.1' def on_connect(self, by=None): - self.logger.debug(f"Connected.. Cycle for retry: {self._sendretry_cycle}") - if self._send_retries >= 1: - self.scheduler_add('resend', self._resend, cycle=self._sendretry_cycle) self.logger.debug("Checking for custom input names.") self.send_command('general.custom_inputnames') - if self.scheduler_get('read_initial_values'): - return - elif self._initial_value_read_delay > 0: - self.logger.dbghigh(f"On connect reading initial values after {self._initial_value_read_delay} seconds.") - self.scheduler_add('read_initial_values', self._read_initial_values, value={'force': True}, next=self.shtime.now() + datetime.timedelta(seconds=self._initial_value_read_delay)) - else: - self._read_initial_values(True) - - def _on_suspend(self): - for scheduler in self.scheduler_get_all(): - self.scheduler_remove(scheduler) - - def _on_resume(self): - if self.scheduler_get('resend'): - self.scheduler_remove('resend') - self.logger.debug(f"Resuming.. Cycle for retry: {self._sendretry_cycle}") - if self._send_retries >= 1: - self.scheduler_add('resend', self._resend, cycle=self._sendretry_cycle) def _set_device_defaults(self): self._use_callbacks = True self._custom_inputnames = {} - self._sending = {} - self._sending_lock = threading.Lock() - self._send_retries = self.get_parameter_value('send_retries') - self._sendretry_cycle = int(self.get_parameter_value('sendretry_cycle')) # set our own preferences concerning connections if PLUGIN_ATTR_NET_HOST in self._parameters and self._parameters[PLUGIN_ATTR_NET_HOST]: @@ -105,38 +78,6 @@ def _set_device_defaults(self): b = b.decode('unicode-escape').encode() self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] = b - # we need to receive data via callback, as the "reply" can be unrelated to - # the sent command. Getting it as return value would assign it to the wrong - # command and discard it... so break the "return result"-chain and don't - # return anything - def _send(self, data_dict, resend_info=None): - if resend_info is None: - resend_info = {} - if resend_info.get('returnvalue') is not None: - self._sending.update({resend_info.get('command'): resend_info}) - return self._connection.send(data_dict) - - def _resend(self): - if not self.alive or self.suspended: - return - self._sending_lock.acquire(True, 2) - _remove_commands = [] - for command in list(self._sending.keys()): - _retry = self._sending[command].get("retry") or 0 - _sent = True - if _retry is not None and _retry < self._send_retries: - self.logger.debug(f'Re-sending {command}, retry {_retry}.') - _sent = self.send_command(command, self._sending[command].get("returnvalue"), return_result=True, retry=_retry + 1) - elif _retry is not None and _retry >= self._send_retries: - _sent = False - if _sent is False: - _remove_commands.append(command) - self.logger.info(f"Giving up re-sending {command} after {_retry} retries.") - for command in _remove_commands: - self._sending.pop(command) - if self._sending_lock.locked(): - self._sending_lock.release() - def _transform_send_data(self, data=None, **kwargs): if isinstance(data, dict): data['limit_response'] = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] @@ -206,17 +147,6 @@ def on_data_received(self, by, data, command=None): self.logger.warning(f'received data "{data}" for command {command}, error {e} occurred while converting. Discarding data.') else: self.logger.debug(f'received data "{data}" for command {command} converted to value {value}.') - if command in self._sending: - self._sending_lock.acquire(True, 2) - _retry = self._sending[command].get("retry") or 0 - _compare = self._sending[command].get('returnvalue') - if self._sending[command].get('returntype')(value) == _compare: - self._sending.pop(command) - self.logger.debug(f'Correct answer for {command}, removing from send. Sending {self._sending}') - elif _retry is not None and _retry <= self._send_retries: - self.logger.debug(f'Should send again {self._sending}...') - if self._sending_lock.locked(): - self._sending_lock.release() self._dispatch_callback(command, value, by) self._process_additional_data(base_command, data, value, custom, by) From d3afef8dc41b1454e6beec522eae4d461522f681 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 11 Aug 2024 22:15:58 +0200 Subject: [PATCH 015/121] denon plugin: remove reading of volumemax from structs (it is not available) --- denon/plugin.yaml | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 513e1eeaa..f817fd492 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -226,7 +226,7 @@ parameters: description: de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' en: 'Repeat initial read on resume' - + connect_cycle: type: num default: 3 @@ -880,9 +880,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - zone1 - - zone1.control input: type: str @@ -2104,10 +2101,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - ALL - - ALL.zone1 - - ALL.zone1.control input: type: str @@ -3195,10 +3188,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.zone1 - - AVR-X6300H.zone1.control input: type: str @@ -4401,10 +4390,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X4300H - - AVR-X4300H.zone1 - - AVR-X4300H.zone1.control input: type: str @@ -5662,10 +5647,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X3300W - - AVR-X3300W.zone1 - - AVR-X3300W.zone1.control input: type: str @@ -6689,10 +6670,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X2300W - - AVR-X2300W.zone1 - - AVR-X2300W.zone1.control input: type: str @@ -7648,10 +7625,6 @@ item_structs: denon_command@instance: zone1.control.volumemax denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X1300W - - AVR-X1300W.zone1 - - AVR-X1300W.zone1.control input: type: str From bc0633e0583072235cb5b398b4f1f3881cfc40aa Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 11 Aug 2024 22:20:15 +0200 Subject: [PATCH 016/121] denon plugin: remove threading import --- denon/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/denon/__init__.py b/denon/__init__.py index 414052ef9..86ebafaa0 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -24,7 +24,6 @@ import builtins import os import sys -import threading import time if __name__ == '__main__': From a3a7fe8e1516ecb6dccb89201a0ba5c0ede181a5 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 11 Aug 2024 22:29:51 +0200 Subject: [PATCH 017/121] oppo plugin: remove _send method, not needed --- oppo/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index f09637415..2e9dfda8a 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -75,10 +75,6 @@ def on_connect(self, by=None): self.logger.debug(f"Activating verbose mode {verbose} after connection.") self.send_command('general.verbose', verbose) - def _send(self, data_dict): - self.logger.debug(f"Sending data_dict {data_dict}") - self._connection.send(data_dict) - def _transform_send_data(self, data=None, **kwargs): if isinstance(data, dict): data['limit_response'] = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] From 4595ec81c59e0f30cc9871df7f21077fb0321192 Mon Sep 17 00:00:00 2001 From: onkelandy Date: Fri, 16 Aug 2024 16:30:42 +0200 Subject: [PATCH 018/121] oppo plugin: set command class automatically, remove some unneccessary entries from plugin.yaml --- oppo/__init__.py | 4 +++- oppo/plugin.yaml | 30 ------------------------------ 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index 2e9dfda8a..70f3d0d05 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -39,7 +39,7 @@ class SmartPluginWebIf(): else: builtins.SDP_standalone = False -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) +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, PLUGIN_ATTR_CMD_CLASS) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone CUSTOM_INPUT_NAME_COMMAND = 'custom_inputnames' @@ -63,6 +63,8 @@ def _set_device_defaults(self): self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_NET_TCP_CLI elif PLUGIN_ATTR_SERIAL_PORT in self._parameters and self._parameters[PLUGIN_ATTR_SERIAL_PORT]: self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_SER_ASYNC + + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() b = b.decode('unicode-escape').encode() diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index 1210475d2..038121d2a 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -39,36 +39,6 @@ parameters: de: Serieller Anschluss (z.B. /dev/ttyUSB0 oder COM1) en: serial port (e.g. /dev/ttyUSB0 or COM1) - conn_type: - type: str - mandatory: false - valid_list: - - '' - - net_tcp_request - - net_tcp_client - - net_tcp_jsonrpc - - net_udp_server - - serial - - serial_async - - description: - de: Verbindungstyp - en: connection type - - command_class: - type: str - default: SDPCommandParseStr - valid_list: - - SDPCommand - - SDPCommandStr - - SDPCommandParseStr - - SDPCommandJSON - - SDPCommandViessmann - - description: - de: Klasse für Verarbeitung von Kommandos - en: class for command processing - model: type: str mandatory: false From 3d82491d073e5eb1cfdad8c17a5063480046374e Mon Sep 17 00:00:00 2001 From: onkelandy Date: Fri, 16 Aug 2024 16:31:10 +0200 Subject: [PATCH 019/121] oppo plugin: add parameters to plugin.yaml --- oppo/plugin.yaml | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index 038121d2a..2cffe5b1a 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -168,6 +168,70 @@ parameters: de: Item-Pfad für das Standby-Item en: item path for standby switch item + protocol: + type: str + default: '' + valid list: + - '' + - 'resend' + description: + de: 'Protokoll' + en: 'Protocol' + + send_retries: + type: num + default: 0 + + description: + de: Anzahl Sendeversuche + en: number of sending retries + description_long: + de: 'Anzahl Sendeversuche\n + Kommt keine (passende) Antwort nach dem Senden + eines Commands zurück, wird das Kommando nochmals + gesendet, sofern der Wert über 0 liegt. + ' + en: 'number of sending retries\n + If no (suiting) answer is received after sending + a command the command is resent as long as this + value is more than 0. + ' + + sendretry_cycle: + type: num + valid_min: 1 + default: 1 + + description: + de: Pause zwischen Durchgängen von Sendeversuchen + en: wait time between sending retry rounds + description_long: + de: 'Pause zwischen Durchgängen von Sendeversuchen\n + Sind Send Retries aktiv, wird ein Scheduler erstellt, + der im angegebenen Sekundentakt Kommandos erneut sendet, + zu denen keine (passenden) Antworten erhalten wurden. + ' + en: 'wait time between sending retry rounds\n + If send retries are active, a scheduler gets added + that resends commands in the given cycle value (in seconds) + where no (suiting) answer got received. + ' + + delay_initial_read: + type: num + default: 0 + + description: + de: Warte nach Verbindungsaufbau mit dem Abfragen von Werten + en: Wait after connection with querying values + + resume_initial_read: + type: bool + defaul: false + description: + de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' + en: 'Repeat initial read on resume' + item_attributes: From a7e54e6b172b5c62d5b0c6a33b98d4e5f772a696 Mon Sep 17 00:00:00 2001 From: onkelandy Date: Fri, 16 Aug 2024 16:33:53 +0200 Subject: [PATCH 020/121] oppo plugin: add min_sdp_version and bump v to 1.0.1 --- oppo/__init__.py | 2 +- oppo/plugin.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index 70f3d0d05..3c3ad7645 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -54,7 +54,7 @@ class oppo(SmartDevicePlugin): The know-how is in the commands.py (and some DT_ classes...) """ - PLUGIN_VERSION = '1.0.0' + PLUGIN_VERSION = '1.0.1' def _set_device_defaults(self): diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index 2cffe5b1a..c38208d18 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -6,9 +6,10 @@ plugin: tester: Morg state: develop keywords: iot device - version: 1.0.0 + version: 1.0.1 sh_minversion: '1.9.5.1' py_minversion: '3.6' + MIN_SDP_VERSION: '1.0.3' multi_instance: false restartable: true classname: oppo From 2615ea60143e40e7311bc2fc2336a821fc221d19 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 09:39:18 +0200 Subject: [PATCH 021/121] oppo plugin: fix metadata, remove protocol from plugin.yaml --- oppo/__init__.py | 3 ++- oppo/plugin.yaml | 13 +------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index 3c3ad7645..7f2c90978 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -55,6 +55,7 @@ class oppo(SmartDevicePlugin): """ PLUGIN_VERSION = '1.0.1' + MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): @@ -63,7 +64,7 @@ def _set_device_defaults(self): self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_NET_TCP_CLI elif PLUGIN_ATTR_SERIAL_PORT in self._parameters and self._parameters[PLUGIN_ATTR_SERIAL_PORT]: self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_SER_ASYNC - + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index c38208d18..534790b05 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -9,7 +9,6 @@ plugin: version: 1.0.1 sh_minversion: '1.9.5.1' py_minversion: '3.6' - MIN_SDP_VERSION: '1.0.3' multi_instance: false restartable: true classname: oppo @@ -169,16 +168,6 @@ parameters: de: Item-Pfad für das Standby-Item en: item path for standby switch item - protocol: - type: str - default: '' - valid list: - - '' - - 'resend' - description: - de: 'Protokoll' - en: 'Protocol' - send_retries: type: num default: 0 @@ -232,7 +221,7 @@ parameters: description: de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' en: 'Repeat initial read on resume' - + item_attributes: From 4344be17d73c54c2eda3d2c65ec1ad282d36a0f3 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 10:34:56 +0200 Subject: [PATCH 022/121] oppo plugin: fix command class --- oppo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index 7f2c90978..be01e4fe9 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -65,7 +65,7 @@ def _set_device_defaults(self): elif PLUGIN_ATTR_SERIAL_PORT in self._parameters and self._parameters[PLUGIN_ATTR_SERIAL_PORT]: self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_SER_ASYNC - self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr + self._parameters[PLUGIN_ATTR_CMD_CLASS] = "SDPCommandParseStr" b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() b = b.decode('unicode-escape').encode() From 9befb9ff79e71df29844e502de203895d9b2d5c1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 10:40:23 +0200 Subject: [PATCH 023/121] denon plugin: remove protocol and command_class from plugin.yaml --- denon/plugin.yaml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index f817fd492..4807df68d 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -128,27 +128,6 @@ parameters: de: Verbindungstyp en: connection type - command_class: - type: str - default: SDPCommandParseStr - valid_list: - - SDPCommand - - SDPCommandParseStr - - description: - de: Klasse für Verarbeitung von Kommandos - en: class for command processing - - protocol: - type: str - default: '' - valid list: - - '' - - 'resend' - description: - de: 'Protokoll' - en: 'Protocol' - autoreconnect: type: bool default: true From 626deafe506bbd46578c1df4e1b78abc0f03dee6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 10:50:14 +0200 Subject: [PATCH 024/121] denon plugin: auto set command class --- denon/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/denon/__init__.py b/denon/__init__.py index 86ebafaa0..ae1cfe675 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -41,8 +41,9 @@ class SmartPluginWebIf(): else: builtins.SDP_standalone = False -from lib.model.sdp.globals import (PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_CONN_TERMINATOR, CONN_NULL, CONN_NET_TCP_CLI, CONN_SER_ASYNC) +from lib.model.sdp.globals import (PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_CMD_CLASS, CONN_NULL, CONN_NET_TCP_CLI, CONN_SER_ASYNC) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone +from lib.model.sdp.command import SDPCommandParseStr # from .webif import WebInterface @@ -73,6 +74,8 @@ def _set_device_defaults(self): self.logger.error('Neither host nor serialport set, connection not possible. Using dummy connection, plugin will not work') self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_NULL + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr + b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() b = b.decode('unicode-escape').encode() self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] = b From 0b38f23a2ac64ac3965aa255f5efe86c27b725b4 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 10:51:00 +0200 Subject: [PATCH 025/121] oppo plugin: fix command class --- oppo/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index be01e4fe9..1ff3d8324 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -41,6 +41,7 @@ class SmartPluginWebIf(): builtins.SDP_standalone = False 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, PLUGIN_ATTR_CMD_CLASS) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone +from lib.model.sdp.command import SDPCommandParseStr CUSTOM_INPUT_NAME_COMMAND = 'custom_inputnames' @@ -65,7 +66,7 @@ def _set_device_defaults(self): elif PLUGIN_ATTR_SERIAL_PORT in self._parameters and self._parameters[PLUGIN_ATTR_SERIAL_PORT]: self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_SER_ASYNC - self._parameters[PLUGIN_ATTR_CMD_CLASS] = "SDPCommandParseStr" + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() b = b.decode('unicode-escape').encode() From 2866381fafc43e64fd7fe844a10e7aeca39cd07d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 11:09:01 +0200 Subject: [PATCH 026/121] pioneer plugin: set command class automatically --- pioneer/__init__.py | 4 ++++ pioneer/plugin.yaml | 11 ----------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index ac9d4aaac..bfe2fb5ad 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -41,6 +41,7 @@ class SmartPluginWebIf(): PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_MODEL, CONN_NET_TCP_CLI, CONN_SER_ASYNC, CONN_NULL) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone +from lib.model.sdp.command import SDPCommandParseStr # from .webif import WebInterface @@ -51,6 +52,7 @@ class pioneer(SmartDevicePlugin): """ Device class for Pioneer AV function. """ PLUGIN_VERSION = '1.0.2' + MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): # set our own preferences concerning connections @@ -62,6 +64,8 @@ def _set_device_defaults(self): self.logger.error('Neither host nor serialport set, connection not possible. Using dummy connection, plugin will not work') self._parameters[PLUGIN_ATTR_CONNECTION] = CONN_NULL + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr + b = self._parameters[PLUGIN_ATTR_CONN_TERMINATOR].encode() b = b.decode('unicode-escape').encode() self._parameters[PLUGIN_ATTR_CONN_TERMINATOR] = b diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index f6904e8ed..7dde20c88 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -130,17 +130,6 @@ parameters: de: Verbindungstyp en: connection type - command_class: - type: str - default: SDPCommandParseStr - valid_list: - - SDPCommand - - SDPCommandParseStr - - description: - de: Klasse für Verarbeitung von Kommandos - en: class for command processing - autoreconnect: type: bool default: true From 95131f5c07322f16445a650fe907adda3b32e172 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 11:09:26 +0200 Subject: [PATCH 027/121] pioneer plugin: add resend and initial_read parameters --- pioneer/plugin.yaml | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index 7dde20c88..c054a826d 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -118,6 +118,60 @@ parameters: de: Anzahl Stopbits en: number of stop bits + send_retries: + type: num + default: 0 + + description: + de: Anzahl Sendeversuche + en: number of sending retries + description_long: + de: 'Anzahl Sendeversuche\n + Kommt keine (passende) Antwort nach dem Senden + eines Commands zurück, wird das Kommando nochmals + gesendet, sofern der Wert über 0 liegt. + ' + en: 'number of sending retries\n + If no (suiting) answer is received after sending + a command the command is resent as long as this + value is more than 0. + ' + + sendretry_cycle: + type: num + valid_min: 1 + default: 1 + + description: + de: Pause zwischen Durchgängen von Sendeversuchen + en: wait time between sending retry rounds + description_long: + de: 'Pause zwischen Durchgängen von Sendeversuchen\n + Sind Send Retries aktiv, wird ein Scheduler erstellt, + der im angegebenen Sekundentakt Kommandos erneut sendet, + zu denen keine (passenden) Antworten erhalten wurden. + ' + en: 'wait time between sending retry rounds\n + If send retries are active, a scheduler gets added + that resends commands in the given cycle value (in seconds) + where no (suiting) answer got received. + ' + + delay_initial_read: + type: num + default: 0 + + description: + de: Warte nach Verbindungsaufbau mit dem Abfragen von Werten + en: Wait after connection with querying values + + resume_initial_read: + type: bool + defaul: false + description: + de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' + en: 'Repeat initial read on resume' + conn_type: type: str mandatory: false From e052546035e3c7ce61bee6d1f9ff0ce82e856722 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 11:12:03 +0200 Subject: [PATCH 028/121] pioneer plugin: dump version to 1.0.3 --- pioneer/__init__.py | 2 +- pioneer/plugin.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index bfe2fb5ad..842a19ef7 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -51,7 +51,7 @@ class SmartPluginWebIf(): class pioneer(SmartDevicePlugin): """ Device class for Pioneer AV function. """ - PLUGIN_VERSION = '1.0.2' + PLUGIN_VERSION = '1.0.3' MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index c054a826d..65c47494f 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -6,7 +6,7 @@ plugin: tester: Morg state: develop keywords: iot device av pioneer sdp - version: 1.0.2 + version: 1.0.3 sh_minversion: '1.9.5' py_minversion: '3.7' multi_instance: false From 684a60f7cf6501b0d037badeba213c9b5de4058e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 11:19:12 +0200 Subject: [PATCH 029/121] lms plugin: autoset command class and connection --- lms/__init__.py | 9 +++++++-- lms/plugin.yaml | 24 +----------------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/lms/__init__.py b/lms/__init__.py index df32aad30..d31250d00 100755 --- a/lms/__init__.py +++ b/lms/__init__.py @@ -40,8 +40,9 @@ class SmartPluginWebIf(): else: builtins.SDP_standalone = False -from lib.model.sdp.globals import (CUSTOM_SEP, PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_RECURSIVE, PLUGIN_ATTR_CONN_TERMINATOR) +from lib.model.sdp.globals import (CUSTOM_SEP, PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_RECURSIVE, PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CONN_TERMINATOR) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone +from lib.model.sdp.command import SDPCommandParseStr import urllib.parse @@ -49,7 +50,8 @@ class SmartPluginWebIf(): class lms(SmartDevicePlugin): """ Device class for Logitech Mediaserver/Squeezebox function. """ - PLUGIN_VERSION = '1.5.2' + PLUGIN_VERSION = '1.5.3' + MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): self.custom_commands = 1 @@ -58,6 +60,9 @@ def _set_device_defaults(self): self._custom_patterns = {1: '(?:[0-9a-fA-F]{2}[-:]){5}[0-9a-fA-F]{2}', 2: '', 3: ''} self._use_callbacks = True self._parameters[PLUGIN_ATTR_RECURSIVE] = 1 + self._parameters[PLUGIN_ATTR_CMD_CLASS] = SDPCommandParseStr + self._parameters[PLUGIN_ATTR_CONNECTION] = 'net_tcp_client' + self._parameters['web_port'] = self.get_parameter_value('web_port') if self.get_parameter_value('web_host') == '': host = self._parameters.get(PLUGIN_ATTR_NET_HOST) diff --git a/lms/plugin.yaml b/lms/plugin.yaml index ae6004f07..9c0ad6fa5 100755 --- a/lms/plugin.yaml +++ b/lms/plugin.yaml @@ -6,7 +6,7 @@ plugin: tester: Morg state: develop keywords: iot device logitechmediaserver lms sdp av - version: 1.5.2 + version: 1.5.3 sh_minversion: '1.9.5' py_minversion: '3.7' multi_instance: false @@ -71,28 +71,6 @@ parameters: de: Port für CLI Netzwerkverbindung en: CLI network port - conn_type: - type: str - default: net_tcp_client - valid_list: - - '' - - net_tcp_client - - description: - de: Verbindungstyp - en: connection type - - command_class: - type: str - default: SDPCommandParseStr - valid_list: - - SDPCommand - - SDPCommandParseStr - - description: - de: Klasse für Verarbeitung von Kommandos - en: class for command processing - autoreconnect: type: bool default: true From 0e82799227820291327bdc72eb4a26dd73ef3487 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 11:20:16 +0200 Subject: [PATCH 030/121] lms plugin: add resend and initial read parameters --- lms/plugin.yaml | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/lms/plugin.yaml b/lms/plugin.yaml index 9c0ad6fa5..6961aa37c 100755 --- a/lms/plugin.yaml +++ b/lms/plugin.yaml @@ -127,6 +127,60 @@ parameters: de: Item-Pfad für das Standby-Item en: item path for standby switch item + send_retries: + type: num + default: 0 + + description: + de: Anzahl Sendeversuche + en: number of sending retries + description_long: + de: 'Anzahl Sendeversuche\n + Kommt keine (passende) Antwort nach dem Senden + eines Commands zurück, wird das Kommando nochmals + gesendet, sofern der Wert über 0 liegt. + ' + en: 'number of sending retries\n + If no (suiting) answer is received after sending + a command the command is resent as long as this + value is more than 0. + ' + + sendretry_cycle: + type: num + valid_min: 1 + default: 1 + + description: + de: Pause zwischen Durchgängen von Sendeversuchen + en: wait time between sending retry rounds + description_long: + de: 'Pause zwischen Durchgängen von Sendeversuchen\n + Sind Send Retries aktiv, wird ein Scheduler erstellt, + der im angegebenen Sekundentakt Kommandos erneut sendet, + zu denen keine (passenden) Antworten erhalten wurden. + ' + en: 'wait time between sending retry rounds\n + If send retries are active, a scheduler gets added + that resends commands in the given cycle value (in seconds) + where no (suiting) answer got received. + ' + + delay_initial_read: + type: num + default: 0 + + description: + de: Warte nach Verbindungsaufbau mit dem Abfragen von Werten + en: Wait after connection with querying values + + resume_initial_read: + type: bool + defaul: false + description: + de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' + en: 'Repeat initial read on resume' + item_attributes: sqb_command: From 0094a8ec2e631859c08817f5a5dc9b1266232269 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:14:55 +0200 Subject: [PATCH 031/121] kodi: adjust to current sdp version --- kodi/__init__.py | 2 +- kodi/plugin.yaml | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/kodi/__init__.py b/kodi/__init__.py index 70a3489b9..6d654583b 100644 --- a/kodi/__init__.py +++ b/kodi/__init__.py @@ -81,7 +81,7 @@ class kodi(SmartDevicePlugin): another place, in ``commands.py`` and/or the item configuration. """ - PLUGIN_VERSION = '1.7.2' + PLUGIN_VERSION = '1.7.3' def _set_device_defaults(self): self._use_callbacks = True diff --git a/kodi/plugin.yaml b/kodi/plugin.yaml index 48bcc26e5..13266def9 100644 --- a/kodi/plugin.yaml +++ b/kodi/plugin.yaml @@ -6,9 +6,10 @@ plugin: tester: OnkelAndy state: develop keywords: iot device mediacenter kodi xmbc sdp - version: 1.7.2 + version: 1.7.3 sh_minversion: '1.9.5' py_minversion: '3.7' + sdp_minversion: '1.0.3' multi_instance: true restartable: true classname: kodi @@ -31,6 +32,20 @@ parameters: de: Item-Pfad für das Suspend-Item en: item path for suspend switch item + delay_initial_read: + type: num + default: 10 + description: + de: 'Verzögerung für das erstmalige Lesen beim Start (in Sekunden)' + en: 'delay for initial command read on start (in seconds)' + + resume_initial_read: + type: bool + defaul: true + description: + de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' + en: 'Repeat initial read on resume' + timeout: type: num default: 3 From 75cd282afca083065f1a05c7ecadc7c8fbc7c76e Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:22:17 +0200 Subject: [PATCH 032/121] add sdp metadata --- oppo/plugin.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index 534790b05..c4f91700e 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -9,6 +9,7 @@ plugin: version: 1.0.1 sh_minversion: '1.9.5.1' py_minversion: '3.6' + sdp_minversion: '1.0.3' multi_instance: false restartable: true classname: oppo From a0a3120d23fa9d11391f8ca94f97a156823136a7 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:23:09 +0200 Subject: [PATCH 033/121] adjust sdp metadata --- oppo/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oppo/__init__.py b/oppo/__init__.py index 1ff3d8324..ee269375d 100755 --- a/oppo/__init__.py +++ b/oppo/__init__.py @@ -56,7 +56,6 @@ class oppo(SmartDevicePlugin): """ PLUGIN_VERSION = '1.0.1' - MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): From 7649398dcdd86d3f2730a59e20233a97f7d51f6d Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:23:57 +0200 Subject: [PATCH 034/121] add sdp metadata --- pioneer/plugin.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index 65c47494f..4e97f4a67 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -9,6 +9,7 @@ plugin: version: 1.0.3 sh_minversion: '1.9.5' py_minversion: '3.7' + sdp_minversion: '1.0.3' multi_instance: false restartable: true classname: pioneer From fe45fd83bf5dc81d20bdbf8a2261d575444ccbd5 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:24:18 +0200 Subject: [PATCH 035/121] adjust sdp metadata --- pioneer/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index 842a19ef7..7cd491eec 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -52,8 +52,7 @@ class pioneer(SmartDevicePlugin): """ Device class for Pioneer AV function. """ PLUGIN_VERSION = '1.0.3' - MIN_SDP_VERSION = '1.0.3' - + def _set_device_defaults(self): # set our own preferences concerning connections if PLUGIN_ATTR_NET_HOST in self._parameters and self._parameters[PLUGIN_ATTR_NET_HOST]: From 2a3d48852a86491ccf227f454d376624c7b4b4ea Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:24:54 +0200 Subject: [PATCH 036/121] adjust sdp metadata --- lms/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/__init__.py b/lms/__init__.py index d31250d00..8cdc17b36 100755 --- a/lms/__init__.py +++ b/lms/__init__.py @@ -51,7 +51,6 @@ class lms(SmartDevicePlugin): """ Device class for Logitech Mediaserver/Squeezebox function. """ PLUGIN_VERSION = '1.5.3' - MIN_SDP_VERSION = '1.0.3' def _set_device_defaults(self): self.custom_commands = 1 From 55f0cd13dcf4bd11d9fbd69b56c0140dc0c19115 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:25:13 +0200 Subject: [PATCH 037/121] add sdp metadata --- lms/plugin.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/plugin.yaml b/lms/plugin.yaml index 6961aa37c..1c588d8a1 100755 --- a/lms/plugin.yaml +++ b/lms/plugin.yaml @@ -9,6 +9,7 @@ plugin: version: 1.5.3 sh_minversion: '1.9.5' py_minversion: '3.7' + sdp_minversion: '1.0.3' multi_instance: false restartable: true classname: lms From d52df4cdf71f7162b588996961477d3a67914cdd Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 17 Aug 2024 12:25:59 +0200 Subject: [PATCH 038/121] add sdp metadata --- denon/plugin.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 4807df68d..8ec1c8bbe 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -9,6 +9,7 @@ plugin: version: 1.0.1 sh_minversion: 1.9.5 py_minversion: 3.7 + sdp_minversion: '1.0.3' multi_instance: false restartable: true classname: denon From 4925eda0a13274cfeb599d8da261a639a3f7de6b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 20:38:21 +0200 Subject: [PATCH 039/121] denon plugin: move custom inputnames to dataype --- denon/__init__.py | 54 ---------------------------------------------- denon/commands.py | 2 +- denon/datatypes.py | 11 ++++++++++ 3 files changed, 12 insertions(+), 55 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index ae1cfe675..04f83d4c0 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -105,60 +105,6 @@ def _process_additional_data(self, command, data, value, custom, by): self.send_command(f'zone{zone}.control.volume') self.send_command(f'zone{zone}.control.listeningmode') - def on_data_received(self, by, data, command=None): - - commands = None - if command is not None: - self.logger.debug(f'received data "{data}" from {by} for command {command}') - commands = [command] - else: - # command == None means that we got raw data from a callback and - # don't know yet to which command this belongs to. So find out... - self.logger.debug(f'received data "{data}" from {by} without command specification') - - # command can be a string (classic single command) or - # - new - a list of strings if multiple commands are identified - # in that case, work on all strings - commands = self._commands.get_commands_from_reply(data) - if not commands: - if self._discard_unknown_command: - self.logger.debug(f'data "{data}" did not identify a known command, ignoring it') - return - else: - self.logger.debug(f'data "{data}" did not identify a known command, forwarding it anyway for {self._unknown_command}') - self._dispatch_callback(self._unknown_command, data, by) - - # TODO: remove later? - assert(isinstance(commands, list)) - - # process all commands - for command in commands: - self._check_for_custominputs(command, data) - custom = None - if self.custom_commands: - custom = self._get_custom_value(command, data) - - base_command = command - value = None - try: - if CUSTOM_INPUT_NAME_COMMAND in command: - value = self._custom_inputnames - else: - value = self._commands.get_shng_data(command, data) - except OSError as e: - self.logger.warning(f'received data "{data}" for command {command}, error {e} occurred while converting. Discarding data.') - else: - self.logger.debug(f'received data "{data}" for command {command} converted to value {value}.') - self._dispatch_callback(command, value, by) - - self._process_additional_data(base_command, data, value, custom, by) - - def _check_for_custominputs(self, command, data): - if CUSTOM_INPUT_NAME_COMMAND in command and isinstance(data, str): - tmp = data.split(' ', 1) - src = tmp[0][5:] - name = tmp[1] - self._custom_inputnames[src] = name if __name__ == '__main__': s = Standalone(denon, sys.argv[0]) diff --git a/denon/commands.py b/denon/commands.py index d256af4ac..20900f8c8 100755 --- a/denon/commands.py +++ b/denon/commands.py @@ -61,7 +61,7 @@ 'region': {'read': True, 'write': False, 'read_cmd': 'SYMODTUN ?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'SYMODTUN\s(.*)', 'item_attrs': {'initial': True}}, }, 'general': { - 'custom_inputnames': {'read': True, 'write': False, 'read_cmd': 'SSFUN ?', 'item_type': 'dict', 'dev_datatype': 'str', 'reply_pattern': 'SSFUN(.*)', 'item_attrs': {'item_template': 'custom_inputnames'}}, + 'custom_inputnames': {'read': True, 'write': False, 'read_cmd': 'SSFUN ?', 'item_type': 'dict', 'dev_datatype': 'DenonCustominput', 'reply_pattern': 'SSFUN(.*)', 'item_attrs': {'item_template': 'custom_inputnames'}}, 'power': {'read': True, 'write': True, 'read_cmd': 'PW?', 'write_cmd': 'PW{VALUE}', 'item_type': 'bool', 'dev_datatype': 'str', 'reply_pattern': 'PW{LOOKUP}', 'lookup': 'POWER'}, 'setupmenu': {'read': True, 'write': True, 'read_cmd': 'MNMEN?', 'write_cmd': 'MNMEN {VALUE}', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': 'MNMEN (ON|OFF)'}, 'display': {'read': True, 'write': False, 'read_cmd': 'NSE', 'item_type': 'str', 'dev_datatype': 'DenonDisplay', 'reply_pattern': 'NSE(.*)'}, diff --git a/denon/datatypes.py b/denon/datatypes.py index 11dbe3490..f56d5a228 100755 --- a/denon/datatypes.py +++ b/denon/datatypes.py @@ -20,6 +20,17 @@ def get_shng_data(self, data, type=None, **kwargs): return None +# read only. Creating dict with custom inputnames +class DT_DenonCustominput(DT.Datatype): + def __init__(self, fail_silent=False): + super().__init__(fail_silent) + self._custom_inputnames = {} + + def get_shng_data(self, data, type=None, **kwargs): + tmp = data.split(' ', 1) + self._custom_inputnames[tmp[0]] = tmp[1] + return self._custom_inputnames + # handle pseudo-decimal values without decimal point class DT_DenonVol(DT.Datatype): def get_send_data(self, data, **kwargs): From eeca3490dbc18171017182b6f35ec15595b6b2d8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 20:40:45 +0200 Subject: [PATCH 040/121] denon plugin: update plugin.yaml --- denon/plugin.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 8ec1c8bbe..0825eb252 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -6,9 +6,9 @@ plugin: tester: Morg state: develop keywords: iot device av denon sdp - version: 1.0.1 - sh_minversion: 1.9.5 - py_minversion: 3.7 + version: '1.0.1' + sh_minversion: '1.9.5' + py_minversion: '3.7' sdp_minversion: '1.0.3' multi_instance: false restartable: true From d8d6b6740697d463a971be60c85919021316c4e5 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 22:08:50 +0200 Subject: [PATCH 041/121] pioneer plugin: fix command class handling --- pioneer/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pioneer/__init__.py b/pioneer/__init__.py index 7cd491eec..7f5f220cf 100755 --- a/pioneer/__init__.py +++ b/pioneer/__init__.py @@ -37,7 +37,7 @@ class SmartPluginWebIf(): BASE = os.path.sep.join(os.path.realpath(__file__).split(os.path.sep)[:-3]) sys.path.insert(0, BASE) -from lib.model.sdp.globals import (PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_CONNECTION, +from lib.model.sdp.globals import (PLUGIN_ATTR_NET_HOST, PLUGIN_ATTR_CONNECTION, PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_SERIAL_PORT, PLUGIN_ATTR_CONN_TERMINATOR, PLUGIN_ATTR_MODEL, CONN_NET_TCP_CLI, CONN_SER_ASYNC, CONN_NULL) from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone @@ -52,7 +52,7 @@ class pioneer(SmartDevicePlugin): """ Device class for Pioneer AV function. """ PLUGIN_VERSION = '1.0.3' - + def _set_device_defaults(self): # set our own preferences concerning connections if PLUGIN_ATTR_NET_HOST in self._parameters and self._parameters[PLUGIN_ATTR_NET_HOST]: From 5d4f0a7a91e26690b03534281fd0b8dafedc3ec7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 22:10:24 +0200 Subject: [PATCH 042/121] pioneer plugin: fix multizone command --- pioneer/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pioneer/commands.py b/pioneer/commands.py index cdac9906d..6938979c7 100755 --- a/pioneer/commands.py +++ b/pioneer/commands.py @@ -21,7 +21,7 @@ 'dimmer': {'read': True, 'write': True, 'write_cmd': '{RAW_VALUE}SAA', 'cmd_settings': {'force_min': 0, 'force_max': 3}, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'SAA(\d)', 'item_attrs': {'attributes': {'remark': '0 = very bright, 1 = bright, 2 = dark, 3 = off'}}}, 'sleep': {'read': True, 'write': True, 'read_cmd': '?SAB', 'write_cmd': '{VALUE}SAB', 'item_type': 'num', 'dev_datatype': 'PioSleep', 'reply_pattern': r'SAB(\d{3})', 'item_attrs': {'attributes': {'remark': '0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes'}}}, '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'}, + 'multizone': {'read': True, 'write': True, 'write_cmd': 'ZZ', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': r'(?:APR|BPR|ZEP)(\d{1})'}, 'settings': { '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}}, From d7d256edb37a1f8bbca050656d03667c6fe4d9ef Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 17 Aug 2024 22:10:49 +0200 Subject: [PATCH 043/121] pioneer plugin: fix plugin.yaml (instance included, other fixes) --- pioneer/plugin.yaml | 5958 +++++++++++++++++++++---------------------- 1 file changed, 2972 insertions(+), 2986 deletions(-) diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index 4e97f4a67..2fa473f60 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -126,17 +126,10 @@ parameters: description: de: Anzahl Sendeversuche en: number of sending retries + description_long: - de: 'Anzahl Sendeversuche\n - Kommt keine (passende) Antwort nach dem Senden - eines Commands zurück, wird das Kommando nochmals - gesendet, sofern der Wert über 0 liegt. - ' - en: 'number of sending retries\n - If no (suiting) answer is received after sending - a command the command is resent as long as this - value is more than 0. - ' + de: "Anzahl Sendeversuche\\n\nKommt keine (passende) Antwort nach dem Senden\neines Commands zurück, wird das Kommando nochmals\ngesendet, sofern der Wert über 0 liegt.\n" + en: "number of sending retries\\n\nIf no (suiting) answer is received after sending\na command the command is resent as long as this\nvalue is more than 0.\n" sendretry_cycle: type: num @@ -146,17 +139,10 @@ parameters: description: de: Pause zwischen Durchgängen von Sendeversuchen en: wait time between sending retry rounds + description_long: - de: 'Pause zwischen Durchgängen von Sendeversuchen\n - Sind Send Retries aktiv, wird ein Scheduler erstellt, - der im angegebenen Sekundentakt Kommandos erneut sendet, - zu denen keine (passenden) Antworten erhalten wurden. - ' - en: 'wait time between sending retry rounds\n - If send retries are active, a scheduler gets added - that resends commands in the given cycle value (in seconds) - where no (suiting) answer got received. - ' + de: "Pause zwischen Durchgängen von Sendeversuchen\\n\nSind Send Retries aktiv, wird ein Scheduler erstellt,\nder im angegebenen Sekundentakt Kommandos erneut sendet,\nzu denen keine (passenden) Antworten erhalten wurden.\n" + en: "wait time between sending retry rounds\\n\nIf send retries are active, a scheduler gets added\nthat resends commands in the given cycle value (in seconds)\nwhere no (suiting) answer got received.\n" delay_initial_read: type: num @@ -169,9 +155,10 @@ parameters: resume_initial_read: type: bool defaul: false + description: - de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' - en: 'Repeat initial read on resume' + de: Bei resume vom Plugin erstmaliges Lesen erneut durchführen + en: Repeat initial read on resume conn_type: type: str @@ -241,7 +228,6 @@ parameters: de: Item-Pfad für das Standby-Item en: item path for standby switch item - item_attributes: pioneer_command: @@ -311,290 +297,290 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: general + pioneer_read_group_trigger@instance: general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes amp: type: str - pioneer_command: general.amp - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.amp + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general remark: 0 = AMP, 1 = THR lookup: type: list - pioneer_lookup: AMP#list + pioneer_lookup@instance: AMP#list multizone: - type: str - pioneer_command: general.multizone - pioneer_read: false - pioneer_write: true + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: general.settings + pioneer_read_group_trigger@instance: general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list surroundposition: type: str - pioneer_command: general.settings.surroundposition - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.surroundposition + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SURROUNDPOSITION#list + pioneer_lookup@instance: SURROUNDPOSITION#list xover: type: str - pioneer_command: general.settings.xover - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xover + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true loudness: type: bool - pioneer_command: general.settings.loudness - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.loudness + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true initialvolume: type: num - pioneer_command: general.settings.initialvolume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.initialvolume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true mutelevel: type: num - pioneer_command: general.settings.mutelevel - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.mutelevel + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: general.settings.hdmi + pioneer_read_group_trigger@instance: general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - general - general.settings - general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: tuner + pioneer_read_group_trigger@instance: tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1 + pioneer_read_group_trigger@instance: zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.control + pioneer_read_group_trigger@instance: zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.control pioneer_read_initial: true @@ -607,75 +593,75 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.settings + pioneer_read_group_trigger@instance: zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -683,21 +669,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.settings.sound + pioneer_read_group_trigger@instance: zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -705,10 +691,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -716,10 +702,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -727,10 +713,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -738,10 +724,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -749,10 +735,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -760,10 +746,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -771,10 +757,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -782,10 +768,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -793,10 +779,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -807,14 +793,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -824,10 +810,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -835,22 +821,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -858,29 +844,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone1.settings.sound.general + pioneer_read_group_trigger@instance: zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -888,14 +874,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -903,14 +889,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -918,22 +904,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -941,14 +927,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -956,14 +942,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone1 - zone1.settings - zone1.settings.sound @@ -985,21 +971,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2 + pioneer_read_group_trigger@instance: zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2.control + pioneer_read_group_trigger@instance: zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.control pioneer_read_initial: true @@ -1012,75 +998,75 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2.settings + pioneer_read_group_trigger@instance: zone2.settings standby: type: num - pioneer_command: zone2.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -1088,21 +1074,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2.settings.sound + pioneer_read_group_trigger@instance: zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1110,10 +1096,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1124,14 +1110,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1141,10 +1127,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1152,10 +1138,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone2 - zone2.settings - zone2.settings.sound @@ -1166,21 +1152,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone3 + pioneer_read_group_trigger@instance: zone3 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone3.control + pioneer_read_group_trigger@instance: zone3.control power: type: bool - pioneer_command: zone3.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.control pioneer_read_initial: true @@ -1193,75 +1179,75 @@ item_structs: mute: type: bool - pioneer_command: zone3.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.control volume: type: num - pioneer_command: zone3.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.control volumeup: type: bool - pioneer_command: zone3.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone3.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone3.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.control lookup: type: list - pioneer_lookup: INPUT3#list + pioneer_lookup@instance: INPUT3#list inputup: type: bool - pioneer_command: zone3.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone3.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone3.settings + pioneer_read_group_trigger@instance: zone3.settings standby: type: num - pioneer_command: zone3.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -1269,21 +1255,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone3.settings.sound + pioneer_read_group_trigger@instance: zone3.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: zone3.settings.sound.channel_level + pioneer_read_group_trigger@instance: zone3.settings.sound.channel_level front_left: type: num - pioneer_command: zone3.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1291,10 +1277,10 @@ item_structs: front_right: type: num - pioneer_command: zone3.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - zone3 - zone3.settings - zone3.settings.sound @@ -1305,53 +1291,53 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: hdzone + pioneer_read_group_trigger@instance: hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: hdzone.control + pioneer_read_group_trigger@instance: hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - hdzone - hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - hdzone - hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: hdzone.settings + pioneer_read_group_trigger@instance: hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - hdzone - hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours ALL: @@ -1359,242 +1345,242 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL + pioneer_read_group_trigger@instance: ALL general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.general + pioneer_read_group_trigger@instance: ALL.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - ALL - ALL.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.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 + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.general.settings + pioneer_read_group_trigger@instance: ALL.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.general.settings.hdmi + pioneer_read_group_trigger@instance: ALL.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - ALL.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - ALL.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - ALL.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.general - ALL.general.settings - ALL.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.tuner + pioneer_read_group_trigger@instance: ALL.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1 + pioneer_read_group_trigger@instance: ALL.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.control + pioneer_read_group_trigger@instance: ALL.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control @@ -1608,79 +1594,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.settings + pioneer_read_group_trigger@instance: ALL.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -1688,21 +1674,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.settings.sound + pioneer_read_group_trigger@instance: ALL.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: ALL.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1711,10 +1697,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1723,10 +1709,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1735,10 +1721,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1747,10 +1733,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1759,10 +1745,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1771,10 +1757,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1783,10 +1769,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1795,10 +1781,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1807,10 +1793,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1822,14 +1808,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: ALL.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1840,10 +1826,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1852,22 +1838,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1876,29 +1862,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone1.settings.sound.general + pioneer_read_group_trigger@instance: ALL.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1907,14 +1893,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1923,14 +1909,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1939,22 +1925,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1963,14 +1949,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -1979,14 +1965,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone1 - ALL.zone1.settings @@ -2009,21 +1995,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone2 + pioneer_read_group_trigger@instance: ALL.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.zone2.control + pioneer_read_group_trigger@instance: ALL.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control @@ -2037,117 +2023,117 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.zone2 - ALL.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true hdzone: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.hdzone + pioneer_read_group_trigger@instance: ALL.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.hdzone.control + pioneer_read_group_trigger@instance: ALL.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.hdzone - ALL.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.hdzone - ALL.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: ALL.hdzone.settings + pioneer_read_group_trigger@instance: ALL.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - ALL - ALL.hdzone - ALL.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours SC-LX87: @@ -2155,293 +2141,293 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87 + pioneer_read_group_trigger@instance: SC-LX87 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.general + pioneer_read_group_trigger@instance: SC-LX87.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes amp: type: str - pioneer_command: general.amp - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.amp + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general remark: 0 = AMP, 1 = THR lookup: type: list - pioneer_lookup: AMP#list + pioneer_lookup@instance: AMP#list multizone: - type: str - pioneer_command: general.multizone - pioneer_read: false - pioneer_write: true + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.general.settings + pioneer_read_group_trigger@instance: SC-LX87.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list surroundposition: type: str - pioneer_command: general.settings.surroundposition - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.surroundposition + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SURROUNDPOSITION#list + pioneer_lookup@instance: SURROUNDPOSITION#list xover: type: str - pioneer_command: general.settings.xover - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xover + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true loudness: type: bool - pioneer_command: general.settings.loudness - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.loudness + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.general.settings.hdmi + pioneer_read_group_trigger@instance: SC-LX87.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - SC-LX87.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - SC-LX87.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - SC-LX87.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.general - SC-LX87.general.settings - SC-LX87.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.tuner + pioneer_read_group_trigger@instance: SC-LX87.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1 + pioneer_read_group_trigger@instance: SC-LX87.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.control + pioneer_read_group_trigger@instance: SC-LX87.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.control @@ -2455,79 +2441,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.settings + pioneer_read_group_trigger@instance: SC-LX87.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -2535,21 +2521,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.settings.sound + pioneer_read_group_trigger@instance: SC-LX87.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX87.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2558,10 +2544,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2570,10 +2556,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2582,10 +2568,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2594,10 +2580,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2606,10 +2592,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2618,10 +2604,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2630,10 +2616,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2642,10 +2628,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2654,10 +2640,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2669,14 +2655,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX87.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2687,10 +2673,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2699,22 +2685,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2723,29 +2709,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone1.settings.sound.general + pioneer_read_group_trigger@instance: SC-LX87.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2754,14 +2740,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2770,14 +2756,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2786,22 +2772,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2810,14 +2796,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2826,14 +2812,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone1 - SC-LX87.zone1.settings @@ -2856,21 +2842,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2 + pioneer_read_group_trigger@instance: SC-LX87.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2.control + pioneer_read_group_trigger@instance: SC-LX87.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.control @@ -2884,89 +2870,89 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2.settings + pioneer_read_group_trigger@instance: SC-LX87.zone2.settings sound: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2.settings.sound + pioneer_read_group_trigger@instance: SC-LX87.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX87.zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.settings @@ -2975,10 +2961,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.settings @@ -2990,14 +2976,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX87.zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.settings @@ -3008,10 +2994,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.settings @@ -3020,10 +3006,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone2 - SC-LX87.zone2.settings @@ -3035,21 +3021,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone3 + pioneer_read_group_trigger@instance: SC-LX87.zone3 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone3.control + pioneer_read_group_trigger@instance: SC-LX87.zone3.control power: type: bool - pioneer_command: zone3.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.control @@ -3063,79 +3049,79 @@ item_structs: mute: type: bool - pioneer_command: zone3.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.control volume: type: num - pioneer_command: zone3.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.control volumeup: type: bool - pioneer_command: zone3.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone3.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone3.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.control lookup: type: list - pioneer_lookup: INPUT3#list + pioneer_lookup@instance: INPUT3#list inputup: type: bool - pioneer_command: zone3.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone3.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone3.settings + pioneer_read_group_trigger@instance: SC-LX87.zone3.settings standby: type: num - pioneer_command: zone3.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -3143,21 +3129,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone3.settings.sound + pioneer_read_group_trigger@instance: SC-LX87.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.zone3.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX87.zone3.settings.sound.channel_level front_left: type: num - pioneer_command: zone3.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.settings @@ -3166,10 +3152,10 @@ item_structs: front_right: type: num - pioneer_command: zone3.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.zone3 - SC-LX87.zone3.settings @@ -3181,56 +3167,56 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.hdzone + pioneer_read_group_trigger@instance: SC-LX87.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.hdzone.control + pioneer_read_group_trigger@instance: SC-LX87.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.hdzone - SC-LX87.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.hdzone - SC-LX87.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX87.hdzone.settings + pioneer_read_group_trigger@instance: SC-LX87.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX87 - SC-LX87.hdzone - SC-LX87.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours SC-LX77: @@ -3238,293 +3224,293 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77 + pioneer_read_group_trigger@instance: SC-LX77 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.general + pioneer_read_group_trigger@instance: SC-LX77.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes amp: type: str - pioneer_command: general.amp - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.amp + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general remark: 0 = AMP, 1 = THR lookup: type: list - pioneer_lookup: AMP#list + pioneer_lookup@instance: AMP#list multizone: - type: str - pioneer_command: general.multizone - pioneer_read: false - pioneer_write: true + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.general.settings + pioneer_read_group_trigger@instance: SC-LX77.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list surroundposition: type: str - pioneer_command: general.settings.surroundposition - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.surroundposition + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SURROUNDPOSITION#list + pioneer_lookup@instance: SURROUNDPOSITION#list xover: type: str - pioneer_command: general.settings.xover - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xover + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true loudness: type: bool - pioneer_command: general.settings.loudness - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.loudness + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.general.settings.hdmi + pioneer_read_group_trigger@instance: SC-LX77.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - SC-LX77.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - SC-LX77.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - SC-LX77.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.general - SC-LX77.general.settings - SC-LX77.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.tuner + pioneer_read_group_trigger@instance: SC-LX77.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1 + pioneer_read_group_trigger@instance: SC-LX77.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.control + pioneer_read_group_trigger@instance: SC-LX77.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.control @@ -3538,79 +3524,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.settings + pioneer_read_group_trigger@instance: SC-LX77.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -3618,21 +3604,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.settings.sound + pioneer_read_group_trigger@instance: SC-LX77.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX77.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3641,10 +3627,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3653,10 +3639,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3665,10 +3651,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3677,10 +3663,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3689,10 +3675,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3701,10 +3687,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3713,10 +3699,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3725,10 +3711,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3737,10 +3723,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3752,14 +3738,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX77.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3770,10 +3756,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3782,22 +3768,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3806,29 +3792,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone1.settings.sound.general + pioneer_read_group_trigger@instance: SC-LX77.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3837,14 +3823,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3853,14 +3839,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3869,22 +3855,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3893,14 +3879,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3909,14 +3895,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone1 - SC-LX77.zone1.settings @@ -3939,21 +3925,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2 + pioneer_read_group_trigger@instance: SC-LX77.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2.control + pioneer_read_group_trigger@instance: SC-LX77.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.control @@ -3967,89 +3953,89 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2.settings + pioneer_read_group_trigger@instance: SC-LX77.zone2.settings sound: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2.settings.sound + pioneer_read_group_trigger@instance: SC-LX77.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX77.zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.settings @@ -4058,10 +4044,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.settings @@ -4073,14 +4059,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX77.zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.settings @@ -4091,10 +4077,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.settings @@ -4103,10 +4089,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone2 - SC-LX77.zone2.settings @@ -4118,21 +4104,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone3 + pioneer_read_group_trigger@instance: SC-LX77.zone3 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone3.control + pioneer_read_group_trigger@instance: SC-LX77.zone3.control power: type: bool - pioneer_command: zone3.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.control @@ -4146,79 +4132,79 @@ item_structs: mute: type: bool - pioneer_command: zone3.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.control volume: type: num - pioneer_command: zone3.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.control volumeup: type: bool - pioneer_command: zone3.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone3.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone3.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.control lookup: type: list - pioneer_lookup: INPUT3#list + pioneer_lookup@instance: INPUT3#list inputup: type: bool - pioneer_command: zone3.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone3.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone3.settings + pioneer_read_group_trigger@instance: SC-LX77.zone3.settings standby: type: num - pioneer_command: zone3.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -4226,21 +4212,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone3.settings.sound + pioneer_read_group_trigger@instance: SC-LX77.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.zone3.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX77.zone3.settings.sound.channel_level front_left: type: num - pioneer_command: zone3.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.settings @@ -4249,10 +4235,10 @@ item_structs: front_right: type: num - pioneer_command: zone3.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.zone3 - SC-LX77.zone3.settings @@ -4264,56 +4250,56 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.hdzone + pioneer_read_group_trigger@instance: SC-LX77.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.hdzone.control + pioneer_read_group_trigger@instance: SC-LX77.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.hdzone - SC-LX77.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.hdzone - SC-LX77.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX77.hdzone.settings + pioneer_read_group_trigger@instance: SC-LX77.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX77 - SC-LX77.hdzone - SC-LX77.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours SC-LX57: @@ -4321,293 +4307,293 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57 + pioneer_read_group_trigger@instance: SC-LX57 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.general + pioneer_read_group_trigger@instance: SC-LX57.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general remark: 0 = off, 30 = 30 minutes, 60 = 60 minutes, 90 = 90 minutes amp: type: str - pioneer_command: general.amp - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.amp + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general remark: 0 = AMP, 1 = THR lookup: type: list - pioneer_lookup: AMP#list + pioneer_lookup@instance: AMP#list multizone: - type: str - pioneer_command: general.multizone - pioneer_read: false - pioneer_write: true + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.general.settings + pioneer_read_group_trigger@instance: SC-LX57.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list surroundposition: type: str - pioneer_command: general.settings.surroundposition - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.surroundposition + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SURROUNDPOSITION#list + pioneer_lookup@instance: SURROUNDPOSITION#list xover: type: str - pioneer_command: general.settings.xover - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xover + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true loudness: type: bool - pioneer_command: general.settings.loudness - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.loudness + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.general.settings.hdmi + pioneer_read_group_trigger@instance: SC-LX57.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - SC-LX57.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - SC-LX57.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - SC-LX57.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.general - SC-LX57.general.settings - SC-LX57.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.tuner + pioneer_read_group_trigger@instance: SC-LX57.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1 + pioneer_read_group_trigger@instance: SC-LX57.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.control + pioneer_read_group_trigger@instance: SC-LX57.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.control @@ -4621,79 +4607,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.settings + pioneer_read_group_trigger@instance: SC-LX57.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -4701,21 +4687,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.settings.sound + pioneer_read_group_trigger@instance: SC-LX57.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX57.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4724,10 +4710,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4736,10 +4722,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4748,10 +4734,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4760,10 +4746,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4772,10 +4758,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4784,10 +4770,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4796,10 +4782,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4808,10 +4794,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4820,10 +4806,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4835,14 +4821,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX57.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4853,10 +4839,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4865,22 +4851,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4889,29 +4875,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone1.settings.sound.general + pioneer_read_group_trigger@instance: SC-LX57.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4920,14 +4906,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4936,14 +4922,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4952,22 +4938,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4976,14 +4962,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -4992,14 +4978,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone1 - SC-LX57.zone1.settings @@ -5022,21 +5008,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2 + pioneer_read_group_trigger@instance: SC-LX57.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2.control + pioneer_read_group_trigger@instance: SC-LX57.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.control @@ -5050,89 +5036,89 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2.settings + pioneer_read_group_trigger@instance: SC-LX57.zone2.settings sound: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2.settings.sound + pioneer_read_group_trigger@instance: SC-LX57.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX57.zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.settings @@ -5141,10 +5127,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.settings @@ -5156,14 +5142,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-LX57.zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.settings @@ -5174,10 +5160,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.settings @@ -5186,10 +5172,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone2 - SC-LX57.zone2.settings @@ -5201,21 +5187,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone3 + pioneer_read_group_trigger@instance: SC-LX57.zone3 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone3.control + pioneer_read_group_trigger@instance: SC-LX57.zone3.control power: type: bool - pioneer_command: zone3.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.control @@ -5229,79 +5215,79 @@ item_structs: mute: type: bool - pioneer_command: zone3.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.control volume: type: num - pioneer_command: zone3.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.control volumeup: type: bool - pioneer_command: zone3.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone3.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone3.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.control lookup: type: list - pioneer_lookup: INPUT3#list + pioneer_lookup@instance: INPUT3#list inputup: type: bool - pioneer_command: zone3.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone3.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone3.settings + pioneer_read_group_trigger@instance: SC-LX57.zone3.settings standby: type: num - pioneer_command: zone3.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -5309,21 +5295,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone3.settings.sound + pioneer_read_group_trigger@instance: SC-LX57.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.zone3.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-LX57.zone3.settings.sound.channel_level front_left: type: num - pioneer_command: zone3.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.settings @@ -5332,10 +5318,10 @@ item_structs: front_right: type: num - pioneer_command: zone3.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.zone3 - SC-LX57.zone3.settings @@ -5347,56 +5333,56 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.hdzone + pioneer_read_group_trigger@instance: SC-LX57.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.hdzone.control + pioneer_read_group_trigger@instance: SC-LX57.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.hdzone - SC-LX57.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.hdzone - SC-LX57.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-LX57.hdzone.settings + pioneer_read_group_trigger@instance: SC-LX57.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-LX57 - SC-LX57.hdzone - SC-LX57.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours SC-2023: @@ -5404,268 +5390,268 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023 + pioneer_read_group_trigger@instance: SC-2023 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.general + pioneer_read_group_trigger@instance: SC-2023.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-2023 - SC-2023.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - 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 + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.general.settings + pioneer_read_group_trigger@instance: SC-2023.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list surroundposition: type: str - pioneer_command: general.settings.surroundposition - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.surroundposition + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SURROUNDPOSITION#list + pioneer_lookup@instance: SURROUNDPOSITION#list xover: type: str - pioneer_command: general.settings.xover - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xover + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.general.settings.hdmi + pioneer_read_group_trigger@instance: SC-2023.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - SC-2023.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - SC-2023.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - SC-2023.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.general - SC-2023.general.settings - SC-2023.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.tuner + pioneer_read_group_trigger@instance: SC-2023.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1 + pioneer_read_group_trigger@instance: SC-2023.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.control + pioneer_read_group_trigger@instance: SC-2023.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.control @@ -5679,79 +5665,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.settings + pioneer_read_group_trigger@instance: SC-2023.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -5759,21 +5745,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.settings.sound + pioneer_read_group_trigger@instance: SC-2023.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-2023.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5782,10 +5768,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5794,10 +5780,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5806,10 +5792,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5818,10 +5804,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5830,10 +5816,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5842,10 +5828,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5854,10 +5840,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5866,10 +5852,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5878,10 +5864,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5893,14 +5879,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-2023.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5911,10 +5897,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5923,22 +5909,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5947,29 +5933,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone1.settings.sound.general + pioneer_read_group_trigger@instance: SC-2023.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5978,14 +5964,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -5994,14 +5980,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -6010,22 +5996,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -6034,14 +6020,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -6050,14 +6036,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone1 - SC-2023.zone1.settings @@ -6080,21 +6066,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2 + pioneer_read_group_trigger@instance: SC-2023.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2.control + pioneer_read_group_trigger@instance: SC-2023.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.control @@ -6108,89 +6094,89 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2.settings + pioneer_read_group_trigger@instance: SC-2023.zone2.settings sound: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2.settings.sound + pioneer_read_group_trigger@instance: SC-2023.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-2023.zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.settings @@ -6199,10 +6185,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.settings @@ -6214,14 +6200,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-2023.zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.settings @@ -6232,10 +6218,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.settings @@ -6244,10 +6230,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone2 - SC-2023.zone2.settings @@ -6259,21 +6245,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone3 + pioneer_read_group_trigger@instance: SC-2023.zone3 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone3.control + pioneer_read_group_trigger@instance: SC-2023.zone3.control power: type: bool - pioneer_command: zone3.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.control @@ -6287,79 +6273,79 @@ item_structs: mute: type: bool - pioneer_command: zone3.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.control volume: type: num - pioneer_command: zone3.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.control volumeup: type: bool - pioneer_command: zone3.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone3.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone3.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.control lookup: type: list - pioneer_lookup: INPUT3#list + pioneer_lookup@instance: INPUT3#list inputup: type: bool - pioneer_command: zone3.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone3.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone3.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone3.settings + pioneer_read_group_trigger@instance: SC-2023.zone3.settings standby: type: num - pioneer_command: zone3.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours sound: @@ -6367,21 +6353,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone3.settings.sound + pioneer_read_group_trigger@instance: SC-2023.zone3.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.zone3.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-2023.zone3.settings.sound.channel_level front_left: type: num - pioneer_command: zone3.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.settings @@ -6390,10 +6376,10 @@ item_structs: front_right: type: num - pioneer_command: zone3.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone3.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.zone3 - SC-2023.zone3.settings @@ -6405,56 +6391,56 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.hdzone + pioneer_read_group_trigger@instance: SC-2023.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.hdzone.control + pioneer_read_group_trigger@instance: SC-2023.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.hdzone - SC-2023.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.hdzone - SC-2023.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-2023.hdzone.settings + pioneer_read_group_trigger@instance: SC-2023.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-2023 - SC-2023.hdzone - SC-2023.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours SC-1223: @@ -6462,242 +6448,242 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223 + pioneer_read_group_trigger@instance: SC-1223 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.general + pioneer_read_group_trigger@instance: SC-1223.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-1223 - SC-1223.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.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 + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.general.settings + pioneer_read_group_trigger@instance: SC-1223.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.general.settings.hdmi + pioneer_read_group_trigger@instance: SC-1223.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - SC-1223.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - SC-1223.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - SC-1223.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.general - SC-1223.general.settings - SC-1223.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.tuner + pioneer_read_group_trigger@instance: SC-1223.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1 + pioneer_read_group_trigger@instance: SC-1223.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.control + pioneer_read_group_trigger@instance: SC-1223.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.control @@ -6711,79 +6697,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.settings + pioneer_read_group_trigger@instance: SC-1223.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -6791,21 +6777,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.settings.sound + pioneer_read_group_trigger@instance: SC-1223.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-1223.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6814,10 +6800,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6826,10 +6812,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6838,10 +6824,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6850,10 +6836,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6862,10 +6848,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6874,10 +6860,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6886,10 +6872,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6898,10 +6884,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6910,10 +6896,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6925,14 +6911,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-1223.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6943,10 +6929,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6955,22 +6941,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -6979,29 +6965,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone1.settings.sound.general + pioneer_read_group_trigger@instance: SC-1223.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7010,14 +6996,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7026,14 +7012,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7042,22 +7028,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7066,14 +7052,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7082,14 +7068,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone1 - SC-1223.zone1.settings @@ -7112,21 +7098,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2 + pioneer_read_group_trigger@instance: SC-1223.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2.control + pioneer_read_group_trigger@instance: SC-1223.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.control @@ -7140,89 +7126,89 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2.settings + pioneer_read_group_trigger@instance: SC-1223.zone2.settings sound: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2.settings.sound + pioneer_read_group_trigger@instance: SC-1223.zone2.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2.settings.sound.channel_level + pioneer_read_group_trigger@instance: SC-1223.zone2.settings.sound.channel_level front_left: type: num - pioneer_command: zone2.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.settings @@ -7231,10 +7217,10 @@ item_structs: front_right: type: num - pioneer_command: zone2.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.settings @@ -7246,14 +7232,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.zone2.settings.sound.tone_control + pioneer_read_group_trigger@instance: SC-1223.zone2.settings.sound.tone_control tone: type: bool - pioneer_command: zone2.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.settings @@ -7264,10 +7250,10 @@ item_structs: treble: type: num - pioneer_command: zone2.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.settings @@ -7276,10 +7262,10 @@ item_structs: bass: type: num - pioneer_command: zone2.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.zone2 - SC-1223.zone2.settings @@ -7291,56 +7277,56 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.hdzone + pioneer_read_group_trigger@instance: SC-1223.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.hdzone.control + pioneer_read_group_trigger@instance: SC-1223.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.hdzone - SC-1223.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.hdzone - SC-1223.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: SC-1223.hdzone.settings + pioneer_read_group_trigger@instance: SC-1223.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - SC-1223 - SC-1223.hdzone - SC-1223.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours VSX-1123: @@ -7348,242 +7334,242 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123 + pioneer_read_group_trigger@instance: VSX-1123 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.general + pioneer_read_group_trigger@instance: VSX-1123.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.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 + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.general.settings + pioneer_read_group_trigger@instance: VSX-1123.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.general.settings.hdmi + pioneer_read_group_trigger@instance: VSX-1123.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - VSX-1123.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - VSX-1123.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - VSX-1123.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.general - VSX-1123.general.settings - VSX-1123.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.tuner + pioneer_read_group_trigger@instance: VSX-1123.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1 + pioneer_read_group_trigger@instance: VSX-1123.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.control + pioneer_read_group_trigger@instance: VSX-1123.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.control @@ -7597,79 +7583,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.settings + pioneer_read_group_trigger@instance: VSX-1123.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -7677,21 +7663,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.settings.sound + pioneer_read_group_trigger@instance: VSX-1123.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: VSX-1123.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7700,10 +7686,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7712,10 +7698,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7724,10 +7710,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7736,10 +7722,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7748,10 +7734,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7760,10 +7746,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7772,10 +7758,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7784,10 +7770,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7796,10 +7782,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7811,14 +7797,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: VSX-1123.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7829,10 +7815,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7841,22 +7827,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7865,29 +7851,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone1.settings.sound.general + pioneer_read_group_trigger@instance: VSX-1123.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7896,14 +7882,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7912,14 +7898,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7928,22 +7914,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7952,14 +7938,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7968,14 +7954,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone1 - VSX-1123.zone1.settings @@ -7998,21 +7984,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone2 + pioneer_read_group_trigger@instance: VSX-1123.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.zone2.control + pioneer_read_group_trigger@instance: VSX-1123.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone2 - VSX-1123.zone2.control @@ -8026,117 +8012,117 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone2 - VSX-1123.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone2 - VSX-1123.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.zone2 - VSX-1123.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true hdzone: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.hdzone + pioneer_read_group_trigger@instance: VSX-1123.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.hdzone.control + pioneer_read_group_trigger@instance: VSX-1123.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.hdzone - VSX-1123.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.hdzone - VSX-1123.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-1123.hdzone.settings + pioneer_read_group_trigger@instance: VSX-1123.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-1123 - VSX-1123.hdzone - VSX-1123.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours VSX-923: @@ -8144,242 +8130,242 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923 + pioneer_read_group_trigger@instance: VSX-923 general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.general + pioneer_read_group_trigger@instance: VSX-923.general error: type: str - pioneer_command: general.error - pioneer_read: true - pioneer_write: false + pioneer_command@instance: general.error + pioneer_read@instance: true + pioneer_write@instance: false display: type: str - pioneer_command: general.display - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: general.display + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - VSX-923 - VSX-923.general pqls: type: bool - pioneer_command: general.pqls - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.pqls + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general dimmer: type: num - pioneer_command: general.dimmer - pioneer_read: true - pioneer_write: true + pioneer_command@instance: general.dimmer + pioneer_read@instance: true + pioneer_write@instance: 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: + pioneer_command@instance: general.sleep + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.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 + type: bool + pioneer_command@instance: general.multizone + pioneer_read@instance: true + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.general.settings + pioneer_read_group_trigger@instance: VSX-923.general.settings language: type: str - pioneer_command: general.settings.language - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.language + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true name: type: str - pioneer_command: general.settings.name - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.name + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true speakersystem: type: str - pioneer_command: general.settings.speakersystem - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.speakersystem + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: SPEAKERSYSTEM#list + pioneer_lookup@instance: SPEAKERSYSTEM#list xcurve: type: str - pioneer_command: general.settings.xcurve - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.xcurve + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true hdmi: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.general.settings.hdmi + pioneer_read_group_trigger@instance: VSX-923.general.settings.hdmi control: type: bool - pioneer_command: general.settings.hdmi.control - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.control + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - VSX-923.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true controlmode: type: bool - pioneer_command: general.settings.hdmi.controlmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.controlmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - VSX-923.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true arc: type: bool - pioneer_command: general.settings.hdmi.arc - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.arc + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - VSX-923.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true standbythrough: type: str - pioneer_command: general.settings.hdmi.standbythrough - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: general.settings.hdmi.standbythrough + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.general - VSX-923.general.settings - VSX-923.general.settings.hdmi - pioneer_read_initial: true + pioneer_read_initial@instance: true lookup: type: list - pioneer_lookup: STANDBYTHROUGH#list + pioneer_lookup@instance: STANDBYTHROUGH#list tuner: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.tuner + pioneer_read_group_trigger@instance: VSX-923.tuner tunerpreset: type: num - pioneer_command: tuner.tunerpreset - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: tuner.tunerpreset + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.tuner tunerpresetup: type: bool - pioneer_command: tuner.tunerpresetup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetup + pioneer_read@instance: false + pioneer_write@instance: true tunerpresetdown: type: bool - pioneer_command: tuner.tunerpresetdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: tuner.tunerpresetdown + pioneer_read@instance: false + pioneer_write@instance: true title: type: str - pioneer_command: tuner.title - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.title + pioneer_read@instance: true + pioneer_write@instance: false genre: type: str - pioneer_command: tuner.genre - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.genre + pioneer_read@instance: true + pioneer_write@instance: false station: type: str - pioneer_command: tuner.station - pioneer_read: true - pioneer_write: false + pioneer_command@instance: tuner.station + pioneer_read@instance: true + pioneer_write@instance: false zone1: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1 + pioneer_read_group_trigger@instance: VSX-923.zone1 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.control + pioneer_read_group_trigger@instance: VSX-923.zone1.control power: type: bool - pioneer_command: zone1.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.control @@ -8393,79 +8379,79 @@ item_structs: mute: type: bool - pioneer_command: zone1.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.control volume: type: num - pioneer_command: zone1.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.control volumeup: type: bool - pioneer_command: zone1.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone1.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone1.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.control lookup: type: list - pioneer_lookup: INPUT#list + pioneer_lookup@instance: INPUT#list inputup: type: bool - pioneer_command: zone1.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone1.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.settings + pioneer_read_group_trigger@instance: VSX-923.zone1.settings standby: type: num - pioneer_command: zone1.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 15 = 15 minutes, 30 = 30 minutes, 60 = 60 minutes sound: @@ -8473,21 +8459,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.settings.sound + pioneer_read_group_trigger@instance: VSX-923.zone1.settings.sound channel_level: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.settings.sound.channel_level + pioneer_read_group_trigger@instance: VSX-923.zone1.settings.sound.channel_level front_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8496,10 +8482,10 @@ item_structs: front_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8508,10 +8494,10 @@ item_structs: front_center: type: num - pioneer_command: zone1.settings.sound.channel_level.front_center - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_center + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8520,10 +8506,10 @@ item_structs: surround_left: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8532,10 +8518,10 @@ item_structs: surround_right: type: num - pioneer_command: zone1.settings.sound.channel_level.surround_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.surround_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8544,10 +8530,10 @@ item_structs: front_height_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8556,10 +8542,10 @@ item_structs: front_height_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_height_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_height_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8568,10 +8554,10 @@ item_structs: front_wide_left: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_left - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_left + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8580,10 +8566,10 @@ item_structs: front_wide_right: type: num - pioneer_command: zone1.settings.sound.channel_level.front_wide_right - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.front_wide_right + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8592,10 +8578,10 @@ item_structs: subwoofer: type: num - pioneer_command: zone1.settings.sound.channel_level.subwoofer - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.channel_level.subwoofer + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8607,14 +8593,14 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.settings.sound.tone_control + pioneer_read_group_trigger@instance: VSX-923.zone1.settings.sound.tone_control tone: type: bool - pioneer_command: zone1.settings.sound.tone_control.tone - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.tone + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8625,10 +8611,10 @@ item_structs: treble: type: num - pioneer_command: zone1.settings.sound.tone_control.treble - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.treble + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8637,22 +8623,22 @@ item_structs: trebleup: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebleup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebleup + pioneer_read@instance: false + pioneer_write@instance: true trebledown: type: bool - pioneer_command: zone1.settings.sound.tone_control.trebledown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.trebledown + pioneer_read@instance: false + pioneer_write@instance: true bass: type: num - pioneer_command: zone1.settings.sound.tone_control.bass - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.tone_control.bass + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8661,29 +8647,29 @@ item_structs: bassup: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassup + pioneer_read@instance: false + pioneer_write@instance: true bassdown: type: bool - pioneer_command: zone1.settings.sound.tone_control.bassdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.tone_control.bassdown + pioneer_read@instance: false + pioneer_write@instance: true general: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone1.settings.sound.general + pioneer_read_group_trigger@instance: VSX-923.zone1.settings.sound.general hdmiout: type: str - pioneer_command: zone1.settings.sound.general.hdmiout - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiout + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8692,14 +8678,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIOUT#list + pioneer_lookup@instance: HDMIOUT#list hdmiaudio: type: str - pioneer_command: zone1.settings.sound.general.hdmiaudio - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.hdmiaudio + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8708,14 +8694,14 @@ item_structs: lookup: type: list - pioneer_lookup: HDMIAUDIO#list + pioneer_lookup@instance: HDMIAUDIO#list dialog: type: num - pioneer_command: zone1.settings.sound.general.dialog - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.dialog + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8724,22 +8710,22 @@ item_structs: dialogup: type: bool - pioneer_command: zone1.settings.sound.general.dialogup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogup + pioneer_read@instance: false + pioneer_write@instance: true dialogdown: type: bool - pioneer_command: zone1.settings.sound.general.dialogdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone1.settings.sound.general.dialogdown + pioneer_read@instance: false + pioneer_write@instance: true listeningmode: type: str - pioneer_command: zone1.settings.sound.general.listeningmode - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.listeningmode + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8748,14 +8734,14 @@ item_structs: lookup: type: list - pioneer_lookup: LISTENINGMODE#list + pioneer_lookup@instance: LISTENINGMODE#list playingmode: type: str - pioneer_command: zone1.settings.sound.general.playingmode - pioneer_read: true - pioneer_write: false - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.playingmode + pioneer_read@instance: true + pioneer_write@instance: false + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8764,14 +8750,14 @@ item_structs: lookup: type: list - pioneer_lookup: PLAYINGMODE#list + pioneer_lookup@instance: PLAYINGMODE#list speakers: type: num - pioneer_command: zone1.settings.sound.general.speakers - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone1.settings.sound.general.speakers + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone1 - VSX-923.zone1.settings @@ -8794,21 +8780,21 @@ item_structs: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone2 + pioneer_read_group_trigger@instance: VSX-923.zone2 control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.zone2.control + pioneer_read_group_trigger@instance: VSX-923.zone2.control power: type: bool - pioneer_command: zone2.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone2 - VSX-923.zone2.control @@ -8822,117 +8808,117 @@ item_structs: mute: type: bool - pioneer_command: zone2.control.mute - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.mute + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone2 - VSX-923.zone2.control volume: type: num - pioneer_command: zone2.control.volume - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.volume + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone2 - VSX-923.zone2.control volumeup: type: bool - pioneer_command: zone2.control.volumeup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumeup + pioneer_read@instance: false + pioneer_write@instance: true volumedown: type: bool - pioneer_command: zone2.control.volumedown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.volumedown + pioneer_read@instance: false + pioneer_write@instance: true input: type: str - pioneer_command: zone2.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: zone2.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.zone2 - VSX-923.zone2.control lookup: type: list - pioneer_lookup: INPUT2#list + pioneer_lookup@instance: INPUT2#list inputup: type: bool - pioneer_command: zone2.control.inputup - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputup + pioneer_read@instance: false + pioneer_write@instance: true inputdown: type: bool - pioneer_command: zone2.control.inputdown - pioneer_read: false - pioneer_write: true + pioneer_command@instance: zone2.control.inputdown + pioneer_read@instance: false + pioneer_write@instance: true hdzone: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.hdzone + pioneer_read_group_trigger@instance: VSX-923.hdzone control: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.hdzone.control + pioneer_read_group_trigger@instance: VSX-923.hdzone.control power: type: bool - pioneer_command: hdzone.control.power - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.power + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.hdzone - VSX-923.hdzone.control input: type: str - pioneer_command: hdzone.control.input - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.control.input + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.hdzone - VSX-923.hdzone.control lookup: type: list - pioneer_lookup: INPUTHD#list + pioneer_lookup@instance: INPUTHD#list settings: read: type: bool enforce_updates: true - pioneer_read_group_trigger: VSX-923.hdzone.settings + pioneer_read_group_trigger@instance: VSX-923.hdzone.settings standby: type: num - pioneer_command: hdzone.settings.standby - pioneer_read: true - pioneer_write: true - pioneer_read_group: + pioneer_command@instance: hdzone.settings.standby + pioneer_read@instance: true + pioneer_write@instance: true + pioneer_read_group@instance: - VSX-923 - VSX-923.hdzone - VSX-923.hdzone.settings - pioneer_read_initial: true + pioneer_read_initial@instance: true remark: 0 = OFF, 0.5 = 30 minutes, 1 = 1 hour, 3 = 3 hours, 6 = 6 hours, 9 = 9 hours plugin_functions: NONE logic_parameters: NONE From 86841c4dbecb6232a17feb6a33f33dd7746a42cf Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 18 Aug 2024 01:16:01 +0200 Subject: [PATCH 044/121] lms plugin: convert specific replies to correct boolean --- lms/commands.py | 8 ++++---- lms/datatypes.py | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lms/commands.py b/lms/commands.py index 2b29e4308..c1401eb30 100755 --- a/lms/commands.py +++ b/lms/commands.py @@ -6,7 +6,7 @@ commands = { 'server': { - 'listenmode': {'read': True, 'write': True, 'write_cmd': 'listen {RAW_VALUE:01}', 'item_type': 'bool', 'dev_datatype': 'str', 'reply_pattern': r'listen (\d)', 'item_attrs': {'custom1': ''}}, + 'listenmode': {'read': True, 'write': True, 'write_cmd': 'listen {RAW_VALUE:01}', 'item_type': 'bool', 'dev_datatype': 'LMSonoff', 'reply_pattern': r'listen (\d)', 'item_attrs': {'custom1': ''}}, 'playercount': {'read': True, 'write': False, 'read_cmd': 'player count ?', 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'player count (\d+)', 'item_attrs': {'initial': True, 'custom1': ''}}, 'favoritescount': {'read': True, 'write': False, 'read_cmd': 'favorites items', 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'favorites items\s+ count:(\d+)', 'item_attrs': {'initial': True, 'custom1': ''}}, }, @@ -28,11 +28,11 @@ }, 'player': { 'control': { - 'power': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} power ?', 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} power {RAW_VALUE:01}', 'dev_datatype': 'str', 'reply_pattern': [r'{CUSTOM_PATTERN1} (?:prefset server\s)?power (\d)', '{CUSTOM_PATTERN1} status(?:.*)power:([^\s]+)'], 'item_attrs': {'enforce': True}}, + 'power': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} power ?', 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} power {RAW_VALUE:01}', 'dev_datatype': 'LMSonoff', 'reply_pattern': [r'{CUSTOM_PATTERN1} (?:prefset server\s)?power (\d)', '{CUSTOM_PATTERN1} status(?:.*)power:([^\s]+)'], 'item_attrs': {'enforce': True}}, 'playmode': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} mode ?', 'item_type': 'str', 'write_cmd': '{CUSTOM_ATTR1} mode {VALUE}', 'dev_datatype': 'LMSPlayMode', 'cmd_settings': {'valid_list_ci': ['PLAY', 'PAUSE', 'STOP']}, 'reply_pattern': [r'{CUSTOM_PATTERN1} mode {VALID_LIST_CI}', r'{CUSTOM_PATTERN1} playlist (pause \d|stop)', '{CUSTOM_PATTERN1} status(?:.*)mode:([^\s]+)'], 'item_attrs': {'enforce': True}}, 'playpause': {'read': True, 'write': True, 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} {VALUE}', 'dev_datatype': 'LMSPlay', 'reply_pattern': [r'{CUSTOM_PATTERN1} (?:playlist\s)?(play|pause)(?:\s3)?$', '{CUSTOM_PATTERN1} pause (0|1)'], 'item_attrs': {'enforce': True}}, 'stop': {'read': True, 'write': True, 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} {VALUE}', 'dev_datatype': 'LMSStop', 'reply_pattern': r'{CUSTOM_PATTERN1} (?:playlist\s)?(stop)$', 'item_attrs': {'enforce': True}}, - 'mute': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} mixer muting ?', 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} mixer muting {RAW_VALUE:01}', 'dev_datatype': 'str', 'reply_pattern': r'{CUSTOM_PATTERN1} (?:mixer muting|prefset server mute) (\d)', 'item_attrs': {'initial': True, 'enforce': True}}, + 'mute': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} mixer muting ?', 'item_type': 'bool', 'write_cmd': '{CUSTOM_ATTR1} mixer muting {RAW_VALUE:01}', 'dev_datatype': 'LMSonoff', 'reply_pattern': r'{CUSTOM_PATTERN1} (?:mixer muting|prefset server mute) (\d)', 'item_attrs': {'initial': True, 'enforce': True}}, 'volume': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} mixer volume ?', 'item_type': 'num', 'write_cmd': '{CUSTOM_ATTR1} mixer volume {VALUE}', 'dev_datatype': 'str', 'reply_pattern': [r'{CUSTOM_PATTERN1} (?:mixer volume \-?|prefset server volume \-?)(\d{1,3})', '{CUSTOM_PATTERN1} status(?:.*)mixer volume:([^\s]+)']}, 'volume_fading': {'read': False, 'write': True, 'item_type': 'num', 'write_cmd': '{CUSTOM_ATTR1} mixer volume {VALUE}', 'dev_datatype': 'str', 'item_attrs': {'item_template': 'volume_fading'}}, 'volume_low': {'read': False, 'write': True, 'item_type': 'num', 'write_cmd': '{CUSTOM_ATTR1} mixer volume {VALUE}', 'dev_datatype': 'str', 'item_attrs': {'attributes': {'cache': True, 'enforce_updates': True, 'initial_value': 60}}}, @@ -42,7 +42,7 @@ 'set_alarm': {'read': True, 'write': True, 'item_type': 'str', 'write_cmd': '{CUSTOM_ATTR1} alarm {VALUE}', 'dev_datatype': 'str', 'reply_pattern': '{CUSTOM_PATTERN1} alarm (.*)'}, 'alarms': {'read': True, 'write': False, 'item_type': 'dict', 'read_cmd': '{CUSTOM_ATTR1} alarms 0 100 all', 'dev_datatype': 'LMSAlarms', 'reply_pattern': r'{CUSTOM_PATTERN1} alarms 0 100 all fade:\d+ count:\d+ (.*)', 'item_attrs': {'initial': True, 'read_groups': [{'name': 'player.control.alarms', 'trigger': 'query'}]}}, 'sync': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} sync ?', 'write_cmd': '{CUSTOM_ATTR1} sync {VALUE}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': '{CUSTOM_PATTERN1} sync (.*)', 'item_attrs': {'initial': True}}, - 'unsync': {'read': False, 'write': True, 'write_cmd': '{CUSTOM_ATTR1} sync -', 'item_type': 'bool', 'dev_datatype': 'str', 'item_attrs': {'attributes': {'autotimer': '1s = 0'}}}, + 'unsync': {'read': False, 'write': True, 'write_cmd': '{CUSTOM_ATTR1} sync -', 'item_type': 'bool', 'dev_datatype': 'LMSonoff', 'item_attrs': {'attributes': {'autotimer': '1s = 0'}}}, 'display': {'read': True, 'write': True, 'read_cmd': '{CUSTOM_ATTR1} display ? ?', 'item_type': 'str', 'write_cmd': '{CUSTOM_ATTR1} display {VALUE}', 'dev_datatype': 'str', 'reply_pattern': r'{CUSTOM_PATTERN1} display\s?(.*)', 'item_attrs': {'initial': True}}, 'connect': {'read': True, 'write': True, 'item_type': 'str', 'write_cmd': '{CUSTOM_ATTR1} connect {VALUE}', 'dev_datatype': 'str', 'reply_pattern': '{CUSTOM_PATTERN1} connect (.*)', 'item_attrs': {'attributes': {'remark': 'ip|www.mysqueezebox.com|www.test.mysqueezebox.com'}}}, 'disconnect': {'read': True, 'write': True, 'item_type': 'str', 'write_cmd': 'disconnect {CUSTOM_ATTR1} {VALUE}', 'dev_datatype': 'str', 'reply_pattern': 'disconnect {CUSTOM_PATTERN1} (.*)', 'item_attrs': {'attributes': {'remark': 'ip|www.mysqueezebox.com|www.test.mysqueezebox.com'}}}, diff --git a/lms/datatypes.py b/lms/datatypes.py index c60618a47..42c10b6a6 100755 --- a/lms/datatypes.py +++ b/lms/datatypes.py @@ -70,3 +70,7 @@ def get_send_data(self, data, type=None, **kwargs): def get_shng_data(self, data, type=None, **kwargs): return True if data == "stop" else False + +class DT_LMSonoff(DT.Datatype): + def get_shng_data(self, data, type=None, **kwargs): + return True if data == "1" else False From 08c6ae01c067948afd8686314c5dc63cae77054b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 18 Aug 2024 15:20:29 +0200 Subject: [PATCH 045/121] SDP plugins: fix and improve plugin.yaml based on newest changes --- denon/commands.py | 2 +- denon/plugin.yaml | 64 ++- lms/plugin.yaml | 55 +- oppo/plugin.yaml | 1340 ++++++++++++++++++++++--------------------- pioneer/plugin.yaml | 52 +- 5 files changed, 809 insertions(+), 704 deletions(-) diff --git a/denon/commands.py b/denon/commands.py index 20900f8c8..908e3e345 100755 --- a/denon/commands.py +++ b/denon/commands.py @@ -106,7 +106,7 @@ 'volume': {'read': True, 'write': True, 'read_cmd': 'MV?', 'write_cmd': 'MV{VALUE}', 'item_type': 'num', 'dev_datatype': 'DenonVol', 'reply_pattern': r'MV(\d{2,3})', 'cmd_settings': {'force_min': 0.0, 'valid_max': 98.0}, 'item_attrs': {'initial': True}}, 'volumeup': {'read': False, 'write': True, 'item_type': 'bool', 'write_cmd': 'MVUP', 'dev_datatype': 'raw'}, 'volumedown': {'read': False, 'write': True, 'write_cmd': 'MVDOWN', 'item_type': 'bool', 'dev_datatype': 'raw'}, - 'volumemax': {'opcode': '{VALUE}', 'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'MVMAX (\d{2,3})'}, + 'volumemax': {'opcode': '{VALUE}', 'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'MVMAX (\d{2,3})', 'item_attrs': {'read_group_levels': 0}}, 'input': {'read': True, 'write': True, 'read_cmd': 'SI?', 'write_cmd': 'SI{VALUE}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': 'SI{LOOKUP}', 'lookup': 'INPUT', 'item_attrs': {'item_template': 'input', 'initial': True}}, 'listeningmode': {'read': True, 'write': True, 'cmd_settings': {'valid_list_ci': ['MOVIE', 'MUSIC', 'GAME', 'DIRECT', 'PURE DIRECT', 'STEREO', 'AUTO', 'DOLBY DIGITAL', 'DOLBY SURROUND', 'DTS SURROUND', 'NEURAL:X', 'AURO3D', 'AURO2DSURR', 'MCH STEREO', 'ROCK ARENA', 'JAZZ CLUB', 'MONO MOVIE', 'MATRIX', 'VIDEO GAME', 'VIRTUAL', 'LEFT', 'RIGHT']}, 'read_cmd': 'MS?', 'write_cmd': 'MS{RAW_VALUE_UPPER}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'\s?MS(.*)', 'item_attrs': {'initial': True}}, 'sleep': {'read': True, 'write': True, 'item_type': 'num', 'read_cmd': 'SLP?', 'write_cmd': 'SLP{VALUE}', 'dev_datatype': 'convert0', 'reply_pattern': r'SLP(\d{3}|OFF)', 'cmd_settings': {'force_min': 0, 'force_max': 120}, 'item_attrs': {'initial': True}}, diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 0825eb252..7994315b6 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -1,7 +1,10 @@ plugin: type: interface - description: Denon AV-Receiver + + description: + de: Denon AV-Receiver + en: Denon AV-Receiver maintainer: OnkelAndy tester: Morg state: develop @@ -160,17 +163,18 @@ parameters: description: de: Anzahl Sendeversuche en: number of sending retries + description_long: - de: 'Anzahl Sendeversuche\n + de: |4 + Anzahl Sendeversuche\n Kommt keine (passende) Antwort nach dem Senden eines Commands zurück, wird das Kommando nochmals gesendet, sofern der Wert über 0 liegt. - ' - en: 'number of sending retries\n + en: |4 + number of sending retries\n If no (suiting) answer is received after sending a command the command is resent as long as this value is more than 0. - ' sendretry_cycle: type: num @@ -180,17 +184,18 @@ parameters: description: de: Pause zwischen Durchgängen von Sendeversuchen en: wait time between sending retry rounds + description_long: - de: 'Pause zwischen Durchgängen von Sendeversuchen\n + de: |4 + Pause zwischen Durchgängen von Sendeversuchen\n Sind Send Retries aktiv, wird ein Scheduler erstellt, der im angegebenen Sekundentakt Kommandos erneut sendet, zu denen keine (passenden) Antworten erhalten wurden. - ' - en: 'wait time between sending retry rounds\n + en: |4 + wait time between sending retry rounds\n If send retries are active, a scheduler gets added that resends commands in the given cycle value (in seconds) where no (suiting) answer got received. - ' delay_initial_read: type: num @@ -203,9 +208,10 @@ parameters: resume_initial_read: type: bool defaul: false + description: - de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' - en: 'Repeat initial read on resume' + de: Bei resume vom Plugin erstmaliges Lesen erneut durchführen + en: Repeat initial read on resume connect_cycle: type: num @@ -305,8 +311,26 @@ item_attributes: en: The lookup table with the given name will be assigned to the item in dict or list format once on startup. description_long: - de: "Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim\nStart einmalig als dict oder list in das Item geschrieben.\n\n\nDurch Anhängen von \"#\" an den Namen der Tabelle kann die Art\nder Tabelle ausgewählt werden:\n- fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard)\n- rev liefert die Tabelle SmartHomeNG -> Gerät\n- rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben\n- list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu)" - en: "The lookup table with the given name will be assigned to the item\nin dict or list format once on startup.\n\n\nBy appending \"#\" to the tables name the type of table can\nbe selected:\n- fwd returns the table device -> SmartHomeNG (default)\n- rev returns the table SmartHomeNG -> device\n- rci returns the table SmartHomeNG -> device in lower case\n- list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications)" + de: |4- + Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim + Start einmalig als dict oder list in das Item geschrieben. + + Durch Anhängen von "#" an den Namen der Tabelle kann die Art + der Tabelle ausgewählt werden: + - fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard) + - rev liefert die Tabelle SmartHomeNG -> Gerät + - rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben + - list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu) + en: |4- + The lookup table with the given name will be assigned to the item + in dict or list format once on startup. + + By appending "#" to the tables name the type of table can + be selected: + - fwd returns the table device -> SmartHomeNG (default) + - rev returns the table SmartHomeNG -> device + - rci returns the table SmartHomeNG -> device in lower case + - list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications) item_structs: @@ -1620,7 +1644,7 @@ item_structs: - zone2.settings.sound - zone2.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone2.settings.sound.general.HPF denon_read@instance: true @@ -1820,7 +1844,7 @@ item_structs: enforce_updates: true denon_read_group_trigger@instance: zone3.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone3.settings.sound.general.HPF denon_read@instance: true @@ -3897,7 +3921,7 @@ item_structs: - AVR-X6300H.zone2.settings.sound - AVR-X6300H.zone2.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone2.settings.sound.general.HPF denon_read@instance: true @@ -4108,7 +4132,7 @@ item_structs: enforce_updates: true denon_read_group_trigger@instance: AVR-X6300H.zone3.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone3.settings.sound.general.HPF denon_read@instance: true @@ -5123,7 +5147,7 @@ item_structs: - AVR-X4300H.zone2.settings.sound - AVR-X4300H.zone2.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone2.settings.sound.general.HPF denon_read@instance: true @@ -5334,7 +5358,7 @@ item_structs: enforce_updates: true denon_read_group_trigger@instance: AVR-X4300H.zone3.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone3.settings.sound.general.HPF denon_read@instance: true @@ -6357,7 +6381,7 @@ item_structs: - AVR-X3300W.zone2.settings.sound - AVR-X3300W.zone2.settings.sound.general - hpf: + HPF: type: bool denon_command@instance: zone2.settings.sound.general.HPF denon_read@instance: true diff --git a/lms/plugin.yaml b/lms/plugin.yaml index 1c588d8a1..ae83616c7 100755 --- a/lms/plugin.yaml +++ b/lms/plugin.yaml @@ -1,12 +1,14 @@ plugin: type: interface - description: Logitech Mediaserver + description: + de: Logitech Mediaserver + en: Logitech Mediaserver maintainer: OnkelAndy tester: Morg state: develop keywords: iot device logitechmediaserver lms sdp av - version: 1.5.3 + version: '1.5.3' sh_minversion: '1.9.5' py_minversion: '3.7' sdp_minversion: '1.0.3' @@ -135,17 +137,18 @@ parameters: description: de: Anzahl Sendeversuche en: number of sending retries + description_long: - de: 'Anzahl Sendeversuche\n + de: | + Anzahl Sendeversuche\n Kommt keine (passende) Antwort nach dem Senden eines Commands zurück, wird das Kommando nochmals gesendet, sofern der Wert über 0 liegt. - ' - en: 'number of sending retries\n + en: | + number of sending retries\n If no (suiting) answer is received after sending a command the command is resent as long as this value is more than 0. - ' sendretry_cycle: type: num @@ -155,17 +158,18 @@ parameters: description: de: Pause zwischen Durchgängen von Sendeversuchen en: wait time between sending retry rounds + description_long: - de: 'Pause zwischen Durchgängen von Sendeversuchen\n + de: | + Pause zwischen Durchgängen von Sendeversuchen\n Sind Send Retries aktiv, wird ein Scheduler erstellt, der im angegebenen Sekundentakt Kommandos erneut sendet, zu denen keine (passenden) Antworten erhalten wurden. - ' - en: 'wait time between sending retry rounds\n + en: | + wait time between sending retry rounds\n If send retries are active, a scheduler gets added that resends commands in the given cycle value (in seconds) where no (suiting) answer got received. - ' delay_initial_read: type: num @@ -178,9 +182,10 @@ parameters: resume_initial_read: type: bool defaul: false + description: - de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' - en: 'Repeat initial read on resume' + de: Bei resume vom Plugin erstmaliges Lesen erneut durchführen + en: Repeat initial read on resume item_attributes: @@ -241,8 +246,26 @@ item_attributes: en: The lookup table with the given name will be assigned to the item in dict or list format once on startup. description_long: - de: "Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim\nStart einmalig als dict oder list in das Item geschrieben.\n\n\nDurch Anhängen von \"#\" an den Namen der Tabelle kann die Art\nder Tabelle ausgewählt werden:\n- fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard)\n- rev liefert die Tabelle SmartHomeNG -> Gerät\n- rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben\n- list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu)" - en: "The lookup table with the given name will be assigned to the item\nin dict or list format once on startup.\n\n\nBy appending \"#\" to the tables name the type of table can\nbe selected:\n- fwd returns the table device -> SmartHomeNG (default)\n- rev returns the table SmartHomeNG -> device\n- rci returns the table SmartHomeNG -> device in lower case\n- list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications)" + de: |- + Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim + Start einmalig als dict oder list in das Item geschrieben. + + Durch Anhängen von "#" an den Namen der Tabelle kann die Art + der Tabelle ausgewählt werden: + - fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard) + - rev liefert die Tabelle SmartHomeNG -> Gerät + - rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben + - list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu) + en: |- + The lookup table with the given name will be assigned to the item + in dict or list format once on startup. + + By appending "#" to the tables name the type of table can + be selected: + - fwd returns the table device -> SmartHomeNG (default) + - rev returns the table SmartHomeNG -> device + - rci returns the table SmartHomeNG -> device in lower case + - list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications) sqb_custom1: type: str @@ -382,7 +405,7 @@ item_structs: duration_format: type: str eval: "'{}d {}h {}i {}s'.format(int(sh...()//86400), int((sh...()%86400)//3600), int((sh...()%3600)//60), round((sh...()%3600)%60))" - eval_trigger: .. + eval_trigger: '..' totalartists: type: num @@ -1146,7 +1169,7 @@ item_structs: duration_format: type: str eval: "'{}d {}h {}i {}s'.format(int(sh...()//86400), int((sh...()%86400)//3600), int((sh...()%3600)//60), round((sh...()%3600)%60))" - eval_trigger: .. + eval_trigger: '..' totalartists: type: num diff --git a/oppo/plugin.yaml b/oppo/plugin.yaml index c4f91700e..9eb08a1cc 100755 --- a/oppo/plugin.yaml +++ b/oppo/plugin.yaml @@ -1,12 +1,14 @@ plugin: type: interface - description: Oppo UHD Player + description: + de: Oppo UHD Player + en: Oppo UHD Player maintainer: OnkelAndy tester: Morg state: develop keywords: iot device - version: 1.0.1 + version: '1.0.1' sh_minversion: '1.9.5.1' py_minversion: '3.6' sdp_minversion: '1.0.3' @@ -176,17 +178,18 @@ parameters: description: de: Anzahl Sendeversuche en: number of sending retries + description_long: - de: 'Anzahl Sendeversuche\n + de: | + Anzahl Sendeversuche\n Kommt keine (passende) Antwort nach dem Senden eines Commands zurück, wird das Kommando nochmals gesendet, sofern der Wert über 0 liegt. - ' - en: 'number of sending retries\n + en: | + number of sending retries\n If no (suiting) answer is received after sending a command the command is resent as long as this value is more than 0. - ' sendretry_cycle: type: num @@ -196,17 +199,18 @@ parameters: description: de: Pause zwischen Durchgängen von Sendeversuchen en: wait time between sending retry rounds + description_long: - de: 'Pause zwischen Durchgängen von Sendeversuchen\n + de: | + Pause zwischen Durchgängen von Sendeversuchen\n Sind Send Retries aktiv, wird ein Scheduler erstellt, der im angegebenen Sekundentakt Kommandos erneut sendet, zu denen keine (passenden) Antworten erhalten wurden. - ' - en: 'wait time between sending retry rounds\n + en: | + wait time between sending retry rounds\n If send retries are active, a scheduler gets added that resends commands in the given cycle value (in seconds) where no (suiting) answer got received. - ' delay_initial_read: type: num @@ -219,10 +223,10 @@ parameters: resume_initial_read: type: bool defaul: false - description: - de: 'Bei resume vom Plugin erstmaliges Lesen erneut durchführen' - en: 'Repeat initial read on resume' + description: + de: Bei resume vom Plugin erstmaliges Lesen erneut durchführen + en: Repeat initial read on resume item_attributes: @@ -283,8 +287,26 @@ item_attributes: en: The lookup table with the given name will be assigned to the item in dict or list format once on startup. description_long: - de: "Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim\nStart einmalig als dict oder list in das Item geschrieben.\n\n\nDurch Anhängen von \"#\" an den Namen der Tabelle kann die Art\nder Tabelle ausgewählt werden:\n- fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard)\n- rev liefert die Tabelle SmartHomeNG -> Gerät\n- rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben\n- list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu)" - en: "The lookup table with the given name will be assigned to the item\nin dict or list format once on startup.\n\n\nBy appending \"#\" to the tables name the type of table can\nbe selected:\n- fwd returns the table device -> SmartHomeNG (default)\n- rev returns the table SmartHomeNG -> device\n- rci returns the table SmartHomeNG -> device in lower case\n- list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications)" + de: |- + Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim + Start einmalig als dict oder list in das Item geschrieben. + + Durch Anhängen von "#" an den Namen der Tabelle kann die Art + der Tabelle ausgewählt werden: + - fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard) + - rev liefert die Tabelle SmartHomeNG -> Gerät + - rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben + - list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu) + en: |- + The lookup table with the given name will be assigned to the item + in dict or list format once on startup. + + By appending "#" to the tables name the type of table can + be selected: + - fwd returns the table device -> SmartHomeNG (default) + - rev returns the table SmartHomeNG -> device + - rci returns the table SmartHomeNG -> device in lower case + - list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications) item_structs: @@ -293,66 +315,66 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: info + oppo_read_group_trigger@instance: info time: read: type: bool enforce_updates: true - oppo_read_group_trigger: info.time + oppo_read_group_trigger@instance: info.time totalelapsed: type: str - oppo_command: info.time.totalelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time totalremaining: type: str - oppo_command: info.time.totalremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time chapterelapsed: type: str - oppo_command: info.time.chapterelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time chapterremaining: type: str - oppo_command: info.time.chapterremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time titleelapsed: type: str - oppo_command: info.time.titleelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time titleremaining: type: str - oppo_command: info.time.titleremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - info.time @@ -361,421 +383,421 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: info.audio + oppo_read_group_trigger@instance: info.audio current: type: num - oppo_command: info.audio.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.audio.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.audio.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.language + oppo_read@instance: true + oppo_write@instance: false subtitle: read: type: bool enforce_updates: true - oppo_read_group_trigger: info.subtitle + oppo_read_group_trigger@instance: info.subtitle current: type: num - oppo_command: info.subtitle.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.subtitle.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.subtitle.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.language + oppo_read@instance: true + oppo_write@instance: false firmware: type: str - oppo_command: info.firmware - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.firmware + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - oppo_read_initial: true + oppo_read_initial@instance: true status: type: str - oppo_command: info.status - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.status + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - oppo_read_initial: true + oppo_read_initial@instance: true disctype: type: str - oppo_command: info.disctype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.disctype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - oppo_read_initial: true + oppo_read_initial@instance: true totaltracks: type: num - oppo_command: info.totaltracks - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.totaltracks + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - oppo_read_initial: true + oppo_read_initial@instance: true displaytype: type: str - oppo_command: info.displaytype - oppo_read: true - oppo_write: true + oppo_command@instance: info.displaytype + oppo_read@instance: true + oppo_write@instance: true audiotype: type: str - oppo_command: info.audiotype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.audiotype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info - oppo_read_initial: true + oppo_read_initial@instance: true channels: type: str - oppo_command: info.channels - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.channels + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info trackinfo: type: num - oppo_command: info.trackinfo - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.trackinfo + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - info inputresolution: type: str - oppo_command: info.inputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.inputresolution + oppo_read@instance: true + oppo_write@instance: false outputresolution: type: str - oppo_command: info.outputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.outputresolution + oppo_read@instance: true + oppo_write@instance: false aspectratio: type: str - oppo_command: info.aspectratio - oppo_read: true - oppo_write: false + oppo_command@instance: info.aspectratio + oppo_read@instance: true + oppo_write@instance: false U3D: type: str - oppo_command: info.U3D - oppo_read: true - oppo_write: false + oppo_command@instance: info.U3D + oppo_read@instance: true + oppo_write@instance: false general: read: type: bool enforce_updates: true - oppo_read_group_trigger: general + oppo_read_group_trigger@instance: general verbose: type: num - oppo_command: general.verbose - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.verbose + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - general cache: true initial_value: 2 hdmiresolution: type: str - oppo_command: general.hdmiresolution - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.hdmiresolution + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - general - oppo_read_initial: true + oppo_read_initial@instance: true control: read: type: bool enforce_updates: true - oppo_read_group_trigger: control + oppo_read_group_trigger@instance: control power: type: bool - oppo_command: control.power - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.power + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - control - oppo_read_initial: true + oppo_read_initial@instance: true pureaudio: type: bool - oppo_command: control.pureaudio - oppo_read: true - oppo_write: true + oppo_command@instance: control.pureaudio + oppo_read@instance: true + oppo_write@instance: true playpause: type: bool - oppo_command: control.playpause - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.playpause + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - control stop: type: bool - oppo_command: control.stop - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.stop + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - control eject: type: bool - oppo_command: control.eject - oppo_read: true - oppo_write: true + oppo_command@instance: control.eject + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true chapter: type: num - oppo_command: control.chapter - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.chapter + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - control title: type: num - oppo_command: control.title - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.title + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - control next: type: bool - oppo_command: control.next - oppo_read: true - oppo_write: true + oppo_command@instance: control.next + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true previous: type: bool - oppo_command: control.previous - oppo_read: true - oppo_write: true + oppo_command@instance: control.previous + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true forward: type: bool - oppo_command: control.forward - oppo_read: true - oppo_write: true + oppo_command@instance: control.forward + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true reverse: type: bool - oppo_command: control.reverse - oppo_read: true - oppo_write: true + oppo_command@instance: control.reverse + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true audio: type: bool - oppo_command: control.audio - oppo_read: true - oppo_write: true + oppo_command@instance: control.audio + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true subtitle: type: bool - oppo_command: control.subtitle - oppo_read: true - oppo_write: true + oppo_command@instance: control.subtitle + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true repeat: type: num - oppo_command: control.repeat - oppo_read: true - oppo_write: true + oppo_command@instance: control.repeat + oppo_read@instance: true + oppo_write@instance: true input: type: num - oppo_command: control.input - oppo_read: true - oppo_write: true + oppo_command@instance: control.input + oppo_read@instance: true + oppo_write@instance: true menu: read: type: bool enforce_updates: true - oppo_read_group_trigger: menu + oppo_read_group_trigger@instance: menu home: type: bool - oppo_command: menu.home - oppo_read: true - oppo_write: true + oppo_command@instance: menu.home + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true setup: type: bool - oppo_command: menu.setup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.setup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true option: type: bool - oppo_command: menu.option - oppo_read: true - oppo_write: true + oppo_command@instance: menu.option + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true info: type: bool - oppo_command: menu.info - oppo_read: true - oppo_write: true + oppo_command@instance: menu.info + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true popup: type: bool - oppo_command: menu.popup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.popup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true top: type: bool - oppo_command: menu.top - oppo_read: true - oppo_write: true + oppo_command@instance: menu.top + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true osd: type: bool - oppo_command: menu.osd - oppo_read: true - oppo_write: true + oppo_command@instance: menu.osd + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pageup: type: bool - oppo_command: menu.pageup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pageup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pagedown: type: bool - oppo_command: menu.pagedown - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pagedown + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true up: type: bool - oppo_command: menu.up - oppo_read: true - oppo_write: true + oppo_command@instance: menu.up + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true down: type: bool - oppo_command: menu.down - oppo_read: true - oppo_write: true + oppo_command@instance: menu.down + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true left: type: bool - oppo_command: menu.left - oppo_read: true - oppo_write: true + oppo_command@instance: menu.left + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true right: type: bool - oppo_command: menu.right - oppo_read: true - oppo_write: true + oppo_command@instance: menu.right + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true select: type: bool - oppo_command: menu.select - oppo_read: true - oppo_write: true + oppo_command@instance: menu.select + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true back: type: bool - oppo_command: menu.back - oppo_read: true - oppo_write: true + oppo_command@instance: menu.back + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true red: type: bool - oppo_command: menu.red - oppo_read: true - oppo_write: true + oppo_command@instance: menu.red + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true green: type: bool - oppo_command: menu.green - oppo_read: true - oppo_write: true + oppo_command@instance: menu.green + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true blue: type: bool - oppo_command: menu.blue - oppo_read: true - oppo_write: true + oppo_command@instance: menu.blue + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true yellow: type: bool - oppo_command: menu.yellow - oppo_read: true - oppo_write: true + oppo_command@instance: menu.yellow + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true ALL: @@ -783,78 +805,78 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL + oppo_read_group_trigger@instance: ALL info: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.info + oppo_read_group_trigger@instance: ALL.info time: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.info.time + oppo_read_group_trigger@instance: ALL.info.time totalelapsed: type: str - oppo_command: info.time.totalelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time totalremaining: type: str - oppo_command: info.time.totalremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time chapterelapsed: type: str - oppo_command: info.time.chapterelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time chapterremaining: type: str - oppo_command: info.time.chapterremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time titleelapsed: type: str - oppo_command: info.time.titleelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time titleremaining: type: str - oppo_command: info.time.titleremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - ALL.info.time @@ -864,162 +886,162 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.info.audio + oppo_read_group_trigger@instance: ALL.info.audio current: type: num - oppo_command: info.audio.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.audio.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.audio.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.language + oppo_read@instance: true + oppo_write@instance: false subtitle: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.info.subtitle + oppo_read_group_trigger@instance: ALL.info.subtitle current: type: num - oppo_command: info.subtitle.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.subtitle.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.subtitle.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.language + oppo_read@instance: true + oppo_write@instance: false firmware: type: str - oppo_command: info.firmware - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.firmware + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - oppo_read_initial: true + oppo_read_initial@instance: true status: type: str - oppo_command: info.status - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.status + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - oppo_read_initial: true + oppo_read_initial@instance: true disctype: type: str - oppo_command: info.disctype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.disctype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - oppo_read_initial: true + oppo_read_initial@instance: true totaltracks: type: num - oppo_command: info.totaltracks - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.totaltracks + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - oppo_read_initial: true + oppo_read_initial@instance: true displaytype: type: str - oppo_command: info.displaytype - oppo_read: true - oppo_write: true + oppo_command@instance: info.displaytype + oppo_read@instance: true + oppo_write@instance: true audiotype: type: str - oppo_command: info.audiotype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.audiotype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info - oppo_read_initial: true + oppo_read_initial@instance: true channels: type: str - oppo_command: info.channels - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.channels + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info trackinfo: type: num - oppo_command: info.trackinfo - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.trackinfo + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - ALL - ALL.info inputresolution: type: str - oppo_command: info.inputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.inputresolution + oppo_read@instance: true + oppo_write@instance: false outputresolution: type: str - oppo_command: info.outputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.outputresolution + oppo_read@instance: true + oppo_write@instance: false aspectratio: type: str - oppo_command: info.aspectratio - oppo_read: true - oppo_write: false + oppo_command@instance: info.aspectratio + oppo_read@instance: true + oppo_write@instance: false U3D: type: str - oppo_command: info.U3D - oppo_read: true - oppo_write: false + oppo_command@instance: info.U3D + oppo_read@instance: true + oppo_write@instance: false general: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.general + oppo_read_group_trigger@instance: ALL.general verbose: type: num - oppo_command: general.verbose - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.verbose + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.general cache: true @@ -1027,272 +1049,272 @@ item_structs: hdmiresolution: type: str - oppo_command: general.hdmiresolution - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.hdmiresolution + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.general - oppo_read_initial: true + oppo_read_initial@instance: true control: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.control + oppo_read_group_trigger@instance: ALL.control power: type: bool - oppo_command: control.power - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.power + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.control - oppo_read_initial: true + oppo_read_initial@instance: true pureaudio: type: bool - oppo_command: control.pureaudio - oppo_read: true - oppo_write: true + oppo_command@instance: control.pureaudio + oppo_read@instance: true + oppo_write@instance: true playpause: type: bool - oppo_command: control.playpause - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.playpause + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.control stop: type: bool - oppo_command: control.stop - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.stop + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.control eject: type: bool - oppo_command: control.eject - oppo_read: true - oppo_write: true + oppo_command@instance: control.eject + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true chapter: type: num - oppo_command: control.chapter - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.chapter + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.control title: type: num - oppo_command: control.title - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.title + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - ALL - ALL.control next: type: bool - oppo_command: control.next - oppo_read: true - oppo_write: true + oppo_command@instance: control.next + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true previous: type: bool - oppo_command: control.previous - oppo_read: true - oppo_write: true + oppo_command@instance: control.previous + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true forward: type: bool - oppo_command: control.forward - oppo_read: true - oppo_write: true + oppo_command@instance: control.forward + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true reverse: type: bool - oppo_command: control.reverse - oppo_read: true - oppo_write: true + oppo_command@instance: control.reverse + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true audio: type: bool - oppo_command: control.audio - oppo_read: true - oppo_write: true + oppo_command@instance: control.audio + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true subtitle: type: bool - oppo_command: control.subtitle - oppo_read: true - oppo_write: true + oppo_command@instance: control.subtitle + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true repeat: type: num - oppo_command: control.repeat - oppo_read: true - oppo_write: true + oppo_command@instance: control.repeat + oppo_read@instance: true + oppo_write@instance: true input: type: num - oppo_command: control.input - oppo_read: true - oppo_write: true + oppo_command@instance: control.input + oppo_read@instance: true + oppo_write@instance: true menu: read: type: bool enforce_updates: true - oppo_read_group_trigger: ALL.menu + oppo_read_group_trigger@instance: ALL.menu home: type: bool - oppo_command: menu.home - oppo_read: true - oppo_write: true + oppo_command@instance: menu.home + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true setup: type: bool - oppo_command: menu.setup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.setup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true option: type: bool - oppo_command: menu.option - oppo_read: true - oppo_write: true + oppo_command@instance: menu.option + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true info: type: bool - oppo_command: menu.info - oppo_read: true - oppo_write: true + oppo_command@instance: menu.info + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true popup: type: bool - oppo_command: menu.popup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.popup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true top: type: bool - oppo_command: menu.top - oppo_read: true - oppo_write: true + oppo_command@instance: menu.top + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true osd: type: bool - oppo_command: menu.osd - oppo_read: true - oppo_write: true + oppo_command@instance: menu.osd + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pageup: type: bool - oppo_command: menu.pageup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pageup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pagedown: type: bool - oppo_command: menu.pagedown - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pagedown + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true up: type: bool - oppo_command: menu.up - oppo_read: true - oppo_write: true + oppo_command@instance: menu.up + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true down: type: bool - oppo_command: menu.down - oppo_read: true - oppo_write: true + oppo_command@instance: menu.down + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true left: type: bool - oppo_command: menu.left - oppo_read: true - oppo_write: true + oppo_command@instance: menu.left + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true right: type: bool - oppo_command: menu.right - oppo_read: true - oppo_write: true + oppo_command@instance: menu.right + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true select: type: bool - oppo_command: menu.select - oppo_read: true - oppo_write: true + oppo_command@instance: menu.select + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true back: type: bool - oppo_command: menu.back - oppo_read: true - oppo_write: true + oppo_command@instance: menu.back + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true red: type: bool - oppo_command: menu.red - oppo_read: true - oppo_write: true + oppo_command@instance: menu.red + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true green: type: bool - oppo_command: menu.green - oppo_read: true - oppo_write: true + oppo_command@instance: menu.green + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true blue: type: bool - oppo_command: menu.blue - oppo_read: true - oppo_write: true + oppo_command@instance: menu.blue + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true yellow: type: bool - oppo_command: menu.yellow - oppo_read: true - oppo_write: true + oppo_command@instance: menu.yellow + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true UDP-203: @@ -1300,78 +1322,78 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203 + oppo_read_group_trigger@instance: UDP-203 info: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.info + oppo_read_group_trigger@instance: UDP-203.info time: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.info.time + oppo_read_group_trigger@instance: UDP-203.info.time totalelapsed: type: str - oppo_command: info.time.totalelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time totalremaining: type: str - oppo_command: info.time.totalremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.totalremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time chapterelapsed: type: str - oppo_command: info.time.chapterelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time chapterremaining: type: str - oppo_command: info.time.chapterremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.chapterremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time titleelapsed: type: str - oppo_command: info.time.titleelapsed - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleelapsed + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time titleremaining: type: str - oppo_command: info.time.titleremaining - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.time.titleremaining + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - UDP-203.info.time @@ -1381,162 +1403,162 @@ item_structs: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.info.audio + oppo_read_group_trigger@instance: UDP-203.info.audio current: type: num - oppo_command: info.audio.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.audio.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.audio.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.audio.language + oppo_read@instance: true + oppo_write@instance: false subtitle: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.info.subtitle + oppo_read_group_trigger@instance: UDP-203.info.subtitle current: type: num - oppo_command: info.subtitle.current - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.current + oppo_read@instance: true + oppo_write@instance: false available: type: num - oppo_command: info.subtitle.available - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.available + oppo_read@instance: true + oppo_write@instance: false language: type: str - oppo_command: info.subtitle.language - oppo_read: true - oppo_write: false + oppo_command@instance: info.subtitle.language + oppo_read@instance: true + oppo_write@instance: false firmware: type: str - oppo_command: info.firmware - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.firmware + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - oppo_read_initial: true + oppo_read_initial@instance: true status: type: str - oppo_command: info.status - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.status + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - oppo_read_initial: true + oppo_read_initial@instance: true disctype: type: str - oppo_command: info.disctype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.disctype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - oppo_read_initial: true + oppo_read_initial@instance: true totaltracks: type: num - oppo_command: info.totaltracks - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.totaltracks + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - oppo_read_initial: true + oppo_read_initial@instance: true displaytype: type: str - oppo_command: info.displaytype - oppo_read: true - oppo_write: true + oppo_command@instance: info.displaytype + oppo_read@instance: true + oppo_write@instance: true audiotype: type: str - oppo_command: info.audiotype - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.audiotype + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info - oppo_read_initial: true + oppo_read_initial@instance: true channels: type: str - oppo_command: info.channels - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.channels + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info trackinfo: type: num - oppo_command: info.trackinfo - oppo_read: true - oppo_write: false - oppo_read_group: + oppo_command@instance: info.trackinfo + oppo_read@instance: true + oppo_write@instance: false + oppo_read_group@instance: - UDP-203 - UDP-203.info inputresolution: type: str - oppo_command: info.inputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.inputresolution + oppo_read@instance: true + oppo_write@instance: false outputresolution: type: str - oppo_command: info.outputresolution - oppo_read: true - oppo_write: false + oppo_command@instance: info.outputresolution + oppo_read@instance: true + oppo_write@instance: false aspectratio: type: str - oppo_command: info.aspectratio - oppo_read: true - oppo_write: false + oppo_command@instance: info.aspectratio + oppo_read@instance: true + oppo_write@instance: false U3D: type: str - oppo_command: info.U3D - oppo_read: true - oppo_write: false + oppo_command@instance: info.U3D + oppo_read@instance: true + oppo_write@instance: false general: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.general + oppo_read_group_trigger@instance: UDP-203.general verbose: type: num - oppo_command: general.verbose - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.verbose + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.general cache: true @@ -1544,272 +1566,272 @@ item_structs: hdmiresolution: type: str - oppo_command: general.hdmiresolution - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: general.hdmiresolution + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.general - oppo_read_initial: true + oppo_read_initial@instance: true control: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.control + oppo_read_group_trigger@instance: UDP-203.control power: type: bool - oppo_command: control.power - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.power + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.control - oppo_read_initial: true + oppo_read_initial@instance: true pureaudio: type: bool - oppo_command: control.pureaudio - oppo_read: true - oppo_write: true + oppo_command@instance: control.pureaudio + oppo_read@instance: true + oppo_write@instance: true playpause: type: bool - oppo_command: control.playpause - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.playpause + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.control stop: type: bool - oppo_command: control.stop - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.stop + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.control eject: type: bool - oppo_command: control.eject - oppo_read: true - oppo_write: true + oppo_command@instance: control.eject + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true chapter: type: num - oppo_command: control.chapter - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.chapter + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.control title: type: num - oppo_command: control.title - oppo_read: true - oppo_write: true - oppo_read_group: + oppo_command@instance: control.title + oppo_read@instance: true + oppo_write@instance: true + oppo_read_group@instance: - UDP-203 - UDP-203.control next: type: bool - oppo_command: control.next - oppo_read: true - oppo_write: true + oppo_command@instance: control.next + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true previous: type: bool - oppo_command: control.previous - oppo_read: true - oppo_write: true + oppo_command@instance: control.previous + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true forward: type: bool - oppo_command: control.forward - oppo_read: true - oppo_write: true + oppo_command@instance: control.forward + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true reverse: type: bool - oppo_command: control.reverse - oppo_read: true - oppo_write: true + oppo_command@instance: control.reverse + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true audio: type: bool - oppo_command: control.audio - oppo_read: true - oppo_write: true + oppo_command@instance: control.audio + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true subtitle: type: bool - oppo_command: control.subtitle - oppo_read: true - oppo_write: true + oppo_command@instance: control.subtitle + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true repeat: type: num - oppo_command: control.repeat - oppo_read: true - oppo_write: true + oppo_command@instance: control.repeat + oppo_read@instance: true + oppo_write@instance: true input: type: num - oppo_command: control.input - oppo_read: true - oppo_write: true + oppo_command@instance: control.input + oppo_read@instance: true + oppo_write@instance: true menu: read: type: bool enforce_updates: true - oppo_read_group_trigger: UDP-203.menu + oppo_read_group_trigger@instance: UDP-203.menu home: type: bool - oppo_command: menu.home - oppo_read: true - oppo_write: true + oppo_command@instance: menu.home + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true setup: type: bool - oppo_command: menu.setup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.setup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true option: type: bool - oppo_command: menu.option - oppo_read: true - oppo_write: true + oppo_command@instance: menu.option + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true info: type: bool - oppo_command: menu.info - oppo_read: true - oppo_write: true + oppo_command@instance: menu.info + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true popup: type: bool - oppo_command: menu.popup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.popup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true top: type: bool - oppo_command: menu.top - oppo_read: true - oppo_write: true + oppo_command@instance: menu.top + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true osd: type: bool - oppo_command: menu.osd - oppo_read: true - oppo_write: true + oppo_command@instance: menu.osd + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pageup: type: bool - oppo_command: menu.pageup - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pageup + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true pagedown: type: bool - oppo_command: menu.pagedown - oppo_read: true - oppo_write: true + oppo_command@instance: menu.pagedown + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true up: type: bool - oppo_command: menu.up - oppo_read: true - oppo_write: true + oppo_command@instance: menu.up + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true down: type: bool - oppo_command: menu.down - oppo_read: true - oppo_write: true + oppo_command@instance: menu.down + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true left: type: bool - oppo_command: menu.left - oppo_read: true - oppo_write: true + oppo_command@instance: menu.left + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true right: type: bool - oppo_command: menu.right - oppo_read: true - oppo_write: true + oppo_command@instance: menu.right + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true select: type: bool - oppo_command: menu.select - oppo_read: true - oppo_write: true + oppo_command@instance: menu.select + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true back: type: bool - oppo_command: menu.back - oppo_read: true - oppo_write: true + oppo_command@instance: menu.back + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true red: type: bool - oppo_command: menu.red - oppo_read: true - oppo_write: true + oppo_command@instance: menu.red + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true green: type: bool - oppo_command: menu.green - oppo_read: true - oppo_write: true + oppo_command@instance: menu.green + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true blue: type: bool - oppo_command: menu.blue - oppo_read: true - oppo_write: true + oppo_command@instance: menu.blue + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true yellow: type: bool - oppo_command: menu.yellow - oppo_read: true - oppo_write: true + oppo_command@instance: menu.yellow + oppo_read@instance: true + oppo_write@instance: true enforce_updates: true plugin_functions: NONE logic_parameters: NONE diff --git a/pioneer/plugin.yaml b/pioneer/plugin.yaml index 2fa473f60..d17bb76eb 100755 --- a/pioneer/plugin.yaml +++ b/pioneer/plugin.yaml @@ -1,12 +1,14 @@ plugin: type: interface - description: Pioneer AV-Receiver + description: + de: Pioneer AV-Receiver + en: Pioneer AV-Receiver maintainer: OnkelAndy tester: Morg state: develop keywords: iot device av pioneer sdp - version: 1.0.3 + version: '1.0.3' sh_minversion: '1.9.5' py_minversion: '3.7' sdp_minversion: '1.0.3' @@ -128,8 +130,16 @@ parameters: en: number of sending retries description_long: - de: "Anzahl Sendeversuche\\n\nKommt keine (passende) Antwort nach dem Senden\neines Commands zurück, wird das Kommando nochmals\ngesendet, sofern der Wert über 0 liegt.\n" - en: "number of sending retries\\n\nIf no (suiting) answer is received after sending\na command the command is resent as long as this\nvalue is more than 0.\n" + de: | + Anzahl Sendeversuche\n + Kommt keine (passende) Antwort nach dem Senden + eines Commands zurück, wird das Kommando nochmals + gesendet, sofern der Wert über 0 liegt. + en: | + number of sending retries\n + If no (suiting) answer is received after sending + a command the command is resent as long as this + value is more than 0. sendretry_cycle: type: num @@ -141,8 +151,16 @@ parameters: en: wait time between sending retry rounds description_long: - de: "Pause zwischen Durchgängen von Sendeversuchen\\n\nSind Send Retries aktiv, wird ein Scheduler erstellt,\nder im angegebenen Sekundentakt Kommandos erneut sendet,\nzu denen keine (passenden) Antworten erhalten wurden.\n" - en: "wait time between sending retry rounds\\n\nIf send retries are active, a scheduler gets added\nthat resends commands in the given cycle value (in seconds)\nwhere no (suiting) answer got received.\n" + de: | + Pause zwischen Durchgängen von Sendeversuchen\n + Sind Send Retries aktiv, wird ein Scheduler erstellt, + der im angegebenen Sekundentakt Kommandos erneut sendet, + zu denen keine (passenden) Antworten erhalten wurden. + en: | + wait time between sending retry rounds\n + If send retries are active, a scheduler gets added + that resends commands in the given cycle value (in seconds) + where no (suiting) answer got received. delay_initial_read: type: num @@ -287,8 +305,26 @@ item_attributes: en: The lookup table with the given name will be assigned to the item in dict or list format once on startup. description_long: - de: "Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim\nStart einmalig als dict oder list in das Item geschrieben.\n\n\nDurch Anhängen von \"#\" an den Namen der Tabelle kann die Art\nder Tabelle ausgewählt werden:\n- fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard)\n- rev liefert die Tabelle SmartHomeNG -> Gerät\n- rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben\n- list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu)" - en: "The lookup table with the given name will be assigned to the item\nin dict or list format once on startup.\n\n\nBy appending \"#\" to the tables name the type of table can\nbe selected:\n- fwd returns the table device -> SmartHomeNG (default)\n- rev returns the table SmartHomeNG -> device\n- rci returns the table SmartHomeNG -> device in lower case\n- list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications)" + de: |- + Der Inhalt der Lookup-Tabelle mit dem angegebenen Namen wird beim + Start einmalig als dict oder list in das Item geschrieben. + + Durch Anhängen von "#" an den Namen der Tabelle kann die Art + der Tabelle ausgewählt werden: + - fwd liefert die Tabelle Gerät -> SmartHomeNG (Standard) + - rev liefert die Tabelle SmartHomeNG -> Gerät + - rci liefert die Tabelle SmarthomeNG -> Gerät in Kleinbuchstaben + - list liefert die Liste der Namen für SmartHomeNG (z.B. für Auswahllisten in der Visu) + en: |- + The lookup table with the given name will be assigned to the item + in dict or list format once on startup. + + By appending "#" to the tables name the type of table can + be selected: + - fwd returns the table device -> SmartHomeNG (default) + - rev returns the table SmartHomeNG -> device + - rci returns the table SmartHomeNG -> device in lower case + - list return the list of names for SmartHomeNG (e.g. for selection dropdowns in visu applications) item_structs: From 6fe68398bd89f257be8e0c5195571fc26d54c752 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 18 Aug 2024 20:43:23 +0200 Subject: [PATCH 046/121] uzsu plugin: improve struct for "next" item --- uzsu/plugin.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml index 12c7eb150..f03cf0f8a 100755 --- a/uzsu/plugin.yaml +++ b/uzsu/plugin.yaml @@ -149,6 +149,7 @@ item_structs: visu_acl: ro eval: sh...planned() eval_trigger: .. + initial_value: "{'value': '-', 'next': '-'}" value: remark: The next value @@ -156,7 +157,6 @@ item_structs: visu_acl: ro eval: sh...property.value['value'] eval_trigger: .. - initial_value: '-' time: remark: The next time @@ -164,7 +164,6 @@ item_structs: visu_acl: ro eval: sh...property.value['next'] eval_trigger: .. - initial_value: '-' logic_parameters: NONE # Definition of logic parameters defined by this plugin From e024abe885bdf33a31a7c3e8cb4362cf621f1010 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 18 Aug 2024 22:57:53 +0200 Subject: [PATCH 047/121] oppo plugin: remove useless lists in commands --- oppo/commands.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/oppo/commands.py b/oppo/commands.py index 1c303d97f..38a9cadb3 100755 --- a/oppo/commands.py +++ b/oppo/commands.py @@ -58,35 +58,35 @@ '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}}, - 'previous': {'read': True, 'write': True, 'write_cmd': '#PRE', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@PRE (.*)'], 'item_attrs': {'enforce': True}}, - 'forward': {'read': True, 'write': True, 'write_cmd': '#FWD', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@FWD (.*) 1x'], 'item_attrs': {'enforce': True}}, - 'reverse': {'read': True, 'write': True, 'write_cmd': '#REV', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@REV (.*) 1x'], 'item_attrs': {'enforce': True}}, + 'next': {'read': True, 'write': True, 'write_cmd': '#NXT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@NXT (.*)', 'item_attrs': {'enforce': True}}, + 'previous': {'read': True, 'write': True, 'write_cmd': '#PRE', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@PRE (.*)', 'item_attrs': {'enforce': True}}, + 'forward': {'read': True, 'write': True, 'write_cmd': '#FWD', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@FWD (.*) 1x', 'item_attrs': {'enforce': True}}, + 'reverse': {'read': True, 'write': True, 'write_cmd': '#REV', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@REV (.*) 1x', 'item_attrs': {'enforce': True}}, 'audio': {'read': True, 'write': True, 'write_cmd': '#AUD', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@AUD (.*)', 'item_attrs': {'enforce': True}}, 'subtitle': {'read': True, 'write': True, 'write_cmd': '#SUB', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@SUB (.*)', 'item_attrs': {'enforce': True}}, - 'repeat': {'read': True, 'write': True, 'write_cmd': '#RPT', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': ['@RPT OK {LOOKUP}$'], 'lookup': 'REPEAT'}, - 'input': {'read': True, 'write': True, 'write_cmd': '#SRC\r#NU{VALUE}', 'item_type': 'num', 'dev_datatype': 'ok', 'reply_pattern': ['@SRC (.*)']}, + 'repeat': {'read': True, 'write': True, 'write_cmd': '#RPT', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': '@RPT OK {LOOKUP}$', 'lookup': 'REPEAT'}, + 'input': {'read': True, 'write': True, 'write_cmd': '#SRC\r#NU{VALUE}', 'item_type': 'num', 'dev_datatype': 'ok', 'reply_pattern': '@SRC (.*)'}, }, 'menu': { 'home': {'read': True, 'write': True, 'write_cmd': '#HOM', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@HOM (.*)', 'item_attrs': {'enforce': True}}, - 'setup': {'read': True, 'write': True, 'write_cmd': '#SET', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@SET (.*)'], 'item_attrs': {'enforce': True}}, - 'option': {'read': True, 'write': True, 'write_cmd': '#OPT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@OPT (.*)'], 'item_attrs': {'enforce': True}}, - 'info': {'read': True, 'write': True, 'write_cmd': '#INH', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@INH (.*)'], 'item_attrs': {'enforce': True}}, - 'popup': {'read': True, 'write': True, 'write_cmd': '#MNU', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@MNU (.*)'], 'item_attrs': {'enforce': True}}, - 'top': {'read': True, 'write': True, 'write_cmd': '#TTL', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@TTL (.*)'], 'item_attrs': {'enforce': True}}, - 'osd': {'read': True, 'write': True, 'write_cmd': '#OSD', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@OSD (.*)'], 'item_attrs': {'enforce': True}}, - 'pageup': {'read': True, 'write': True, 'write_cmd': '#PUP', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@PUP (.*)'], 'item_attrs': {'enforce': True}}, - 'pagedown': {'read': True, 'write': True, 'write_cmd': '#PDN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@PDN (.*)'], 'item_attrs': {'enforce': True}}, - 'up': {'read': True, 'write': True, 'write_cmd': '#NUP', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@NUP (.*)'], 'item_attrs': {'enforce': True}}, - 'down': {'read': True, 'write': True, 'write_cmd': '#NDN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@NDN (.*)'], 'item_attrs': {'enforce': True}}, - 'left': {'read': True, 'write': True, 'write_cmd': '#NLT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@NLT (.*)'], 'item_attrs': {'enforce': True}}, - 'right': {'read': True, 'write': True, 'write_cmd': '#NRT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@NRT (.*)'], 'item_attrs': {'enforce': True}}, - 'select': {'read': True, 'write': True, 'write_cmd': '#SEL', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@SEL (.*)'], 'item_attrs': {'enforce': True}}, - 'back': {'read': True, 'write': True, 'write_cmd': '#RET', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@RET (.*)'], 'item_attrs': {'enforce': True}}, - 'red': {'read': True, 'write': True, 'write_cmd': '#RED', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@RED (.*)'], 'item_attrs': {'enforce': True}}, - 'green': {'read': True, 'write': True, 'write_cmd': '#GRN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@GRN (.*)'], 'item_attrs': {'enforce': True}}, - 'blue': {'read': True, 'write': True, 'write_cmd': '#BLU', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@BLU (.*)'], 'item_attrs': {'enforce': True}}, - 'yellow': {'read': True, 'write': True, 'write_cmd': '#YLW', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': ['@YLW (.*)'], 'item_attrs': {'enforce': True}}, + 'setup': {'read': True, 'write': True, 'write_cmd': '#SET', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@SET (.*)', 'item_attrs': {'enforce': True}}, + 'option': {'read': True, 'write': True, 'write_cmd': '#OPT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@OPT (.*)', 'item_attrs': {'enforce': True}}, + 'info': {'read': True, 'write': True, 'write_cmd': '#INH', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@INH (.*)', 'item_attrs': {'enforce': True}}, + 'popup': {'read': True, 'write': True, 'write_cmd': '#MNU', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@MNU (.*)', 'item_attrs': {'enforce': True}}, + 'top': {'read': True, 'write': True, 'write_cmd': '#TTL', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@TTL (.*)', 'item_attrs': {'enforce': True}}, + 'osd': {'read': True, 'write': True, 'write_cmd': '#OSD', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@OSD (.*)', 'item_attrs': {'enforce': True}}, + 'pageup': {'read': True, 'write': True, 'write_cmd': '#PUP', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@PUP (.*)', 'item_attrs': {'enforce': True}}, + 'pagedown': {'read': True, 'write': True, 'write_cmd': '#PDN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@PDN (.*)', 'item_attrs': {'enforce': True}}, + 'up': {'read': True, 'write': True, 'write_cmd': '#NUP', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@NUP (.*)', 'item_attrs': {'enforce': True}}, + 'down': {'read': True, 'write': True, 'write_cmd': '#NDN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@NDN (.*)', 'item_attrs': {'enforce': True}}, + 'left': {'read': True, 'write': True, 'write_cmd': '#NLT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@NLT (.*)', 'item_attrs': {'enforce': True}}, + 'right': {'read': True, 'write': True, 'write_cmd': '#NRT', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@NRT (.*)', 'item_attrs': {'enforce': True}}, + 'select': {'read': True, 'write': True, 'write_cmd': '#SEL', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@SEL (.*)', 'item_attrs': {'enforce': True}}, + 'back': {'read': True, 'write': True, 'write_cmd': '#RET', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@RET (.*)', 'item_attrs': {'enforce': True}}, + 'red': {'read': True, 'write': True, 'write_cmd': '#RED', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@RED (.*)', 'item_attrs': {'enforce': True}}, + 'green': {'read': True, 'write': True, 'write_cmd': '#GRN', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@GRN (.*)', 'item_attrs': {'enforce': True}}, + 'blue': {'read': True, 'write': True, 'write_cmd': '#BLU', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@BLU (.*)', 'item_attrs': {'enforce': True}}, + 'yellow': {'read': True, 'write': True, 'write_cmd': '#YLW', 'item_type': 'bool', 'dev_datatype': 'ok', 'reply_pattern': '@YLW (.*)', 'item_attrs': {'enforce': True}}, } } From aca09b33b1325d44d06610927acbd7298b0604a0 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Fri, 23 Aug 2024 08:54:15 +0200 Subject: [PATCH 048/121] kodi: adjust to latest sdp changes, cleanup plugin.yaml --- kodi/__init__.py | 11 +++++++++-- kodi/plugin.yaml | 37 ++----------------------------------- 2 files changed, 11 insertions(+), 37 deletions(-) diff --git a/kodi/__init__.py b/kodi/__init__.py index 6d654583b..a5f3159d5 100644 --- a/kodi/__init__.py +++ b/kodi/__init__.py @@ -40,7 +40,9 @@ class SmartPluginWebIf(): else: builtins.SDP_standalone = False -from lib.model.sdp.globals import JSON_MOVE_KEYS +from lib.model.sdp.globals import (JSON_MOVE_KEYS, PLUGIN_ATTR_CMD_CLASS, PLUGIN_ATTR_PROTOCOL, + PROTO_JSONRPC, PLUGIN_ATTR_CONNECTION, CONN_NET_TCP_CLI) + from lib.model.smartdeviceplugin import SmartDevicePlugin, Standalone # from .webif import WebInterface @@ -85,7 +87,12 @@ class kodi(SmartDevicePlugin): def _set_device_defaults(self): self._use_callbacks = True - self._parameters[JSON_MOVE_KEYS] = ['playerid', 'properties'] + self._parameters.update({ + JSON_MOVE_KEYS: ['playerid', 'properties'], + PLUGIN_ATTR_CONNECTION: CONN_NET_TCP_CLI, + PLUGIN_ATTR_PROTOCOL: PROTO_JSONRPC, + PLUGIN_ATTR_CMD_CLASS: 'SDPCommandJSON' + }) def _post_init(self): self._activeplayers = [] diff --git a/kodi/plugin.yaml b/kodi/plugin.yaml index 13266def9..af8eaa34f 100644 --- a/kodi/plugin.yaml +++ b/kodi/plugin.yaml @@ -102,7 +102,7 @@ parameters: de: Anzahl von Durchgängen vor Verbindungsabbruch oder Suspend-Modus en: number of connect rounds before giving up / entering suspend mode - message_timeout: + send_timeout: type: num default: 5 @@ -110,7 +110,7 @@ parameters: de: Timeout für Antwort auf Protokollebene en: timeout for reply at protocol level - message_repeat: + send_retries: type: num default: 3 @@ -126,39 +126,6 @@ parameters: de: Port für Netzwerkverbindung en: network port - conn_type: - type: str - default: net_tcp_client - valid_list: - - '' - - net_tcp_client - - description: - de: Verbindungstyp - en: connection type - - protocol: - type: str - default: jsonrpc - valid_list: - - '' - - jsonrpc - - description: - de: Protokolltyp für Verbindung - en: protocol type for connection - - command_class: - type: str - default: SDPCommandJSON - valid_list: - - SDPCommand - - SDPCommandJSON - - description: - de: Klasse für Verarbeitung von Kommandos - en: class for command processing - item_attributes: kodi_command: From c9b65bc2ad7e1d0e51f1029b2c06733584d46da1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 30 Aug 2024 17:40:54 +0200 Subject: [PATCH 049/121] denon plugin: do not overwrite on_connect anymore, but read custom inputs with other "initial reads" --- denon/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/denon/__init__.py b/denon/__init__.py index 04f83d4c0..275efa482 100755 --- a/denon/__init__.py +++ b/denon/__init__.py @@ -57,10 +57,6 @@ class denon(SmartDevicePlugin): PLUGIN_VERSION = '1.0.1' - def on_connect(self, by=None): - self.logger.debug("Checking for custom input names.") - self.send_command('general.custom_inputnames') - def _set_device_defaults(self): self._use_callbacks = True self._custom_inputnames = {} From d12cada20adf6ed9f5ac98ad54a2150e3923d85b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 30 Aug 2024 17:41:09 +0200 Subject: [PATCH 050/121] denon plugin: update commands and plugin.yaml --- denon/commands.py | 52 +++++------ denon/plugin.yaml | 227 +++++++++++++++++----------------------------- 2 files changed, 109 insertions(+), 170 deletions(-) diff --git a/denon/commands.py b/denon/commands.py index 908e3e345..041c178c1 100755 --- a/denon/commands.py +++ b/denon/commands.py @@ -36,32 +36,32 @@ 'info': { 'fullmodel': {'read': True, 'write': False, 'read_cmd': 'NSFRN ?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'NSFRN\s(.*)', 'item_attrs': {'initial': True}}, 'model': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': 'VIALL(AVR.*)', 'item_attrs': {'initial': True}}, - 'serialnumber': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLS/N\.(.*)'}, - 'main': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLMAIN:(.*)'}, - 'mainfbl': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLMAINFBL:(.*)'}, - 'dsp1': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP1:(.*)'}, - 'dsp2': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP2:(.*)'}, - 'dsp3': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP3:(.*)'}, - 'dsp4': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP4:(.*)'}, - 'apld': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLAPLD:(.*)'}, - 'vpld': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLVPLD:(.*)'}, - 'guidat': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLGUIDAT:(.*)'}, - 'heosversion': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSVER:(.*)'}, - 'heosbuild': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSBLD:(.*)'}, - 'heosmod': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSMOD:(.*)'}, - 'heoscnf': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLHEOSCNF:(.*)'}, - 'heoslanguage': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLHEOSLCL:(.*)'}, - 'mac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLMAC:(.*)'}, - 'wifimac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLWIFIMAC:(.*)'}, - 'btmac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLBTMAC:(.*)'}, - 'audyif': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLAUDYIF:(.*)'}, - 'productid': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLPRODUCTID:(.*)'}, - 'packageid': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLPACKAGEID:(.*)'}, - 'cmp': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLCMP:(.*)'}, + 'serialnumber': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLS/N\.(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'main': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLMAIN:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'mainfbl': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLMAINFBL:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'dsp1': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP1:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'dsp2': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP2:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'dsp3': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP3:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'dsp4': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLDSP4:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'apld': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLAPLD:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'vpld': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLVPLD:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'guidat': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLGUIDAT:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'heosversion': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSVER:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'heosbuild': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSBLD:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'heosmod': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLHEOSMOD:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'heoscnf': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLHEOSCNF:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'heoslanguage': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLHEOSLCL:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'mac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLMAC:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'wifimac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLWIFIMAC:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'btmac': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLBTMAC:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'audyif': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLAUDYIF:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'productid': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLPRODUCTID:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'packageid': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'VIALLPACKAGEID:(.*)', 'item_attrs': {'read_group_levels': 0}}, + 'cmp': {'read': True, 'write': False, 'read_cmd': 'VIALL?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'VIALLCMP:(.*)', 'item_attrs': {'read_group_levels': 0}}, 'region': {'read': True, 'write': False, 'read_cmd': 'SYMODTUN ?', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'SYMODTUN\s(.*)', 'item_attrs': {'initial': True}}, }, 'general': { - 'custom_inputnames': {'read': True, 'write': False, 'read_cmd': 'SSFUN ?', 'item_type': 'dict', 'dev_datatype': 'DenonCustominput', 'reply_pattern': 'SSFUN(.*)', 'item_attrs': {'item_template': 'custom_inputnames'}}, + 'custom_inputnames': {'read': True, 'write': False, 'read_cmd': 'SSFUN ?', 'item_type': 'dict', 'dev_datatype': 'DenonCustominput', 'reply_pattern': 'SSFUN(.*)', 'item_attrs': {'read_groups': [{'name': 'custom_inputnames', 'trigger': 'update'}], 'initial': True, 'item_template': 'custom_inputnames'}}, 'power': {'read': True, 'write': True, 'read_cmd': 'PW?', 'write_cmd': 'PW{VALUE}', 'item_type': 'bool', 'dev_datatype': 'str', 'reply_pattern': 'PW{LOOKUP}', 'lookup': 'POWER'}, 'setupmenu': {'read': True, 'write': True, 'read_cmd': 'MNMEN?', 'write_cmd': 'MNMEN {VALUE}', 'item_type': 'bool', 'dev_datatype': 'onoff', 'reply_pattern': 'MNMEN (ON|OFF)'}, 'display': {'read': True, 'write': False, 'read_cmd': 'NSE', 'item_type': 'str', 'dev_datatype': 'DenonDisplay', 'reply_pattern': 'NSE(.*)'}, @@ -108,7 +108,7 @@ 'volumedown': {'read': False, 'write': True, 'write_cmd': 'MVDOWN', 'item_type': 'bool', 'dev_datatype': 'raw'}, 'volumemax': {'opcode': '{VALUE}', 'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'str', 'reply_pattern': r'MVMAX (\d{2,3})', 'item_attrs': {'read_group_levels': 0}}, 'input': {'read': True, 'write': True, 'read_cmd': 'SI?', 'write_cmd': 'SI{VALUE}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': 'SI{LOOKUP}', 'lookup': 'INPUT', 'item_attrs': {'item_template': 'input', 'initial': True}}, - 'listeningmode': {'read': True, 'write': True, 'cmd_settings': {'valid_list_ci': ['MOVIE', 'MUSIC', 'GAME', 'DIRECT', 'PURE DIRECT', 'STEREO', 'AUTO', 'DOLBY DIGITAL', 'DOLBY SURROUND', 'DTS SURROUND', 'NEURAL:X', 'AURO3D', 'AURO2DSURR', 'MCH STEREO', 'ROCK ARENA', 'JAZZ CLUB', 'MONO MOVIE', 'MATRIX', 'VIDEO GAME', 'VIRTUAL', 'LEFT', 'RIGHT']}, 'read_cmd': 'MS?', 'write_cmd': 'MS{RAW_VALUE_UPPER}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'\s?MS(.*)', 'item_attrs': {'initial': True}}, + 'listeningmode': {'read': True, 'write': True, 'cmd_settings': {'valid_list_ci': ['MOVIE', 'MUSIC', 'GAME', 'DIRECT', 'PURE DIRECT', 'STEREO', 'AUTO', 'DOLBY DIGITAL', 'DOLBY SURROUND', 'DTS SURROUND', 'NEURAL:X', 'AURO3D', 'AURO2DSURR', 'MCH STEREO', 'ROCK ARENA', 'JAZZ CLUB', 'MONO MOVIE', 'MATRIX', 'VIDEO GAME', 'VIRTUAL', 'LEFT', 'RIGHT']}, 'read_cmd': 'MS?', 'write_cmd': 'MS{RAW_VALUE_UPPER}', 'item_type': 'str', 'dev_datatype': 'str', 'reply_pattern': r'\x00?MS(.*)', 'item_attrs': {'initial': True}}, 'sleep': {'read': True, 'write': True, 'item_type': 'num', 'read_cmd': 'SLP?', 'write_cmd': 'SLP{VALUE}', 'dev_datatype': 'convert0', 'reply_pattern': r'SLP(\d{3}|OFF)', 'cmd_settings': {'force_min': 0, 'force_max': 120}, 'item_attrs': {'initial': True}}, 'standby': {'read': True, 'write': True, 'item_type': 'num', 'read_cmd': 'STBY?', 'write_cmd': 'STBY{VALUE}', 'dev_datatype': 'DenonStandby1', 'reply_pattern': r'STBY(\d{2}M|OFF)', 'cmd_settings': {'valid_list_ci': [0, 15, 30, 60]}, 'item_attrs': {'initial': True}}, }, @@ -421,10 +421,10 @@ } }, 'input': { - 'on_change': [".custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value]",], + 'on_change': [".custom_name = sh.....general.custom_inputnames().get(value, '')"], 'custom_name': { 'type': 'str', - 'on_change': "sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None" + 'on_change': "sh...(sh......general.custom_inputnames.reverse().get(value, ''))" } } } diff --git a/denon/plugin.yaml b/denon/plugin.yaml index 7994315b6..0e79b628d 100755 --- a/denon/plugin.yaml +++ b/denon/plugin.yaml @@ -364,176 +364,132 @@ item_structs: denon_command@instance: info.serialnumber denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info main: type: str denon_command@instance: info.main denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info mainfbl: type: num denon_command@instance: info.mainfbl denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info dsp1: type: num denon_command@instance: info.dsp1 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info dsp2: type: num denon_command@instance: info.dsp2 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info dsp3: type: num denon_command@instance: info.dsp3 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info dsp4: type: num denon_command@instance: info.dsp4 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info apld: type: num denon_command@instance: info.apld denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info vpld: type: num denon_command@instance: info.vpld denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info guidat: type: num denon_command@instance: info.guidat denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info heosversion: type: str denon_command@instance: info.heosversion denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info heosbuild: type: num denon_command@instance: info.heosbuild denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info heosmod: type: num denon_command@instance: info.heosmod denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info heoscnf: type: str denon_command@instance: info.heoscnf denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info heoslanguage: type: str denon_command@instance: info.heoslanguage denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info mac: type: str denon_command@instance: info.mac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info wifimac: type: str denon_command@instance: info.wifimac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info btmac: type: str denon_command@instance: info.btmac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info audyif: type: num denon_command@instance: info.audyif denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info productid: type: num denon_command@instance: info.productid denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info packageid: type: num denon_command@instance: info.packageid denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info cmp: type: str denon_command@instance: info.cmp denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - info region: type: str @@ -558,6 +514,8 @@ item_structs: denon_write@instance: false denon_read_group@instance: - general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -569,6 +527,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -895,11 +858,11 @@ item_structs: - zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -1506,11 +1469,11 @@ item_structs: - zone2 - zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -1735,11 +1698,11 @@ item_structs: - zone3 - zone3.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) settings: @@ -1877,6 +1840,8 @@ item_structs: denon_read_group@instance: - ALL - ALL.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -1888,6 +1853,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -2117,11 +2087,11 @@ item_structs: - ALL.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -2569,11 +2539,11 @@ item_structs: - ALL.zone2 - ALL.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -2667,198 +2637,132 @@ item_structs: denon_command@instance: info.serialnumber denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info main: type: str denon_command@instance: info.main denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info mainfbl: type: num denon_command@instance: info.mainfbl denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info dsp1: type: num denon_command@instance: info.dsp1 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info dsp2: type: num denon_command@instance: info.dsp2 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info dsp3: type: num denon_command@instance: info.dsp3 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info dsp4: type: num denon_command@instance: info.dsp4 denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info apld: type: num denon_command@instance: info.apld denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info vpld: type: num denon_command@instance: info.vpld denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info guidat: type: num denon_command@instance: info.guidat denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info heosversion: type: str denon_command@instance: info.heosversion denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info heosbuild: type: num denon_command@instance: info.heosbuild denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info heosmod: type: num denon_command@instance: info.heosmod denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info heoscnf: type: str denon_command@instance: info.heoscnf denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info heoslanguage: type: str denon_command@instance: info.heoslanguage denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info mac: type: str denon_command@instance: info.mac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info wifimac: type: str denon_command@instance: info.wifimac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info btmac: type: str denon_command@instance: info.btmac denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info audyif: type: num denon_command@instance: info.audyif denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info productid: type: num denon_command@instance: info.productid denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info packageid: type: num denon_command@instance: info.packageid denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info cmp: type: str denon_command@instance: info.cmp denon_read@instance: true denon_write@instance: false - denon_read_group@instance: - - AVR-X6300H - - AVR-X6300H.info region: type: str @@ -2885,6 +2789,8 @@ item_structs: denon_read_group@instance: - AVR-X6300H - AVR-X6300H.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -2896,6 +2802,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -3204,11 +3115,11 @@ item_structs: - AVR-X6300H.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -3776,11 +3687,11 @@ item_structs: - AVR-X6300H.zone2 - AVR-X6300H.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -4019,11 +3930,11 @@ item_structs: - AVR-X6300H.zone3 - AVR-X6300H.zone3.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) settings: @@ -4166,6 +4077,8 @@ item_structs: denon_read_group@instance: - AVR-X4300H - AVR-X4300H.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -4177,6 +4090,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -4406,11 +4324,11 @@ item_structs: - AVR-X4300H.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -5002,11 +4920,11 @@ item_structs: - AVR-X4300H.zone2 - AVR-X4300H.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -5245,11 +5163,11 @@ item_structs: - AVR-X4300H.zone3 - AVR-X4300H.zone3.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) settings: @@ -5392,6 +5310,8 @@ item_structs: denon_read_group@instance: - AVR-X3300W - AVR-X3300W.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -5403,6 +5323,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -5663,11 +5588,11 @@ item_structs: - AVR-X3300W.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -6236,11 +6161,11 @@ item_structs: - AVR-X3300W.zone2 - AVR-X3300W.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -6415,6 +6340,8 @@ item_structs: denon_read_group@instance: - AVR-X2300W - AVR-X2300W.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -6426,6 +6353,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -6686,11 +6618,11 @@ item_structs: - AVR-X2300W.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -7258,11 +7190,11 @@ item_structs: - AVR-X2300W.zone2 - AVR-X2300W.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num @@ -7370,6 +7302,8 @@ item_structs: denon_read_group@instance: - AVR-X1300W - AVR-X1300W.general + - custom_inputnames + denon_read_initial@instance: true cache: true reverse: @@ -7381,6 +7315,11 @@ item_structs: eval: sh...timer(2, {}) eval_trigger: '...' + update: + type: bool + enforce_updates: 'true' + denon_read_group_trigger: custom_inputnames + power: type: bool denon_command@instance: general.power @@ -7641,11 +7580,11 @@ item_structs: - AVR-X1300W.zone1.control denon_read_initial@instance: true on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) listeningmode: type: str @@ -8129,11 +8068,11 @@ item_structs: - AVR-X1300W.zone2 - AVR-X1300W.zone2.control on_change: - - .custom_name = '' if sh.....general.custom_inputnames() == {} else sh.....general.custom_inputnames()[value] + - .custom_name = sh.....general.custom_inputnames().get(value, '') custom_name: type: str - on_change: sh...(sh......general.custom_inputnames.reverse()[value]) if sh......general.custom_inputnames.reverse() != {} else None + on_change: sh...(sh......general.custom_inputnames.reverse().get(value, '')) sleep: type: num From bb117986215b3acad1e9e3a7dcef69474091d72e Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:38:42 +0200 Subject: [PATCH 051/121] database: Added logging --- database/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/database/__init__.py b/database/__init__.py index 1a02a6475..65eb46b76 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -1137,6 +1137,7 @@ def _series(self, func, start, end='now', count=100, ratio=1, update=False, step 'step': logs['step'], 'sid': sid}, 'update': self.shtime.now() + datetime.timedelta(seconds=int(logs['step'] / 1000)) } + self.logger.dbgmed(f"_series: {sid=}, {step=}, update={result['update']}, delta={int(logs['step'] / 1000)}, now={self.shtime.now()}") #self.logger.debug("_series: result={}".format(result)) return result From 9de1fe7718d8598bd6e22cbf4edea495e452b42d Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:40:05 +0200 Subject: [PATCH 052/121] onewire: Changed some Log levels from info to dbghigh --- onewire/__init__.py | 14 +++++++------- onewire/plugin.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/onewire/__init__.py b/onewire/__init__.py index 8d1a493fb..9065fc0d7 100755 --- a/onewire/__init__.py +++ b/onewire/__init__.py @@ -43,7 +43,7 @@ class OneWire(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.9.4' + PLUGIN_VERSION = '1.9.5' _flip = {0: '1', False: '1', 1: '0', True: '0', '0': True, '1': False} @@ -448,7 +448,7 @@ def _sensor_cycle(self): def _discovery_process_bus(self, path): bus = path.split("/")[-2] - self.logger.info(f"- Processing of data for bus {bus} started") + self.logger.dbghigh(f"Discovery: Processing of data for bus {bus} started") if bus not in self._buses: self._buses[bus] = [] self._webif_buses[bus] = {} @@ -461,7 +461,7 @@ def _discovery_process_bus(self, path): self.logger.info(f"_discovery_process_bus: Problem reading {bus}, error: {e}") return - self.logger.info(f"- On bus {bus} found sensors: {sensors}") + self.logger.info(f"Discovery: On bus {bus} {len(sensors)} sensors found: {sensors}") for sensor in sensors: # skip subdirectories alarm, interface and simultaneous @@ -547,7 +547,7 @@ def _discovery_process_bus(self, path): else: self.logger.debug(f"_discovery_process_bus: Sensor {sensor} was already found in bus {bus}") - self.logger.info(f"- Processing of data for bus {bus} finished") + self.logger.dbghigh(f"Discovery: Processing of data for bus {bus} finished") return def _discovery(self): @@ -558,14 +558,14 @@ def _discovery(self): If the next call takes places it will be checked if there is something changed in top level directory. The rest of the discovery will be skipped if now changes are found. """ - self.logger.info("discovery started") + self.logger.dbghigh("Discovery started") self._intruders = [] # reset intrusion detection try: listing = self.owbase.dir('/') except Exception as e: self.logger.error(f"_discovery: listing '/' failed with error '{e}'") return - self.logger.info(f"_discovery: got listing for '/' = '{listing}' self.alive: {self.alive}") + self.logger.dbghigh(f"Discovery: Got listing for '/' = '{listing}' self.alive: {self.alive}") if type(listing) != list: self.logger.warning(f"_discovery: listing '{listing}' is not a list.") return @@ -588,7 +588,7 @@ def _discovery(self): else: # for did not end prematurely with break or something else self._discovered = True - self.logger.info("discovery finished") + self.logger.dbghigh("Discovery finished") # get a list of all directory entries from owserver # self.devices = self.tree() diff --git a/onewire/plugin.yaml b/onewire/plugin.yaml index b880b7521..a7a839b7d 100755 --- a/onewire/plugin.yaml +++ b/onewire/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: 1wire onewire dallas ibutton sensor temperature humidity documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1493319-support-thread-zum-onewire-plugin - version: 1.9.4 # Plugin version + version: 1.9.5 # Plugin version sh_minversion: '1.9.3.5' # minimum shNG version to use this plugin multi_instance: True restartable: True From 1ea5e2516c33e696f4a1a47269d64f7643171078 Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:40:48 +0200 Subject: [PATCH 053/121] shelly: Bumped version to 1.8.3 --- shelly/__init__.py | 2 +- shelly/plugin.yaml | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/shelly/__init__.py b/shelly/__init__.py index 35843ccfe..066731a86 100755 --- a/shelly/__init__.py +++ b/shelly/__init__.py @@ -39,7 +39,7 @@ class Shelly(MqttPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.8.2' + PLUGIN_VERSION = '1.8.3' def __init__(self, sh): diff --git a/shelly/plugin.yaml b/shelly/plugin.yaml index eccdf8aca..5844d63f7 100755 --- a/shelly/plugin.yaml +++ b/shelly/plugin.yaml @@ -12,7 +12,7 @@ plugin: # documentation: http://smarthomeng.de/user/plugins/mqtt2/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1451853-support-thread-für-das-shelly-plugin - version: 1.8.2 # Plugin version + version: 1.8.3 # Plugin version sh_minversion: '1.9.5.6' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance @@ -123,17 +123,20 @@ item_structs: online: type: bool + description: State of Shelly device (online/offline) shelly_id: ..:. shelly_type: ..:. shelly_attr: online power: type: num + description: Power consumption shelly_id: ..:. shelly_type: ..:. shelly_group: "switch:0" shelly_attr: power energy: type: num + description: Energy measured in Milliwatt shelly_id: ..:. shelly_type: ..:. shelly_group: "switch:0" @@ -146,11 +149,13 @@ item_structs: #shelly_attr: relay temp: type: num + description: Temperature of Shelly device measured in °C shelly_id: ..:. shelly_type: ..:. shelly_attr: temp temp_f: type: num + description: Temperature of Shelly device measured in °F shelly_id: ..:. shelly_type: ..:. shelly_attr: temp_f @@ -170,22 +175,36 @@ item_structs: #shelly_attr: relay voltage: type: num + description: Voltage of switched outlet of Shelly device measured in Volt shelly_id: ..:. shelly_type: ..:. shelly_group: "switch:0" shelly_attr: voltage current: type: num + description: Current on switched outlet of Shelly device measured in Ampere shelly_id: ..:. shelly_type: ..:. shelly_group: "switch:0" shelly_attr: current energy_by_minute: type: num + cache: True + description: Energy by minute measured in Milliwatt/minute shelly_id: ..:. shelly_type: ..:. shelly_group: "switch:0" shelly_attr: energy_by_minute + energy_wh: + type: num + description: Calculated energy measured in Wh + eval: round(sh...energy_by_minute() *60 /1000, 4) + eval_trigger: ..energy_by_minute + energy_kwh: + type: num + description: Calculated energy measured in kWh + eval: round(sh...energy_wh() /1000, 4) + eval_trigger: ..energy_wh shellyht: name: WLAN Temperatur & Feuchtesensor (Zwischenstecker) @@ -238,10 +257,12 @@ item_structs: shelly_attr: illumination temp: type: num + description: Temperature of Shelly device measured in °C shelly_id: ..:. shelly_attr: temp temp_f: type: num + description: Temperature of Shelly device measured in °F shelly_id: ..:. shelly_attr: temp_f battery: @@ -251,6 +272,7 @@ item_structs: shelly_attr: battery voltage: type: num + description: Voltage on switched outlet of Shelly device measured in Volt shelly_id: ..:. shelly_group: sensor shelly_attr: voltage @@ -282,6 +304,7 @@ item_structs: shelly_attr: online temp: type: num + description: Temperature of Shelly device measured in °C shelly_id: ..:. shelly_type: ..:. shelly_attr: temp From 1a9f3676645eb6dda93c221961e74d813eaff804 Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:42:32 +0200 Subject: [PATCH 054/121] smartvisu: Fixed version-check bug in widget installation; Bumped version to 1.8.16 --- smartvisu/__init__.py | 2 +- smartvisu/plugin.yaml | 2 +- smartvisu/svinstallwidgets.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/smartvisu/__init__.py b/smartvisu/__init__.py index 732eca49d..bb64e465b 100755 --- a/smartvisu/__init__.py +++ b/smartvisu/__init__.py @@ -46,7 +46,7 @@ class SmartVisu(SmartPlugin): - PLUGIN_VERSION="1.8.15" + PLUGIN_VERSION="1.8.16" ALLOW_MULTIINSTANCE = True visu_definition = None diff --git a/smartvisu/plugin.yaml b/smartvisu/plugin.yaml index bd6878872..cf6280780 100755 --- a/smartvisu/plugin.yaml +++ b/smartvisu/plugin.yaml @@ -12,7 +12,7 @@ plugin: #documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1586800-support-thread-für-das-smartvisu-plugin - version: 1.8.15 # Plugin version + version: 1.8.16 # Plugin version sh_minversion: '1.9.3.5' # 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 diff --git a/smartvisu/svinstallwidgets.py b/smartvisu/svinstallwidgets.py index b9b16361d..9378ab1c5 100755 --- a/smartvisu/svinstallwidgets.py +++ b/smartvisu/svinstallwidgets.py @@ -46,6 +46,7 @@ def __init__(self, plugin_instance): self.pgbdir = os.path.join(self.smartvisu_dir, 'dropins') self.icndir_ws = os.path.join(self.smartvisu_dir, 'dropins/icons/ws') self.icndir_sw = os.path.join(self.smartvisu_dir, 'dropins/icons/sw') + #if self.smartvisu_version >= '3.0' and self.smartvisu_version <= '3.4': if self.smartvisu_version >= '3.0': # v3.x self.outdir = os.path.join(self.smartvisu_dir, 'dropins/shwidgets') From 3eca87f732c0aed4ad838f289c1398cd5ae272c3 Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:43:44 +0200 Subject: [PATCH 055/121] beolink: Bug fixes; Bumped version to 0.8.1 --- beolink/__init__.py | 76 +++++++++++++++++++++++-------------------- beolink/beodevices.py | 24 +++++++------- beolink/plugin.yaml | 2 +- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/beolink/__init__.py b/beolink/__init__.py index 3d5f3796d..77f9c6893 100755 --- a/beolink/__init__.py +++ b/beolink/__init__.py @@ -42,7 +42,7 @@ class BeoNetlink(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '0.8.0' + PLUGIN_VERSION = '0.8.1' def __init__(self, sh): """ @@ -267,7 +267,7 @@ def poll_device(self): item = self.beo_items[beo_itemkey] beo_id = item.conf['beo_id'] beo_status = item.conf['beo_status'] - if beo_status: + if beo_status and beo_id != '': # set items according to beo_status #if beo_status == 'beoname': # deviceinfo = self.beodevices.beodeviceinfo[beo_id].get('FriendlyName', None) @@ -275,43 +275,47 @@ def poll_device(self): # deviceinfo = self.beodevices.beodeviceinfo[beo_id].get('productType', None) #else: # deviceinfo = self.beodevices.beodeviceinfo[beo_id].get(beo_status, None) - if beo_status == 'audiomode': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('audiomode'[1], False) - elif beo_status == 'videomode': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('videomode'[1], False) - elif beo_status == 'powerstate': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('powerstate', False) - elif beo_status == 'stand': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('stand'[1], False) - elif beo_status == 'source': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['source'].get('source', '-') - elif beo_status == 'volume': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['volume'].get('level', 0) - elif beo_status == 'muted': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['volume'].get('muted', False) - elif beo_status == 'FriendlyName': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('FriendlyName', False) - elif beo_status == 'productType': - deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('productType', False) - + beo_device = self.beodevices.beodeviceinfo.get(beo_id, None) + if beo_device is None: + self.logger.warning(f"poll_device: No deviceinfo found for device-id '{beo_id}'") else: - deviceinfo = self.beodevices.beodeviceinfo[beo_id].get(beo_status, None) - #self.logger.info(f"poll_device: item={item.property.path}, beo_id={beo_id}, beo_status={beo_status}, self.beodevices.beodeviceinfo[beo_id]={self.beodevices.beodeviceinfo[beo_id]}") - #self.logger.info(f"poll_device: item={item.property.path}, deviceinfo={deviceinfo}") - if isinstance(deviceinfo, tuple): - if item._type == 'num': - beo_value = deviceinfo[1] + if beo_status == 'audiomode': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('audiomode'[1], False) + elif beo_status == 'videomode': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('videomode'[1], False) + elif beo_status == 'powerstate': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('powerstate', False) + elif beo_status == 'stand': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('stand'[1], False) + elif beo_status == 'source': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['source'].get('source', '-') + elif beo_status == 'volume': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['volume'].get('level', 0) + elif beo_status == 'muted': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['volume'].get('muted', False) + elif beo_status == 'FriendlyName': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('FriendlyName', False) + elif beo_status == 'productType': + deviceinfo = self.beodevices.beodeviceinfo[beo_id]['device'].get('productType', False) + else: - beo_value = deviceinfo[0] - else: - beo_value = deviceinfo + deviceinfo = self.beodevices.beodeviceinfo[beo_id].get(beo_status, None) + #self.logger.info(f"poll_device: item={item.property.path}, beo_id={beo_id}, beo_status={beo_status}, self.beodevices.beodeviceinfo[beo_id]={self.beodevices.beodeviceinfo[beo_id]}") + #self.logger.info(f"poll_device: item={item.property.path}, deviceinfo={deviceinfo}") + if isinstance(deviceinfo, tuple): + if item._type == 'num': + beo_value = deviceinfo[1] + else: + beo_value = deviceinfo[0] + else: + beo_value = deviceinfo - if item() == beo_value: - self.logger.debug("update_deviceinfo: Updated item {} with beo-{} {}".format(item.property.path, beo_status, beo_value)) - else: - self.logger.info("update_deviceinfo: Changed item {} with beo-{} {}".format(item.property.path, beo_status, beo_value)) - item(beo_value, self.get_shortname()) - self._update_item_values(item, beo_value) + if item() == beo_value: + self.logger.debug("update_deviceinfo: Updated item {} with beo-{} {}".format(item.property.path, beo_status, beo_value)) + else: + self.logger.info("update_deviceinfo: Changed item {} with beo-{} {}".format(item.property.path, beo_status, beo_value)) + item(beo_value, self.get_shortname()) + self._update_item_values(item, beo_value) else: self.logger.info(f"poll_device: No beo_status") return diff --git a/beolink/beodevices.py b/beolink/beodevices.py index b9bacfcbc..92e538c8b 100644 --- a/beolink/beodevices.py +++ b/beolink/beodevices.py @@ -234,20 +234,22 @@ def update_deviceinfo(self, beo_id): self.beodeviceinfo[beo_id]['device']['audiomode'] = ['-', -1] self.beodeviceinfo[beo_id]['device']['stand'] = ['-', -1] else: - self.beodeviceinfo[beo_id]['source']['source'] = self.get_beo_api(ip, '/BeoZone/Zone/ActiveSources', ['primaryExperience','source','friendlyName']) - - # get picture-mode of the B&O device - self.beodeviceinfo[beo_id]['device']['videomode'] = list(self.read_list_value(ip, '/Picture/Mode', 'mode')) - self.logger.debug("update_deviceinfo: ip: {} videomode-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['videomode'])) + try: + self.beodeviceinfo[beo_id]['source']['source'] = self.get_beo_api(ip, '/BeoZone/Zone/ActiveSources', ['primaryExperience','source','friendlyName']) - # get sound-mode of the B&O device - self.beodeviceinfo[beo_id]['device']['audiomode'] = list(self.read_list_value(ip, '/Sound/Mode', 'mode')) - self.logger.debug("update_deviceinfo: ip: {} audiomode-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['audiomode'])) + # get picture-mode of the B&O device + self.beodeviceinfo[beo_id]['device']['videomode'] = list(self.read_list_value(ip, '/Picture/Mode', 'mode')) + self.logger.debug("update_deviceinfo: ip: {} videomode-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['videomode'])) - # get stand position of the B&O device - self.beodeviceinfo[beo_id]['device']['stand'] = list(self.read_list_value(ip, '/Stand', 'stand')) - self.logger.debug("update_deviceinfo: ip: {} stand-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['stand'])) + # get sound-mode of the B&O device + self.beodeviceinfo[beo_id]['device']['audiomode'] = list(self.read_list_value(ip, '/Sound/Mode', 'mode')) + self.logger.debug("update_deviceinfo: ip: {} audiomode-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['audiomode'])) + # get stand position of the B&O device + self.beodeviceinfo[beo_id]['device']['stand'] = list(self.read_list_value(ip, '/Stand', 'stand')) + self.logger.debug("update_deviceinfo: ip: {} stand-friendly = {}".format(ip, self.beodeviceinfo[beo_id]['device']['stand'])) + except Exception as ex: + self.logger.warning(f"beodevices/update_deviceinfo: {beo_id} - Exception '{ex}'") # get possible sources of the B&O device #raw_sources = self.get_beo_api(ip, '/BeoZone/Zone/Sources', []) #self.beodeviceinfo[beo_id]['sources'] = [] diff --git a/beolink/plugin.yaml b/beolink/plugin.yaml index 9ddb079b3..764f0e229 100755 --- a/beolink/plugin.yaml +++ b/beolink/plugin.yaml @@ -12,7 +12,7 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page # support: https://knx-user-forum.de/forum/supportforen/smarthome-py - version: 0.8.0 # Plugin version + version: 0.8.1 # Plugin version sh_minversion: '1.9' # 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 From ce3825e9335caf92a65c784980e7d65347e9e449 Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 12:50:15 +0200 Subject: [PATCH 056/121] panasonic_ac: New plugin to control Panasonic air conditioning units, still in develop state --- panasonic_ac/__init__.py | 439 ++++++++++++++++ .../packages/pcomfortcloud/__init__.py | 32 ++ .../packages/pcomfortcloud/__main__.py | 299 +++++++++++ .../packages/pcomfortcloud/apiclient.py | 276 ++++++++++ .../packages/pcomfortcloud/authentication.py | 419 +++++++++++++++ .../packages/pcomfortcloud/constants.py | 69 +++ .../packages/pcomfortcloud/exceptions.py | 14 + .../packages/pcomfortcloud/session.py | 46 ++ panasonic_ac/plugin.yaml | 200 +++++++ panasonic_ac/requirements.txt | 2 + panasonic_ac/user_doc.rst | 132 +++++ panasonic_ac/webif/__init__.py | 200 +++++++ panasonic_ac/webif/static/img/plugin_logo.png | Bin 0 -> 27830 bytes panasonic_ac/webif/static/img/readme.txt | 6 + panasonic_ac/webif/templates/index.html | 488 ++++++++++++++++++ 15 files changed, 2622 insertions(+) create mode 100644 panasonic_ac/__init__.py create mode 100644 panasonic_ac/packages/pcomfortcloud/__init__.py create mode 100644 panasonic_ac/packages/pcomfortcloud/__main__.py create mode 100644 panasonic_ac/packages/pcomfortcloud/apiclient.py create mode 100644 panasonic_ac/packages/pcomfortcloud/authentication.py create mode 100644 panasonic_ac/packages/pcomfortcloud/constants.py create mode 100644 panasonic_ac/packages/pcomfortcloud/exceptions.py create mode 100644 panasonic_ac/packages/pcomfortcloud/session.py create mode 100755 panasonic_ac/plugin.yaml create mode 100755 panasonic_ac/requirements.txt create mode 100755 panasonic_ac/user_doc.rst create mode 100755 panasonic_ac/webif/__init__.py create mode 100644 panasonic_ac/webif/static/img/plugin_logo.png create mode 100644 panasonic_ac/webif/static/img/readme.txt create mode 100755 panasonic_ac/webif/templates/index.html diff --git a/panasonic_ac/__init__.py b/panasonic_ac/__init__.py new file mode 100644 index 000000000..360ac20e8 --- /dev/null +++ b/panasonic_ac/__init__.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2024- Martin Sinn m.sinn@gmx.de +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Plugin to control Panasonic air conditioning systems via the +# Panasonic comfort cloud +# +# 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 os +import json + +from lib.model.smartplugin import SmartPlugin +from lib.item import Items + + +mapping_delimiter = '|' + +# optionally import package 'pcomfcloud' from local plugin folder +LOCAL_PACKAGE_IMPORTED = False +EXCEPTION_TEXT = '' +if os.path.isfile(os.path.join('plugins', 'panasonic_ac', 'packages', 'pcomfortcloud', '__init__.py')): + try: + import plugins.panasonic_ac.packages.pcomfortcloud as pcomfortcloud + LOCAL_PACKAGE_IMPORTED = True + except Exception as ex: + EXCEPTION_TEXT = str(ex) +else: + import pcomfortcloud + +from .webif import WebInterface + + +# If a needed package is imported, which might be not installed in the Python environment, +# add it to a requirements.txt file within the plugin's directory + + +class PanComfortCloud(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.3.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release + + 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__() + + # 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 = 30 + + # if you want to use an item to toggle plugin execution, enable the + # definition in plugin.yaml and uncomment the following line + #self._pause_item_path = self.get_parameter_value('pause_item') + + # create the var directory for this plugin + var_plugin_dir = os.path.join(self.get_sh()._var_dir, self.get_shortname()) + try: + os.mkdir(var_plugin_dir, 0o775 ) + except FileExistsError: + pass + + # Initialization code goes here + self._username = self.get_parameter_value('username') + self._password = self.get_parameter_value('password') + + self._raw = False + self._token = os.path.join(var_plugin_dir, 'token.json') + self._skipVerify = False + self.session = None + + if LOCAL_PACKAGE_IMPORTED: + self.logger.notice("Using local installation of 'pcomfortcloud' package") + if EXCEPTION_TEXT != "": + self.logger.error(f"Exception while importing LOCAL package: {EXCEPTION_TEXT}") + self._init_complete = False + return + + if not self.pcc_login(): + # Do not load Plugin, if login fails due to wrong 'pcomfortcloud' package + self._init_complete = False + return + + self._devices = {} # devices reported by comfort cloud + + self.pcc_readdevicelist() + for idx, device in enumerate(self._devices): + index = idx + 1 + self.poll_device() + #self.logger.notice(f"- Device {index} status: {self._devices[str(index)]['parameters']}") + + + self.init_webinterface(WebInterface) + return + + + def _check_plugin_var_dir(self): + + var_dir = os.path.join(self.get_sh()._var_dir, self.get_shortname()) + try: + os.mkdir(var_dir, 0o775 ) + except FileExistsError: + pass + + + def pcc_getdevicestatus(self, index): + # index: 1, ... + if int(index) <= 0 or int(index) > len(self._devices): + self.logger.error(f"Device with index={index} not found in device list") + + id = self._devices[str(index)]['id'] + try: + device_status = self.session.get_device(id)['parameters'] + except Exception as ex: + device_status = {} + self.logger.dbghigh(f"- Status of device (index={index}) cannot be read - Exception {ex}") + return device_status + + + def pcc_readdevicelist(self): + + for idx, device in enumerate(self.session.get_devices()): + self._devices[str(idx+1)] = device + + + def pcc_login(self): + + if os.path.isfile(self._token): + with open(self._token, "r") as token_file: + json_token = json.load(token_file) + else: + json_token = None + + try: + auth = pcomfortcloud.Authentication( + self._username, self._password, + json_token, + self._raw + ) + except: + self.logger.error("The installed verson of 'pcomfortcloud' package is not compatible with this plugin") + self._init_complete = False + return + + try: + auth.login() + except Exception as ex: + self.logger.notice(f"Exception on auth.login: {ex}") + self.logger.notice(f"Retrying login without old token") + json_token = None + auth = pcomfortcloud.Authentication( + self._username, self._password, + json_token, + self._raw + ) + auth.login() + + json_token = auth.get_token() + self.session = pcomfortcloud.ApiClient( + auth, + self._raw + ) + with open(self._token, "w") as token_file: + json.dump(json_token, token_file, indent=4) + return True + + + def run(self): + """ + Run method for the plugin + """ + self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'run()'})) + + # connect to network / web / serial device + # (enable the following lines if you want to open a connection + # don't forget to implement a connect (and disconnect) method.. :) ) + #self.connect() + + # setup scheduler for device poll loop + # (enable the following line, if you need to poll the device. + # Rember to un-comment the self._cycle statement in __init__ as well) + self.scheduler_add(self.get_fullname() + '_poll', self.poll_device, cycle=self._cycle) + + # Start the asyncio eventloop in it's own thread + # and set self.alive to True when the eventloop is running + # (enable the following line, if you need to use asyncio in the plugin) + #self.start_asyncio(self.plugin_coro()) + + self.alive = True # if using asyncio, do not set self.alive here. Set it in the session coroutine + + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(False, self.get_fullname()) + + # if you need to create child threads, do not make them daemon = True! + # They will not shutdown properly. (It's a python bug) + # Also, don't create the thread in __init__() and start them here, but + # create and start them here. Threads can not be restarted after they + # have been stopped... + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.dbghigh(self.translate("Methode '{method}' aufgerufen", {'method': 'stop()'})) + self.alive = False # if using asyncio, do not set self.alive here. Set it in the session coroutine + + # let the plugin change the state of pause_item + if self._pause_item: + self._pause_item(True, self.get_fullname()) + + # this stops all schedulers the plugin has started. + # you can disable/delete the line if you don't use schedulers + self.scheduler_remove_all() + + # stop the asyncio eventloop and it's thread + # If you use asyncio, enable the following line + #self.stop_asyncio() + + # If you called connect() on run(), disconnect here + # (remember to write a disconnect() method!) + #self.disconnect() + + # also, clean up anything you set up in run(), so the plugin can be + # cleanly stopped and started again + + 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. + """ + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item + self.add_item(item, updating=True) + return self.update_item + + if self.has_iattr(item.conf, 'pcc_index') and self.has_iattr(item.conf, 'pcc_parameter'): + index = str(self.get_iattr_value(item.conf, 'pcc_index')) + parameter = self.get_iattr_value(item.conf, 'pcc_parameter') + config_data = {} + config_data['index'] = index + config_data['parameter'] = parameter + config_data['item'] = item + mapping = config_data['index'] + mapping_delimiter + config_data['parameter'] + self.add_item(item, mapping=mapping, config_data_dict=config_data, updating=True) + return self.update_item + + 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): + """ + Item has been updated + + This method is called, if the value of an item has been updated by SmartHomeNG. + It should write the changed value out to the device (hardware/interface) that + is managed by this plugin. + + To prevent a loop, the changed value should only be written to the device, if the plugin is running and + the value was changed outside of this plugin(-instance). That is checked by comparing the caller parameter + with the fullname (plugin name & instance) of the plugin. + + :param item: item to be updated towards the plugin + :param caller: if given it represents the callers name + :param source: if given it represents the source + :param dest: if given it represents the dest + """ + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + + if self.alive and caller != self.get_fullname(): + # code to execute if the plugin is not stopped + # and only, if the item has not been changed by this plugin: + self.logger.dbghigh(f"update_item: '{item.property.path}' has been changed outside this plugin by caller '{self.callerinfo(caller, source)}'") + + config_data = self.get_item_config(item) + #self.logger.notice(f"update_item: Sending '{item()}' of '{config_data['item']}' to comfort cloud -> {config_data=}") + + self.update_pcc_from_item(config_data, item) + + return + + + def update_pcc_from_item(self, config_data, item): + value = item() + #self.logger.notice(f"update_pcc_from_item: config_data = {config_data}") + + try: + index = config_data['index'] + kwargs = {} + if config_data['parameter'] == 'temperature': + kwargs['temperature'] = float(value) + self.logger.info(f" -> Device {index}: Setting '{config_data['parameter']}' to {value}°C") + else: + if config_data['parameter'] == 'power': + kwargs['power'] = pcomfortcloud.constants.Power(int(value)) + elif config_data['parameter'] == 'mode': + kwargs['mode'] = pcomfortcloud.constants.OperationMode(int(value)) + elif config_data['parameter'] == 'fanSpeed': + kwargs['fanSpeed'] = pcomfortcloud.constants.FanSpeed(int(value)) + elif config_data['parameter'] == 'airSwingHorizontal': + kwargs['airSwingHorizontal'] = pcomfortcloud.constants.AirSwingLR(int(value)) + elif config_data['parameter'] == 'airSwingVertical': + kwargs['airSwingVertical'] = pcomfortcloud.constants.AirSwingUD(int(value)) + elif config_data['parameter'] == 'eco': + kwargs['eco'] = pcomfortcloud.constants.EcoMode(int(value)) + elif config_data['parameter'] == 'nanoe': + kwargs['nanoe'] = pcomfortcloud.constants.NanoeMode(int(value)) + else: + self.logger.notice(f"update_pcc_from_item: Updating the parameter {config_data['parameter']} is not supported/implemented") + if kwargs != {}: + self.logger.info(f" -> Device {index=}: Setting '{config_data['parameter']}' to {value} ({kwargs[config_data['parameter']].name})") + if kwargs != {}: + self.session.set_device(self._devices[config_data['index']]['id'], **kwargs) + except Exception as ex: + self.logger.warning(f"Cannot set parameter '{config_data['parameter']}' of device index={config_data['index']} - Exception: {ex}") + + return + + + def update_items_with_mapping(self, mapping_root, mapping_key, value=None, value_enum=None): + + update_items = self.get_items_for_mapping(mapping_root + mapping_key) + if value_enum is None: + for item in update_items: + item(value, self.get_fullname()) + else: + val_num = int(value_enum.value) + val_str = value_enum.name + for item in update_items: + if item.property.type == 'num': + item(val_num, self.get_fullname()) + else: + item(val_str, self.get_fullname()) + + + def poll_device(self): + """ + Polls for updates of the device + + This method is only needed, if the device (hardware/interface) does not propagate + changes on it's own, but has to be polled to get the actual status. + It is called by the scheduler which is set within run() method. + """ + for idx, device in enumerate(self._devices): + index = idx + 1 + self._devices[str(index)]['parameters'] = self.pcc_getdevicestatus(index) + + # Items updaten + mapping_root = str(index) + mapping_delimiter + self.update_items_with_mapping(mapping_root, 'name', self._devices[str(index)]['name']) + if self._devices[str(index)]['parameters'] != {}: + self.update_items_with_mapping(mapping_root, 'connected', True) + for key in self._devices[str(index)]['parameters'].keys(): + try: + value = int(self._devices[str(index)]['parameters'][key].value) + self.update_items_with_mapping(mapping_root, key, value_enum=self._devices[str(index)]['parameters'][key]) + except: + value = self._devices[str(index)]['parameters'][key] + self.update_items_with_mapping(mapping_root, key, value) + else: + self.update_items_with_mapping(mapping_root, 'connected', False) + + async def plugin_coro(self): + """ + Coroutine for the plugin session (only needed, if using asyncio) + + This coroutine is run as the PluginTask and should + only terminate, when the plugin is stopped + """ + self.logger.notice("plugin_coro started") + + self.alive = True + + # ... + + self.alive = False + + self.logger.notice("plugin_coro finished") + return diff --git a/panasonic_ac/packages/pcomfortcloud/__init__.py b/panasonic_ac/packages/pcomfortcloud/__init__.py new file mode 100644 index 000000000..6d3dc9992 --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/__init__.py @@ -0,0 +1,32 @@ +""" +A python module for reading and changing status of panasonic climate devices through Panasonic Comfort Cloud app api +""" + +__all__ = [ + 'ApiClient', + 'Error', + 'LoginError', + 'RequestError', + 'ResponseError' +] + +from .apiclient import ( + ApiClient +) + +from .session import ( + Session +) + +from .authentication import ( + Authentication +) + +from .exceptions import ( + Error, + LoginError, + RequestError, + ResponseError +) + +from . import constants diff --git a/panasonic_ac/packages/pcomfortcloud/__main__.py b/panasonic_ac/packages/pcomfortcloud/__main__.py new file mode 100644 index 000000000..984020ddc --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/__main__.py @@ -0,0 +1,299 @@ +import argparse +import os +import json +import pcomfortcloud + +from enum import Enum + +def print_result(obj, indent=0): + for key in obj: + value = obj[key] + + if isinstance(value, dict): + print(" "*indent + key) + print_result(value, indent + 4) + elif isinstance(value, Enum): + print( + " "*indent + "{0: <{width}}: {1}".format(key, value.name, width=25-indent)) + elif isinstance(value, list): + print(" "*indent + "{0: <{width}}:".format(key, width=25-indent)) + for elt in value: + print_result(elt, indent + 4) + print("") + else: + print(" "*indent + + "{0: <{width}}: {1}".format(key, value, width=25-indent)) + + +def str2bool(boolean_string_value): + if boolean_string_value.lower() in ('yes', 'true', 't', 'y', '1'): + return True + if boolean_string_value.lower() in ('no', 'false', 'f', 'n', '0'): + return False + raise argparse.ArgumentTypeError('Boolean value expected.') + + +def main(): + """ Start pcomfortcloud Comfort Cloud command line """ + + parser = argparse.ArgumentParser( + description='Read or change status of pcomfortcloud Climate devices') + + parser.add_argument( + 'username', + help='Username for pcomfortcloud Comfort Cloud') + + parser.add_argument( + 'password', + help='Password for pcomfortcloud Comfort Cloud') + + parser.add_argument( + '-t', '--token', + help='File to store token in', + default='token.json') + + parser.add_argument( + '-r', '--raw', + help='Raw dump of response', + type=str2bool, nargs='?', const=True, + default=False) + + commandparser = parser.add_subparsers( + help='commands', + dest='command') + + commandparser.add_parser( + 'list', + help="Get a list of all devices") + + get_parser = commandparser.add_parser( + 'get', + help="Get status of a device") + + get_parser.add_argument( + dest='device', + type=int, + help='Device number #') + + set_parser = commandparser.add_parser( + 'set', + help="Set status of a device") + + set_parser.add_argument( + dest='device', + type=int, + help='Device number #' + ) + + set_parser.add_argument( + '-p', '--power', + choices=[ + pcomfortcloud.constants.Power.On.name, + pcomfortcloud.constants.Power.Off.name], + help='Power mode') + + set_parser.add_argument( + '-t', '--temperature', + type=float, + help="Temperature") + + set_parser.add_argument( + '-f', '--fanSpeed', + choices=[ + pcomfortcloud.constants.FanSpeed.Auto.name, + pcomfortcloud.constants.FanSpeed.Low.name, + pcomfortcloud.constants.FanSpeed.LowMid.name, + pcomfortcloud.constants.FanSpeed.Mid.name, + pcomfortcloud.constants.FanSpeed.HighMid.name, + pcomfortcloud.constants.FanSpeed.High.name], + help='Fan speed') + + set_parser.add_argument( + '-m', '--mode', + choices=[ + pcomfortcloud.constants.OperationMode.Auto.name, + pcomfortcloud.constants.OperationMode.Cool.name, + pcomfortcloud.constants.OperationMode.Dry.name, + pcomfortcloud.constants.OperationMode.Heat.name, + pcomfortcloud.constants.OperationMode.Fan.name], + help='Operation mode') + + set_parser.add_argument( + '-e', '--eco', + choices=[ + pcomfortcloud.constants.EcoMode.Auto.name, + pcomfortcloud.constants.EcoMode.Quiet.name, + pcomfortcloud.constants.EcoMode.Powerful.name], + help='Eco mode') + + set_parser.add_argument( + '-n', '--nanoe', + choices=[ + pcomfortcloud.constants.NanoeMode.On.name, + pcomfortcloud.constants.NanoeMode.Off.name, + pcomfortcloud.constants.NanoeMode.ModeG.name, + pcomfortcloud.constants.NanoeMode.All.name], + help='Nanoe mode') + + # set_parser.add_argument( + # '--airswingauto', + # choices=[ + # pcomfortcloud.constants.AirSwingAutoMode.Disabled.name, + # pcomfortcloud.constants.AirSwingAutoMode.AirSwingLR.name, + # pcomfortcloud.constants.AirSwingAutoMode.AirSwingUD.name, + # pcomfortcloud.constants.AirSwingAutoMode.Both.name], + # help='Automation of air swing') + + set_parser.add_argument( + '-y', '--airSwingVertical', + choices=[ + pcomfortcloud.constants.AirSwingUD.Auto.name, + pcomfortcloud.constants.AirSwingUD.Down.name, + pcomfortcloud.constants.AirSwingUD.DownMid.name, + pcomfortcloud.constants.AirSwingUD.Mid.name, + pcomfortcloud.constants.AirSwingUD.UpMid.name, + pcomfortcloud.constants.AirSwingUD.Up.name], + help='Vertical position of the air swing') + + set_parser.add_argument( + '-x', '--airSwingHorizontal', + choices=[ + pcomfortcloud.constants.AirSwingLR.Auto.name, + pcomfortcloud.constants.AirSwingLR.Left.name, + pcomfortcloud.constants.AirSwingLR.LeftMid.name, + pcomfortcloud.constants.AirSwingLR.Mid.name, + pcomfortcloud.constants.AirSwingLR.RightMid.name, + pcomfortcloud.constants.AirSwingLR.Right.name], + help='Horizontal position of the air swing') + + dump_parser = commandparser.add_parser( + 'dump', + help="Dump data of a device") + + dump_parser.add_argument( + dest='device', + type=int, + help='Device number 1-x') + + history_parser = commandparser.add_parser( + 'history', + help="Dump history of a device") + + history_parser.add_argument( + dest='device', + type=int, + help='Device number 1-x') + + history_parser.add_argument( + dest='mode', + type=str, + help='mode (Day, Week, Month, Year)') + + history_parser.add_argument( + dest='date', + type=str, + help='date of day like 20190807') + + args = parser.parse_args() + + if os.path.isfile(args.token): + with open(args.token, "r") as token_file: + json_token = json.load(token_file) + else: + json_token = None + + auth = pcomfortcloud.Authentication( + args.username, + args.password, + json_token, + args.raw + ) + auth.login() + + json_token = auth.get_token() + + session = pcomfortcloud.ApiClient( + auth, + args.raw + ) + + with open(args.token, "w") as token_file: + json.dump(json_token, token_file, indent=4) + + try: + if args.command == 'list': + print("list of devices and its device id (1-x)") + for idx, device in enumerate(session.get_devices()): + if idx > 0: + print('') + + print("device #{}".format(idx + 1)) + print_result(device, 4) + + if args.command == 'get': + if int(args.device) <= 0 or int(args.device) > len(session.get_devices()): + raise Exception("device not found, acceptable device id is from {} to {}".format(1, len(session.get_devices()))) + + device = session.get_devices()[int(args.device) - 1] + print("reading from device '{}' ({})".format( + device['name'], device['id'])) + + print_result(session.get_device(device['id'])) + + if args.command == 'set': + if int(args.device) <= 0 or int(args.device) > len(session.get_devices()): + raise Exception("device not found, acceptable device id is from {} to {}".format(1, len(session.get_devices()))) + + device = session.get_devices()[int(args.device) - 1] + print("writing to device '{}' ({})".format(device['name'], device['id'])) + + kwargs = {} + + if args.power is not None: + kwargs['power'] = pcomfortcloud.constants.Power[args.power] + + if args.temperature is not None: + kwargs['temperature'] = args.temperature + + if args.fanSpeed is not None: + kwargs['fanSpeed'] = pcomfortcloud.constants.FanSpeed[args.fanSpeed] + + if args.mode is not None: + kwargs['mode'] = pcomfortcloud.constants.OperationMode[args.mode] + + if args.eco is not None: + kwargs['eco'] = pcomfortcloud.constants.EcoMode[args.eco] + + if args.nanoe is not None: + kwargs['nanoe'] = pcomfortcloud.constants.NanoeMode[args.nanoe] + + if args.airSwingHorizontal is not None: + kwargs['airSwingHorizontal'] = pcomfortcloud.constants.AirSwingLR[args.airSwingHorizontal] + + if args.airSwingVertical is not None: + kwargs['airSwingVertical'] = pcomfortcloud.constants.AirSwingUD[args.airSwingVertical] + + session.set_device(device['id'], **kwargs) + + if args.command == 'dump': + if int(args.device) <= 0 or int(args.device) > len(session.get_devices()): + raise Exception("device not found, acceptable device id is from {} to {}".format(1, len(session.get_devices()))) + + device = session.get_devices()[int(args.device) - 1] + + print_result(session.dump(device['id'])) + + if args.command == 'history': + if int(args.device) <= 0 or int(args.device) > len(session.get_devices()): + raise Exception("device not found, acceptable device id is from {} to {}".format(1, len(session.get_devices()))) + + device = session.get_devices()[int(args.device) - 1] + + print_result(session.history(device['id'], args.mode, args.date)) + + except pcomfortcloud.ResponseError as ex: + print(ex) + + +if __name__ == "__main__": + main() diff --git a/panasonic_ac/packages/pcomfortcloud/apiclient.py b/panasonic_ac/packages/pcomfortcloud/apiclient.py new file mode 100644 index 000000000..31ff6a619 --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/apiclient.py @@ -0,0 +1,276 @@ +''' +Panasonic session, using Panasonic Comfort Cloud app api +''' + +import hashlib +import re +from urllib.parse import quote_plus + +from . import authentication +from . import constants + + +class ApiClient(): + def __init__(self, auth: authentication.Authentication, raw=False): + self._auth = auth + + self._groups = None + self._devices = None + self._device_indexer = {} + self._raw = raw + self._acc_client_id = None + + def _ensure_logged_in(self): + self._auth.login() + + def _get_groups(self): + self._ensure_logged_in() + self._groups = self._auth.execute_get( + self._get_group_url(), + "get_groups", + 200 + ) + self._devices = None + + def get_devices(self): + if self._devices is None: + if self._groups is None: + self._get_groups() + + self._devices = [] + + for group in self._groups['groupList']: + if 'deviceList' in group: + device_list = group.get('deviceList', []) + else: + device_list = group.get('deviceIdList', []) + + for device in device_list: + if device: + if 'deviceHashGuid' in device: + device_id = device['deviceHashGuid'] + else: + device_id = hashlib.md5( + device['deviceGuid'].encode('utf-8')).hexdigest() + + self._device_indexer[device_id] = device['deviceGuid'] + self._devices.append({ + 'id': device_id, + 'name': device['deviceName'], + 'group': group['groupName'], + 'model': device['deviceModuleNumber'] if 'deviceModuleNumber' in device else '' + }) + return self._devices + + def dump(self, device_id): + device_guid = self._device_indexer.get(device_id) + if device_guid: + return self._auth.execute_get(self._get_device_status_url(device_guid), "dump", 200) + return None + + def history(self, device_id, mode, date, time_zone="+01:00"): + self._ensure_logged_in() + + device_guid = self._device_indexer.get(device_id) + + if device_guid: + try: + data_mode = constants.DataMode[mode].value + except KeyError: + raise Exception("Wrong mode parameter") + + payload = { + "deviceGuid": device_guid, + "dataMode": data_mode, + "date": date, + "osTimezone": time_zone + } + + json_response = self._auth.execute_post( + self._get_device_history_url(), payload, "history", 200) + + return { + 'id': device_id, + 'parameters': self._read_parameters(json_response) + } + return None + + def get_device(self, device_id): + self._ensure_logged_in() + + device_guid = self._device_indexer.get(device_id) + + if device_guid: + json_response = self._auth.execute_get( + self._get_device_status_url(device_guid), "get_device", 200) + return { + 'id': device_id, + 'parameters': self._read_parameters(json_response['parameters']) + } + return None + + def set_device(self, device_id, **kwargs): + """ Set parameters of device + + Args: + device_id (str): Id of the device + kwargs : {temperature=float}, {mode=OperationMode}, {fanSpeed=FanSpeed}, {power=Power}, + {airSwingHorizontal=}, {airSwingVertical=}, {eco=EcoMode} + """ + + parameters = {} + air_x = None + air_y = None + + if kwargs is not None: + for key, value in kwargs.items(): + if key == 'power' and isinstance(value, constants.Power): + parameters['operate'] = value.value + + if key == 'temperature': + parameters['temperatureSet'] = value + + if key == 'mode' and isinstance(value, constants.OperationMode): + parameters['operationMode'] = value.value + + if key == 'fanSpeed' and isinstance(value, constants.FanSpeed): + parameters['fanSpeed'] = value.value + + if key == 'airSwingHorizontal' and isinstance(value, constants.AirSwingLR): + air_x = value + + if key == 'airSwingVertical' and isinstance(value, constants.AirSwingUD): + air_y = value + + if key == 'eco' and isinstance(value, constants.EcoMode): + parameters['ecoMode'] = value.value + + if key == 'nanoe' and \ + isinstance(value, constants.NanoeMode) and \ + value != constants.NanoeMode.Unavailable: + parameters['nanoe'] = value.value + + # routine to set the auto mode of fan + # (either horizontal, vertical, both or disabled) + if air_x is not None or air_y is not None: + fan_auto = 0 + device = self.get_device(device_id) + + if device and device['parameters']['airSwingHorizontal'].value == -1: + fan_auto = fan_auto | 1 + + if device and device['parameters']['airSwingVertical'].value == -1: + fan_auto = fan_auto | 2 + + if air_x is not None: + if air_x.value == -1: + fan_auto = fan_auto | 1 + else: + fan_auto = fan_auto & ~1 + parameters['airSwingLR'] = air_x.value + + if air_y is not None: + if air_y.value == -1: + fan_auto = fan_auto | 2 + else: + fan_auto = fan_auto & ~2 + print(air_y.name) + parameters['airSwingUD'] = air_y.value + + if fan_auto == 3: + parameters['fanAutoMode'] = constants.AirSwingAutoMode.Both.value + elif fan_auto == 1: + parameters['fanAutoMode'] = constants.AirSwingAutoMode.AirSwingLR.value + elif fan_auto == 2: + parameters['fanAutoMode'] = constants.AirSwingAutoMode.AirSwingUD.value + else: + parameters['fanAutoMode'] = constants.AirSwingAutoMode.Disabled.value + + device_guid = self._device_indexer.get(device_id) + if device_guid: + payload = { + "deviceGuid": device_guid, + "parameters": parameters + } + _ = self._auth.execute_post( + self._get_device_status_control_url(), payload, "set_device", 200) + return True + return False + + def _read_parameters(self, parameters=dict()): + value = dict() + + _convert = { + 'insideTemperature': 'temperatureInside', + 'outTemperature': 'temperatureOutside', + 'temperatureSet': 'temperature', + 'currencyUnit': 'currencyUnit', + 'energyConsumption': 'energyConsumption', + 'estimatedCost': 'estimatedCost', + 'historyDataList': 'historyDataList', + } + for key in _convert: + if key in parameters: + value[_convert[key]] = parameters[key] + + if 'operate' in parameters: + value['power'] = constants.Power(parameters['operate']) + + if 'operationMode' in parameters: + value['mode'] = constants.OperationMode( + parameters['operationMode']) + + if 'fanSpeed' in parameters: + value['fanSpeed'] = constants.FanSpeed(parameters['fanSpeed']) + + if 'airSwingLR' in parameters: + value['airSwingHorizontal'] = constants.AirSwingLR( + parameters['airSwingLR']) + + if 'airSwingUD' in parameters: + value['airSwingVertical'] = constants.AirSwingUD( + parameters['airSwingUD']) + + if 'ecoMode' in parameters: + value['eco'] = constants.EcoMode(parameters['ecoMode']) + + if 'nanoe' in parameters: + value['nanoe'] = constants.NanoeMode(parameters['nanoe']) + + if 'fanAutoMode' in parameters: + if parameters['fanAutoMode'] == constants.AirSwingAutoMode.Both.value: + value['airSwingHorizontal'] = constants.AirSwingLR.Auto + value['airSwingVertical'] = constants.AirSwingUD.Auto + elif parameters['fanAutoMode'] == constants.AirSwingAutoMode.AirSwingLR.value: + value['airSwingHorizontal'] = constants.AirSwingLR.Auto + elif parameters['fanAutoMode'] == constants.AirSwingAutoMode.AirSwingUD.value: + value['airSwingVertical'] = constants.AirSwingUD.Auto + + return value + + def _get_group_url(self): + return '{base_url}/device/group'.format( + base_url=constants.BASE_PATH_ACC + ) + + def _get_device_status_url(self, guid): + return '{base_url}/deviceStatus/{guid}'.format( + base_url=constants.BASE_PATH_ACC, + guid=re.sub(r'(?i)\%2f', 'f', quote_plus(guid)) + ) + + def _get_device_status_now_url(self, guid): + return '{base_url}/deviceStatus/now/{guid}'.format( + base_url=constants.BASE_PATH_ACC, + guid=re.sub(r'(?i)\%2f', 'f', quote_plus(guid)) + ) + + def _get_device_status_control_url(self): + return '{base_url}/deviceStatus/control'.format( + base_url=constants.BASE_PATH_ACC + ) + + def _get_device_history_url(self): + return '{base_url}/deviceHistoryData'.format( + base_url=constants.BASE_PATH_ACC, + ) diff --git a/panasonic_ac/packages/pcomfortcloud/authentication.py b/panasonic_ac/packages/pcomfortcloud/authentication.py new file mode 100644 index 000000000..1d5e47d8f --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/authentication.py @@ -0,0 +1,419 @@ +import base64 +import datetime +import hashlib +import json +import random +import string +import time +import urllib +import requests + +from bs4 import BeautifulSoup +from . import exceptions +from . import constants + +def generate_random_string(length): + return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) + + +def generate_random_string_hex(length): + return ''.join(random.choice(string.hexdigits) for _ in range(length)) + + +def check_response(response, function_description, expected_status): + if response.status_code != expected_status: + raise exceptions.ResponseError( + f"({function_description}: Expected status code {expected_status}, received: {response.status_code}: " + + f"{response.text}" + ) + + +def get_querystring_parameter_from_header_entry_url(response, header_entry, querystring_parameter): + header_entry_value = response.headers[header_entry] + parsed_url = urllib.parse.urlparse(header_entry_value) + params = urllib.parse.parse_qs(parsed_url.query) + return params.get(querystring_parameter, [None])[0] + + +class Authentication(): + # token: + # - access_token + # - refresh_token + # - id_token + # - unix_timestamp_token_received + # - expires_in_sec + # - acc_client_id + # - scope + + def __init__(self, username, password, token, raw=False): + self._username = username + self._password = password + self._token = token + self._raw = raw + self._app_version = constants.X_APP_VERSION + self._update_app_version() + + def _check_token_is_valid(self): + if self._raw: + print("--- Checking token is valid") + if self._token is not None: + now = datetime.datetime.now() + now_unix = time.mktime(now.timetuple()) + + # multiple parts in access_token which are separated by . + part_of_token_b64 = str(self._token["access_token"].split(".")[1]) + # as seen here: https://stackoverflow.com/questions/3302946/how-to-decode-base64-url-in-python + part_of_token = base64.urlsafe_b64decode( + part_of_token_b64 + '=' * (4 - len(part_of_token_b64) % 4)) + token_info_json = json.loads(part_of_token) + + if self._raw: + print(json.dumps(token_info_json, indent=4)) + + expiry_in_token = token_info_json["exp"] + + if (now_unix > expiry_in_token) or \ + (now_unix > self._token["unix_timestamp_token_received"] + self._token["expires_in_sec"]): + return False + return True + else: + return False + + def _get_new_token(self): + requests_session = requests.Session() + + # generate initial state and code_challenge + state = generate_random_string(20) + code_verifier = generate_random_string(43) + + code_challenge = base64.urlsafe_b64encode( + hashlib.sha256( + code_verifier.encode('utf-8') + ).digest()).split('='.encode('utf-8'))[0].decode('utf-8') + + # -------------------------------------------------------------------- + # AUTHORIZE + # -------------------------------------------------------------------- + + response = requests_session.get( + f'{constants.BASE_PATH_AUTH}/authorize', + headers={ + "user-agent": "okhttp/4.10.0", + }, + params={ + "scope": "openid offline_access comfortcloud.control a2w.control", + "audience": f"https://digital.panasonic.com/{constants.APP_CLIENT_ID}/api/v1/", + "protocol": "oauth2", + "response_type": "code", + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "auth0Client": constants.AUTH_0_CLIENT, + "client_id": constants.APP_CLIENT_ID, + "redirect_uri": constants.REDIRECT_URI, + "state": state, + }, + allow_redirects=False) + check_response(response, 'authorize', 302) + + # ------------------------------------------------------------------- + # FOLLOW REDIRECT + # ------------------------------------------------------------------- + + location = response.headers['Location'] + state = get_querystring_parameter_from_header_entry_url( + response, 'Location', 'state') + + if not location.startswith(constants.REDIRECT_URI): + response = requests_session.get( + f"{constants.BASE_PATH_AUTH}/{location}", + allow_redirects=False) + check_response(response, 'authorize_redirect', 200) + + # get the "_csrf" cookie + csrf = response.cookies['_csrf'] + + # ------------------------------------------------------------------- + # LOGIN + # ------------------------------------------------------------------- + + response = requests_session.post( + f'{constants.BASE_PATH_AUTH}/usernamepassword/login', + headers={ + "Auth0-Client": constants.AUTH_0_CLIENT, + "user-agent": "okhttp/4.10.0", + }, + json={ + "client_id": constants.APP_CLIENT_ID, + "redirect_uri": constants.REDIRECT_URI, + "tenant": "pdpauthglb-a1", + "response_type": "code", + "scope": "openid offline_access comfortcloud.control a2w.control", + "audience": f"https://digital.panasonic.com/{constants.APP_CLIENT_ID}/api/v1/", + "_csrf": csrf, + "state": state, + "_intstate": "deprecated", + "username": self._username, + "password": self._password, + "lang": "en", + "connection": "PanasonicID-Authentication" + }, + allow_redirects=False) + check_response(response, 'login', 200) + + # ------------------------------------------------------------------- + # CALLBACK + # ------------------------------------------------------------------- + + # get wa, wresult, wctx from body + soup = BeautifulSoup(response.content, "html.parser") + input_lines = soup.find_all("input", {"type": "hidden"}) + parameters = dict() + for input_line in input_lines: + parameters[input_line.get("name")] = input_line.get("value") + + user_agent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 " + user_agent += "(KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36" + + response = requests_session.post( + url=f"{constants.BASE_PATH_AUTH}/login/callback", + data=parameters, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": user_agent, + }, + allow_redirects=False) + check_response(response, 'login_callback', 302) + + # ------------------------------------------------------------------ + # FOLLOW REDIRECT + # ------------------------------------------------------------------ + + location = response.headers['Location'] + + response = requests_session.get( + f"{constants.BASE_PATH_AUTH}/{location}", + allow_redirects=False) + check_response(response, 'login_redirect', 302) + + # ------------------------------------------------------------------ + # GET TOKEN + # ------------------------------------------------------------------ + + code = get_querystring_parameter_from_header_entry_url( + response, 'Location', 'code') + + # do before, so that timestamp is older rather than newer + now = datetime.datetime.now() + unix_time_token_received = time.mktime(now.timetuple()) + + response = requests_session.post( + f'{constants.BASE_PATH_AUTH}/oauth/token', + headers={ + "Auth0-Client": constants.AUTH_0_CLIENT, + "user-agent": "okhttp/4.10.0", + }, + json={ + "scope": "openid", + "client_id": constants.APP_CLIENT_ID, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": constants.REDIRECT_URI, + "code_verifier": code_verifier + }, + allow_redirects=False) + check_response(response, 'get_token', 200) + + token_response = json.loads(response.text) + + # ------------------------------------------------------------------ + # RETRIEVE ACC_CLIENT_ID + # ------------------------------------------------------------------ + now = datetime.datetime.now() + timestamp = now.strftime("%Y-%m-%d %H:%M:%S") + response = requests.post( + f'{constants.BASE_PATH_ACC}/auth/v2/login', + headers={ + "Content-Type": "application/json;charset=utf-8", + "User-Agent": "G-RAC", + "x-app-name": "Comfort Cloud", + "x-app-timestamp": timestamp, + "x-app-type": "1", + "x-app-version": self._app_version, + "x-cfc-api-key": generate_random_string_hex(128), + "x-user-authorization-v2": "Bearer " + token_response["access_token"] + }, + json={ + "language": 0 + }) + check_response(response, 'get_acc_client_id', 200) + + json_body = json.loads(response.text) + acc_client_id = json_body["clientId"] + + self._token = { + "access_token": token_response["access_token"], + "refresh_token": token_response["refresh_token"], + "id_token": token_response["id_token"], + "unix_timestamp_token_received": unix_time_token_received, + "expires_in_sec": token_response["expires_in"], + "acc_client_id": acc_client_id, + "scope": token_response["scope"] + } + + def get_token(self): + return self._token + + def login(self): + if self._token is not None: + if not self._check_token_is_valid(): + self._refresh_token() + else: + self._get_new_token() + + def logout(self): + response = requests.post( + f"{constants.BASE_PATH_ACC}/auth/v2/logout", + headers=self._get_header_for_api_calls() + ) + check_response(response, "logout", 200) + if json.loads(response.text)["result"] != 0: + # issue during logout, but do we really care? + pass + + def _refresh_token(self): + # do before, so that timestamp is older rather than newer + now = datetime.datetime.now() + unix_time_token_received = time.mktime(now.timetuple()) + + response = requests.post( + f'{constants.BASE_PATH_AUTH}/oauth/token', + headers={ + "Auth0-Client": constants.AUTH_0_CLIENT, + "user-agent": "okhttp/4.10.0", + }, + json={ + "scope": self._token["scope"], + "client_id": constants.APP_CLIENT_ID, + "refresh_token": self._token["refresh_token"], + "grant_type": "refresh_token" + }, + allow_redirects=False) + check_response(response, 'refresh_token', 200) + token_response = json.loads(response.text) + + self._token = { + "access_token": token_response["access_token"], + "refresh_token": token_response["refresh_token"], + "id_token": token_response["id_token"], + "unix_timestamp_token_received": unix_time_token_received, + "expires_in_sec": token_response["expires_in"], + "acc_client_id": self._token["acc_client_id"], + "scope": token_response["scope"] + } + + def _get_header_for_api_calls(self, no_client_id=False): + now = datetime.datetime.now() + timestamp = now.strftime("%Y-%m-%d %H:%M:%S") + return { + "Content-Type": "application/json;charset=utf-8", + "x-app-name": "Comfort Cloud", + "user-agent": "G-RAC", + "x-app-timestamp": timestamp, + "x-app-type": "1", + "x-app-version": self._app_version, + # Seems to work by either setting X-CFC-API-KEY to 0 or to a 128-long hex string + # "X-CFC-API-KEY": "0", + "x-cfc-api-key": generate_random_string_hex(128), + "x-client-id": self._token["acc_client_id"], + "x-user-authorization-v2": "Bearer " + self._token["access_token"] + } + + def _get_user_info(self): + response = requests.get( + f'{constants.BASE_PATH_AUTH}/userinfo', + headers={ + "Auth0-Client": self.AUTH_0_CLIENT, + "Authorization": "Bearer " + self._token["access_token"] + }) + check_response(response, 'userinfo', 200) + + def execute_post(self, + url, + json_data, + function_description, + expected_status_code): + self._ensure_valid_token() + + try: + response = requests.post( + url, + json=json_data, + headers=self._get_header_for_api_calls() + ) + except requests.exceptions.RequestException as ex: + raise exceptions.RequestError(ex) + + self._print_response_if_raw_is_set(response, function_description) + check_response(response, function_description, expected_status_code) + return json.loads(response.text) + + def execute_get(self, url, function_description, expected_status_code): + self._ensure_valid_token() + + try: + response = requests.get( + url, + headers=self._get_header_for_api_calls() + ) + except requests.exceptions.RequestException as ex: + raise exceptions.RequestError(ex) + + self._print_response_if_raw_is_set(response, function_description) + check_response(response, function_description, expected_status_code) + return json.loads(response.text) + + def _print_response_if_raw_is_set(self, response, function_description): + if self._raw: + print("=" * 79) + print(f"Response: {function_description}") + print("=" * 79) + print(f"Status: {response.status_code}") + print("-" * 79) + print("Headers:") + for header in response.headers: + print(f'{header}: {response.headers[header]}') + print("-" * 79) + print("Response body:") + print(response.text) + print("-" * 79) + + def _ensure_valid_token(self): + if self._check_token_is_valid(): + return + self._refresh_token() + + def _update_app_version(self): + if self._raw: + print("--- auto detecting latest app version") + try: + response = requests.get(constants.APPBRAIN_URL) + responseContent = response.content + soup = BeautifulSoup(responseContent, "html.parser") + meta_tag = soup.find("meta", itemprop="softwareVersion") + if meta_tag is not None: + version = meta_tag['content'] + self._app_version = version + if self._raw: + print("--- found version: {}".format(self._app_version)) + return + else: + self._app_version = constants.X_APP_VERSION + print("--- Error finding meta_tag") + return + + except Exception: + self._app_version = constants.X_APP_VERSION + if self._raw: + print("--- failed to detect app version using version default") + pass + diff --git a/panasonic_ac/packages/pcomfortcloud/constants.py b/panasonic_ac/packages/pcomfortcloud/constants.py new file mode 100644 index 000000000..9b999d1a7 --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/constants.py @@ -0,0 +1,69 @@ +from enum import Enum + +APP_CLIENT_ID = "Xmy6xIYIitMxngjB2rHvlm6HSDNnaMJx" +AUTH_0_CLIENT = "eyJuYW1lIjoiQXV0aDAuQW5kcm9pZCIsImVudiI6eyJhbmRyb2lkIjoiMzAifSwidmVyc2lvbiI6IjIuOS4zIn0=" +REDIRECT_URI = "panasonic-iot-cfc://authglb.digital.panasonic.com/android/com.panasonic.ACCsmart/callback" +BASE_PATH_AUTH = "https://authglb.digital.panasonic.com" +BASE_PATH_ACC = "https://accsmart.panasonic.com" +X_APP_VERSION = "1.21.0" +APPBRAIN_URL = "https://www.appbrain.com/app/panasonic-comfort-cloud/com.panasonic.ACCsmart" + +class Power(Enum): + Off = 0 + On = 1 + +class OperationMode(Enum): + Auto = 0 + Dry = 1 + Cool = 2 + Heat = 3 + Fan = 4 + +class AirSwingUD(Enum): + Auto = -1 + Up = 0 + UpMid = 3 + Mid = 2 + DownMid = 4 + Down = 1 + Swing = 5 + +class AirSwingLR(Enum): + Auto = -1 + Left = 1 + LeftMid = 5 + Mid = 2 + RightMid = 4 + Right = 0 + +class EcoMode(Enum): + Auto = 0 + Powerful = 1 + Quiet = 2 + +class AirSwingAutoMode(Enum): + Disabled = 1 + Both = 0 + AirSwingLR = 3 + AirSwingUD = 2 + +class FanSpeed(Enum): + Auto = 0 + Low = 1 + LowMid = 2 + Mid = 3 + HighMid = 4 + High = 5 + +class DataMode(Enum): + Day = 0 + Week = 1 + Month = 2 + Year = 4 + +class NanoeMode(Enum): + Unavailable = 0 + Off = 1 + On = 2 + ModeG = 3 + All = 4 diff --git a/panasonic_ac/packages/pcomfortcloud/exceptions.py b/panasonic_ac/packages/pcomfortcloud/exceptions.py new file mode 100644 index 000000000..73e5e5194 --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/exceptions.py @@ -0,0 +1,14 @@ +class Error(Exception): + pass + + +class LoginError(Error): + pass + + +class RequestError(Error): + pass + + +class ResponseError(Error): + pass diff --git a/panasonic_ac/packages/pcomfortcloud/session.py b/panasonic_ac/packages/pcomfortcloud/session.py new file mode 100644 index 000000000..ee6368074 --- /dev/null +++ b/panasonic_ac/packages/pcomfortcloud/session.py @@ -0,0 +1,46 @@ +from .authentication import Authentication +from .apiclient import ApiClient +from . import constants + +class Session(Authentication): + def __init__(self, username, password, token, raw=False): + super().__init__(username, password, token, raw) + + self._app_version = constants.X_APP_VERSION + self._update_app_version() + + self._api = ApiClient(self, raw) + + def get_token(self): + return super().get_token() + + def login(self): + super().login() + + def logout(self): + super().logout() + + def execute_post(self, + url, + json_data, + function_description, + expected_status_code): + return super().execute_post(url, json_data, function_description, expected_status_code) + + def execute_get(self, url, function_description, expected_status_code): + return super().execute_get(url, function_description, expected_status_code) + + def get_devices(self, group=None): + return self._api.get_devices() + + def dump(self, id): + return self._api.dump(id) + + def history(self, id, mode, date, tz="+01:00"): + return self._api.history(id, mode, date, tz) + + def get_device(self, id): + return self._api.get_device(id) + + def set_device(self, id, **kwargs): + return self._api.set_device(id, **kwargs) diff --git a/panasonic_ac/plugin.yaml b/panasonic_ac/plugin.yaml new file mode 100755 index 000000000..976d46d2c --- /dev/null +++ b/panasonic_ac/plugin.yaml @@ -0,0 +1,200 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: gateway # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Ansteuerung von Panasonic Klimaanlagen über die Panasonic Comfort Cloud' + en: 'Control Panasonic air conditioning systems using the Panasonc comfort cloud' + maintainer: msinn +# tester: # Who tests this plugin? + state: develop # change to ready when done with development + keywords: panasonic cloud air-condition +# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1976353-support-thread-für-das-panasonic_ac-plugin + + version: 0.3.0 # Plugin version (must match the version specified in __init__.py) + sh_minversion: '1.10.0' # 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: unknown + configuration_needed: False # False: The plugin will be enabled by the Admin GUI without configuration + classname: PanComfortCloud # class containing the plugin + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) + username: + type: str + description: + de: 'Login name für die Panasonic Comfort Cloud' + en: 'Login name for the Panasonic comfort cloud' + + password: + type: str + description: + de: 'Login Password für die Panasonic Comfort Cloud' + en: 'Login password for the Panasonic comfort cloud' + +item_attributes: + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + + pcc_index: + type: str + description: + de: "Index der anzusteuernden Klimaanlage" + en: "Index of the air conditioning unit" + + pcc_parameter: + type: str + valid_list: + - name + - temperatureInside + - temperatureOutside + - temperature + - power + - mode + - fanSpeed + - airSwingHorizontal + - airSwingVertical + - eco + - nanoe + - connected + description: + de: "Parameter der über pcc_index gewählten Klimaanlage" + en: "Parameter of the air conditioning unit selected by pcc_index" + + +item_structs: + # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + air_condition: + type: str + #pcc_index: 1 + pcc_parameter: name + device_connected: + type: bool + enforce_updates: True + pcc_index: ..:. + pcc_parameter: connected + temp_inside: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperatureInside + temp_outside: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperatureOutside + temp: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperature + power: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: power + mode: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: mode + fanspeed: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: fanSpeed + swing_hor: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: airSwingHorizontal + swing_vert: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: airSwingVertical + eco: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: eco + nanoe: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: nanoe + + air_condition_enum: + type: str + #pcc_index: 2 + pcc_parameter: name + device_connected: + type: bool + enforce_updates: True + pcc_index: ..:. + pcc_parameter: connected + temp_inside: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperatureInside + temp_outside: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperatureOutside + temp: + type: num + enforce_updates: True + pcc_index: ..:. + pcc_parameter: temperature + power: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: power + mode: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: mode + fanspeed: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: fanSpeed + swing_hor: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: airSwingHorizontal + swing_vert: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: airSwingVertical + eco: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: eco + nanoe: + type: str + enforce_updates: True + pcc_index: ..:. + pcc_parameter: nanoe + + +#item_attribute_prefixes: + # Definition of item attributes that only have a common prefix (enter 'item_attribute_prefixes: NONE' or ommit this section, if section should be empty) + # NOTE: This section should only be used, if really nessesary (e.g. for the stateengine plugin) + +plugin_functions: NONE + # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) + +logic_parameters: NONE + # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) + diff --git a/panasonic_ac/requirements.txt b/panasonic_ac/requirements.txt new file mode 100755 index 000000000..b5c7ab4a6 --- /dev/null +++ b/panasonic_ac/requirements.txt @@ -0,0 +1,2 @@ +pcomfortcloud +bs4 diff --git a/panasonic_ac/user_doc.rst b/panasonic_ac/user_doc.rst new file mode 100755 index 000000000..a2b4e2769 --- /dev/null +++ b/panasonic_ac/user_doc.rst @@ -0,0 +1,132 @@ +.. index:: Plugins; pcomfcloud +.. index:: pcomfcloud + +========== +pcomfcloud +========== + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + + + + +| + + +Plugin Instanz hinzufügen +========================= + +Da das Plugin ohne vorherige Konfiguration weiterer Parameter lauffähig ist, wird die Instanz beim Hinzufügen in +der Admin GUI auch gleich aktiviert und beim Neustart von SmartHomeNG geladen. Die Konfiguration erfolgt anschließend +im Web Interface. + +Das Plugin unterstützt je Instanz nur eine Bridge. Dafür ist es Multi-Instance fähig, so dass bei Einsatz mehrerer +Bridges einfach mehrere Instanzen des Plugins konfiguriert werden können. + + +Konfiguration +============= + +Die grundlegende Konfiguration des Plugins selbst, erfolgt durch das Web Interface des Plugins. Mit dem Web Interface +kann die Verbindung zu einer Bridge hergestellt werden kann. Optionale weitere Einstellungen +(z.B. default_transitionTime) können über die Admin GUI vorgenommen werden. Diese Parameter und die Informationen +zur Item-spezifischen Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/hue3` beschrieben. + +| + + + +| + +Web Interface +============= + +Das pcomfcloud Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzen +übersichtlich dargestellt werden. Außerdem können Informationen zu den Devices angezeigt werden, +die durch die Panasonic Comfort Cloud verwaltet werden. + + +Aufruf des Webinterfaces +------------------------ + +Das Plugin kann aus der Admin GUI (von der Seite Plugins/Plugin Liste aus) aufgerufen werden. Dazu auf der Seite +in der entsprechenden Zeile das Icon in der Spalte **Web Interface** anklicken. + +Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/plugin/pcomfcloud`` bzw. +``http://smarthome.local:8383/plugin/pcomfcloud_`` aufgerufen werden. + +| + +Beispiele +--------- + +Folgende Informationen können im Webinterface angezeigt werden: + +Oben rechts werden allgemeine Parameter zum Plugin angezeigt. Die weiteren Informationen werden in den +sechs Tabs des Webinterface angezeigt. + +Im ersten Tab werden die Items angezeigt, die das Plugin nutzen: + +.. image:: assets/webif_tab1.jpg + :class: screenshot + + +| +| + +Im zweiten Tab werden Informationen zu den Leuchten angezeigt, die in der Hue Bridge bekannt sind: + +.. image:: assets/webif_tab2.jpg + :class: screenshot + +| +| + +Im dritten Tab werden die Szenen angezeigt, die in der Hue Bridge definiert sind: + +.. image:: assets/webif_tab3.jpg + :class: screenshot + + +| +| + +Im vierten Tab werden die Gruppen angezeigt, die in der Hue Bridge definiert sind: + +.. image:: assets/webif_tab4.jpg + :class: screenshot + + +| +| + +Im fünften Tab werden die Sensoren angezeigt, die in der Hue Bridge bekannt sind: + +.. image:: assets/webif_tab5.jpg + :class: screenshot + +| +| + +Im sechsten Tab werden die Devices angezeigt, die in der Hue Bridge bekannt sind: + +.. image:: assets/webif_tab6.jpg + :class: screenshot + +| +| + +Auf dem siebten Reiter werden Informationen zur Hue Bridge angezeigt. Wenn weitere Anwendungen die Bridge nutzen, +wird zusätzlich eine Liste der in der Bridge konfigurierten Benutzer/Apps angezeigt. + +.. image:: assets/webif_tab7.jpg + :class: screenshot + +| +| + diff --git a/panasonic_ac/webif/__init__.py b/panasonic_ac/webif/__init__.py new file mode 100755 index 000000000..949880e86 --- /dev/null +++ b/panasonic_ac/webif/__init__.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2023- +######################################################################### +# 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 Sample 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() + + + def get_itemsdata(self): + + result = {} + + for item in self.plugin.get_item_list(): + item_config = self.plugin.get_item_config(item) + value_dict = {} + value_dict['path'] = item.property.path + value_dict['type'] = item.type() + value_dict['value'] = item() + if value_dict['type'] == 'dict': + value_dict['value'] = str(item()) + value_dict['index'] = item_config['index'] + value_dict['parameter'] = item_config['parameter'] + + value_dict['last_update'] = item.property.last_update.strftime('%d.%m.%y %H:%M:%S') + value_dict['last_change'] = item.property.last_change.strftime('%d.%m.%y %H:%M:%S') + + result[value_dict['path']] = value_dict + return result + + + def get_device_parameter(self, device_index, parametername, suffix=''): + result = self.plugin._devices[device_index]['parameters'].get(parametername, '-') + try: + result = str(result.value) + ' (' + result.name + ')' # get name of enum type + except: + pass + if result != '-': + result = str(result) + suffix + return result + + + def get_devicesdata(self): + + result = {} + + for device_index in self.plugin._devices: + value_dict = {} + value_dict['name'] = self.plugin._devices[device_index]['name'] + value_dict['group'] = self.plugin._devices[device_index]['group'] + value_dict['model'] = self.plugin._devices[device_index]['model'] + value_dict['id'] = self.plugin._devices[device_index]['id'] + # + try: + value_dict['parameters'] = {} + value_dict['parameters']['temperatureInside'] = self.get_device_parameter(device_index, 'temperatureInside', '°C') + value_dict['parameters']['temperatureOutside'] = self.get_device_parameter(device_index, 'temperatureOutside', '°C') + if value_dict['parameters']['temperatureOutside'] == '126°C': + value_dict['parameters']['temperatureOutside'] = '-' + value_dict['parameters']['temperature'] = self.get_device_parameter(device_index, 'temperature', '°C') + value_dict['parameters']['power'] = self.get_device_parameter(device_index, 'power') + value_dict['parameters']['mode'] = self.get_device_parameter(device_index, 'mode') + value_dict['parameters']['fanSpeed'] = self.get_device_parameter(device_index, 'fanSpeed') + value_dict['parameters']['airSwingHorizontal'] = self.get_device_parameter(device_index, 'airSwingHorizontal') + value_dict['parameters']['airSwingVertical'] = self.get_device_parameter(device_index, 'airSwingVertical') + value_dict['parameters']['eco'] = self.get_device_parameter(device_index, 'eco') + value_dict['parameters']['nanoe'] = self.get_device_parameter(device_index, 'nanoe') + except Exception as ex: + self.logger.warning(f"WebIf get_devicesdata(): Exception {ex}") + self.logger.warning(f" - Devicedata for index={device_index}: {self.plugin._devices[device_index]}") + self.logger.warning(f" - Devices: {self.plugin._devices}") + result[device_index] = value_dict + return result + + + @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 being 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. + """ + # self.plugin.logger.info(f"get_data_html: dataSet={dataSet}") + item_list = [] + if dataSet is None : + result_array = [] + + # callect data for items + items = self.get_itemsdata() + for item in items: + value_dict = {} + for key in items[item]: + value_dict[key] = items[item][key] + + item_list.append(value_dict) + + # collect data for devices + devices_data = self.get_devicesdata() + + # if dataSets are used, define them here + if dataSet == 'overview': + # get the new data from the plugin variable called _webdata + data = self.plugin._webdata + try: + data = json.dumps(data) + return data + except Exception as e: + self.logger.error(f"get_data_html exception: {e}") + + result = {'items': item_list, 'devices': devices_data} + + # send result to wen interface + try: + data = json.dumps(result) + if data: + return data + else: + return None + except Exception as e: + self.logger.error(f"get_data_html exception: {e}") + self.logger.error(f"- {result}") + + return {} + diff --git a/panasonic_ac/webif/static/img/plugin_logo.png b/panasonic_ac/webif/static/img/plugin_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ad05a451d9cc80812a081b8b9f829093484829 GIT binary patch literal 27830 zcmd3Ng;N~O7wrOzySoN=2m}f41b0~6!r~I#A-KB)0xTYMfdztFfM8+qV8MeW_y&i^ z_j^_EZ+KI8W~!&Ur)T<3ci%qeOuV+HGAzjl$o|Wu@96Df4{&mH_+ahFKs+!Tg&GwSR_`e+T z?j8YNjt*dNfV@0EgSL;2i=!=rg1f7=qZ@;Qy`7`2wYQ_Y8w1$e+e4g>&(_QSCGBf( z>+b66?)Kro>i9hVSH|vtMfd{kz1;Z(c=`E8c6x3B0OxvD1zCOnl|MaxUzwEyPM&e2UKutY-T%L= z{#M~LC%EORIL(6qlJQsk0lvxclQn_?Zkv%vP9v51SF)tk;oYX^pMj=(sTAB#DPa~Z z2v(?i|Er7?cgsiI$&SyDAtJU4=u&`9T_kVezjTyeKkIo9(px-}(zxjp?g^hmgDKYu ztWZ~pv;jbk6ud}Tj+K%A8P`Z>dO)M&5HSnCkt{~u!*^K#RyYu2T6QxGitZ`m9(Lmb zys7rqbIIy6H}Yi7gbz_LY+K2N%cS*hrURHz2{>d30BG45NvelNnVaGM3=>S@5&*6D zDX9Gb0u&YGuQ_3#1<=xPJZK3<-c?Nc@)V;Jh8tK}8`~Y<7y*L!uL!!ffHM`t*+yyB zw=qDSFN~BI1c%(M+kY$Fo$=C^)l9SOu+7OyG~(KiqNF>HMb0Bc9#Nhq>Nl=WQnx>T z>KCy?TQAd=%TUSc>T|kw|Mc}yudY%jq8;VobOp`Wytw2!C-bY6UKJKb#y3DEf&aU5 z>Os3sMt)2NrOnRo2458bUk;VVIow4Ak@Ij40`C}qyiutvr*7cAz)ccd_2GGz*R4FS zOiop&%W|C0R~kq9P8Ci8{X?iy;hI7jOxG7LJ(B8FUoddJJ$0uP>@%!oN(7HD6VAx` zvawxmcQjF!)?}{&fS+dD^ojr@#cFn~SOlNrAMjZ&zI_jk;twwe7x1_B3$E7f>hZV_ zMC_^Z_*{5gSe`tvrfisGRC*w3#YP|d#ZQXTeYa)F z+X}Pqt5ehJE2mNphig|ODd@F zAaBtL03+zF@PDO9ejRe`x#}wpaS3?)Y?>y_W&Xo`{1;Z`3ID<{GnzQ&9s#x6;Lxk4h-8f)b6+kbBC|VB9^5nDE62C1mqv0Jl=>~c=%xds=NS;qLjnp@0 z%1y?LekMSvAvhZ;%@v%DVnPaz0BDKG8|8Z6|vIJX9IiO>Qu*T>e7_ILQygDuE(uY>la*90cJiPyOw z)AHBRNl6=T7@7m<`6uSKX*FY4kf9w7{O1|e4}|gzlLMsa|>Ju@_loU9>G118{CPk8Sdf?SN<=i)Q3&P55!@< zEKd!t;Rb(*xWOvJxS8BS{Mg}N&|v_pYYch*%_;YV&n8fm^`3%uyc#^f4gf@Yl=aed z>X9>)>_`A|6Q%`15EP9^|07wAd}38GC|}LL0(@)3&45rF}~3@8UvI29BB$28k65QvF2v%B#FEb#4J8i2a{I z{qJ*M9qXLYQ+`apPxX=5KgbSp=FoL(*u2Q?Hyy+I>-xR+C>Igpdus7j@8yHQTz?B@tz7N8iZs{e4l*o z#SZeQ%Lxp)sp1RE*Tm(^K~d*{d(j?c2OBT*Lgr<_>1uG!%<1@7f&czJ*-s~`EMB~w zvwT&l@a^x^!Qu+CP4@dWDYBe!3*=9%@)H@ad%@3Xoy2_HyA%kmRztNvBq>vWQY8s= z?_KXUYUFEqbHK8>a8XFDM;4S0&KZ%6nr0H2qbe1gzB61V4#2>?$)VPeF;@vOPq|!B z@^z*~YO=MqFFaxRuW_+KzRCn2>=|f+S!1#Xm;sXz{Ps>A5v}#d?TxF$w?shE7_$6d zE+G(u1~nC*$Q*;8c_~TDQH#6IR5{xKBYAr+X)RfYu>0;lg_W<=mef?Mhm-itI5^dnr zoaL(GCXdZ4!xcQ3`qAZ0*R`qJ;&iPR{qw()PDTALKjUb_dSdmlUEA}EVmWLz&hEU0 znSpH%Tzzk;cUDHkFm7W@u2`Wz#e@Lxwn@FU9G+ldq#TYfZQOd zMM-FLS=1Ydf#ASAGK}-Cn?{rWrQO7+(6pRo{|pta#B?Oi{VLWBb>6b~Ew3{;z~Dzj zlQqGJkK|_Y;6~wAxv`L3i4>VUwOM0u(T{bjKbCP3p(jJ?yjB0#ic~-RDk{^- zP450LqPk3eT62Rg?d}MWcULFPZ0J#wB~THaKaFKj63q!8|I1d^@&yZKwQ-j~x8Uim z1L#*Pz7@0hGJ0AUc(poD`UsZnYIVccv};(Enxg_v$JKXW)}u8@OZW=nq%X$K10;-6 z#aH~U=ffsXmmHkvFr(GrHE%dg4!ozmF<(#29wYI*cIchJj>^sTZ@a%N3v5MQK}q)Z ztS(w#`2;b5X=H^U8jY+lKL#y&-{P8F$O}6^ZrKQZS`VIetGMU){oWSCxsb1@JlF`m#lxaYF|mUuG2OswWFBhgzs5=(hPHY2d{Fnr-4qBdyV$C!8ZWvp@&3{~BbBKJ>OxwvATsjM&l8v7QDioPz*37Y@?24+PI zdZ$!WH*K^r@TgwYRZ1^LGBJM7MOrP+sJYX;7*%&Lz@6u z&{Rl>k>ruep+hlE9z&yRVw1@+K6d*U%iF0Ed*oNwkUg`W`M2Nr=kYNjMCNxyo~EfG zl_ckZm8FrB*b7G2Xf|jGpnpN5lEQ6MoO*vWe;?ir|>Q!%hC71AvrS>U(~vgy}2X@tp;Gi|?N8ZEmt+ zE0cq>PQ_!tx)HJRjA-tBGGEk3V|V!0>Wbm9Kc^&ZVN;#YUpftbrd3ENl98cb>jkxjOwQjOjjDDKL7CTy8WMNlcMN*6X z{o|tGH}_sTw_+ds^0~weys-j};qv1?OP1W=` zEkc(Sg}6)Kg=YA#-&1c3f!pw5?}luu+}95~cs>l&j%%ML)so>%Mvw)iIjTt#<@)f- z=tcSyx=aS8h)rF$eGw?S(L6|`hrX&fHF+kSQ_SO27iv8fTi*MKVch+8L0HWc;BqWgt4sbylT`TKWahMen$kbwg zpg$a@x97jtQ1H1YFZ7?znoNI-yu5DM243q@$0W9}2)lBZiSf5NbGcfwbzt!3VGb+Z-g_Q{lsgdfLuxptw zE9DcNbotf;qhsHwvr7U(L$9a4{{4}<9a+(N? z+{sBhMG0i=)e@220>v0H_C2%|>59jq)YNCDc^??OrKB7{N^6>7q!YUDo^ck{@JbUI zWiBc@E$wTh^boUDlH`qT(l+{yO2Afolizm2jpkjbDEo+IyMY|sHgAu6ve7XRpX`Vr ztBA`$Kuf>lc^jMhg1Zfhh2?akhD>nyY_ItP*1Z;rl+L66Z!l)3nf~&Yj7~Z7qR)1~ zGrOU<7qhf+_-Fy0Mv~OMe$TpLwsK1~laxxX-fDu+$olF-F?ECznAS?@+ow7j>Q123I z0;Ys8<0NM#lo{Y)FqBk>zF7;%>+u#OMrm3@qUl8ZIhn>#{Z)zkwM z6b+a>t$qswM0H0kxn@F%9P-SF@<6|hRRNe9GIn)MC9&`ww!)bqej1yA*ir%?H1%wW z#1u~~p4ZrkN!t7o=mz12p?zX72KLNkG1%<=ggn=}s3NFLw5y&9HXg2Si0L)*?uYQd zchE0Z!`Ud<T^4U# z;-s<=Ra|Z3Cv?8Ql+?FVPiJt=t3+vI3Ad>$NLUfcl!;Hd3figu<9<7aF)e8WFpAS^f!W z4u*(jx3yj%1!*p*&1fTZyKd(jg7&mA2*@tnM65h%RR=!TU$)GbfIJ={wQ#w|5`zid zq6{55G(G>ccpp71mJeLumwPWN{;sGYHDW`|`l96UrV)JNN5>VW2Q6NDjYc|h!q&{f zz(+$)HrI{O^J>RTmo}JjKDvCawfQIF7gF3mi$i)1m*_iR;%|JATcdQHD1NODymV2Y zufAY1(E!|WDDV6hn=2YfzDQM+>1JHpAnlC`dhxoC*?1;8nR8uPkF{C|NJe1b4S~5n zpX=MIAS}-T+VGJnL@X=Zsr4pyIHmQ|gBq@Jm~mZf_BzN2tkW~EhUr$5Ylxa}4mOz_ zZ6dJ3maRR!gvzL3PH9u(v3e0oenf2c#Y6}* zF`umOXY*)@f{rIROwD*Yz?cDDEyT(*q%^xis^a+4>ZiHHvaY`Ildg8F{RKy^Uu!U6 z=h5B}Q-7jMPUw3kT3}(HN5~iaS}K?}kmWTo*hry8evar&+M>?h~)F%E&1vY3Cyze%m9bRlm@+qi7>>p5jO6)(eEn9}=-Il(Sy4fQLerMGAwtkdQlb4~g zei4?XoK6B5qk)Q2Dijp@J}1@Br%PzqLKal=b64v5P+)(&oo7P(2P5`V9yddldXAr^ z+8Le6VcKY;vA!PIP}q47@WWFNDHzMi;IWn|AhNc1@;{B`;?)tHMm(v@uX=TSkCl%=8 zbvlIK?K?5vdL#8OyvqmjtNy3=+^MD*j({aJNs36|M34G*afk))e!N2R<+4V+s zx>2oR&*xrJn)rLhWVfLl*De9-w-G~XL7p~y1_u%bUM7t+{f1^A`+_%>;r9y2{T+T9 z)^;|g*L|AZwl%E~fEjr#Naitq8 z(E6LggjK;}1Zqmzbk*5Bm$+d@hiz)4$6KU(By}R|bQDQ~pPb>BBHlBmZ7D4cxWq&f zJ6;2;`dJ)17quK~cVE!}(zjPAUm@$^_Uh&UrtqLNPEGlJj{V>R{Kh_8x83yguXJwW zut{F)K?Y^`5h7)~dB$(2OZtI>u5g{+DCYcW)#040M!EN{4!?*P3kMyWs)XOMx{)qn zyMeqcj|AnX*8u8SV2Eh%16B(6sk~?hAf6-%*luO?Y=1M}w;mZHS*1%f)VMAdUClkS z#P_hU-t#UugKm`zJ*x%|#b&wOS)_vf33u`7i@B$NXYC~HH^Q@dbEzdARmIgBE9oGE zz}ZgbJWH%foedIk%aC{^7Tjdh1E0tB4_&i(fGuPQLQh{`?=-GZ8sn`1qTiQldmyIwe~xycKe|gkF8w&pv>P)V z(qctD@%KjJRexDNiCn?{P(#2z>f)#J;P; zMQQ{WfoD%m1dqS|dA8YmzxwEB_QPCS_{+|150ypUHh>>HWlrjmfb4C*Q#k%?mv5vT9(aFz^KtPdLBcrtb?uw>bJtX63&j*>dprh2Y$?yfujMLJhjI&Z8IGi2gIa0oCk`%##Nt`cQCHQ;I;?Py)i%cNol1I&?U2s2 z-e8{2^ly#f9@zwENd9;@8xL30p5^ABPo`AY4|)!#*(|dBG*iS;lA(6A!c-t;3~C$q zZ$@-o#DR@*as&UAf0s*({iSv2Zr@L04AXm%NNti8GXsEU$xCaaTZTikajZ*SDh{r+U-!r=);0pK}v+AAHot1F;W3 zH{c}|Vq(PPEXf)vwXQnC3k?=W)uB$g0r51mXYaH69MHvD6i0|2ns z(v=yCQss81{bu83}YHw6Ca}lXm>8iFlLRT`$88Es0d?fm*Gx2h*^Jy@T%PXwMAanRupc4 z+4d{LE%?zne#B(bzMVu@03JX>@+sUoxuHM3R}-?A5XiA$&?7`R!Na}2fn<2k%*y5B zHx$`c`UIb4l9BsSe|ZTA|3>j)SQq@he=bnIw%XZwnusN+tP1ts`UNe=sn06P&lvEm zL8*JAT9kNJK%6@_(nJR#TSmfoxp?%w+<18}~{WB(+MzR%)Co976 zcX3UnAOAbG8}IPeCt_6OD@gB@dP6JqZ#38%7R!hoL)y~q8ZLlDwa9y(-IVd&hXoE? zJc!ArilzJu(D|2&Au1R^esBz^rLC(~JAYecKINMQu%i7YeO23$I`s2nqGvFI;~l1R zaz;X_^PAW2K1gk|84sH}6j@nSHg_IE!elKdVQ?9Wqa2Sxe2RRRRTB524V(aChIFNRCRsx2s;qW+sATMo1GL;5?& zE;N|5L6-E#lsvk936Pk50Qe5^HFOIMahH+Xjbd$N<_}m6mFPJ6{DVg)ftnGO?eI%` z$UCtKX$9Sg3jlhG5rb@*d_eb&UieAUtV*vI_F{V(~F>;2oK3l%pmm!n8q z@eH|QqsUzQY)-CP27SFqkGk2-&T_Bgnp`EVL;tNA1y0h@%JxS!@r&4k!-fqR5xWQE z@MNI6DwSK$9rbW}%?gI`)6$lR#k1^Q
  • QliB@i$EYTjh}@GUPcXf*C~LSIpeIk6 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. + +Extension can be png, svg or jpg + diff --git a/panasonic_ac/webif/templates/index.html b/panasonic_ac/webif/templates/index.html new file mode 100755 index 000000000..cd5063799 --- /dev/null +++ b/panasonic_ac/webif/templates/index.html @@ -0,0 +1,488 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 5000 %} + + + + + + + + + + +{% set buttons = true %} + + +{% set autorefresh_buttons = true %} + + +{% set reload_button = false %} + + +{% set close_button = false %} + + +{% set row_count = true %} + + +{% set initial_update = true %} + + +{% block pluginstyles %} + +{% endblock pluginstyles %} + + +{% block pluginscripts %} + + + + + + + + +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Prompt 1{% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %}Prompt 4{{ _('Wert 4') }}
    Prompt 2{{ _('Wert 2') }}Prompt 5-
    Prompt 3-Prompt 6-
    +{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
    + + +
    +{% endif %} +{% endblock %} + + +{% set tabcount = 3 %} + + + +{% if p._plg_item_dict|length == 0 %} + {% set start_tab = 2 %} +{% endif %} + + + +{% set tab1title = "Plugin Items (" ~ p._plg_item_dict|length ~ ")" %} +{% block bodytab1 %} +
    +
    +
    +
    + Etwaige Informationen unterhalb der Tabelle (optional) +
    + +{% endblock bodytab1 %} + + + +{% set tab2title = " Geräte Einstellungen (" ~ p._devices | length ~ ")" %} +{% block bodytab2 %} + +
    +
    +
    + +{% endblock bodytab2 %} + + +{% set tab3title = "Klima-Geräte (" ~ p._devices | length ~ ")" %} +{% block bodytab3 %} + +
    +
    +
    + +{% endblock bodytab3 %} + + + +{% block bodytab4 %} +{% endblock bodytab4 %} From d35216ee918c141073e8e019d6d5be47ddb455f8 Mon Sep 17 00:00:00 2001 From: msinn Date: Sat, 31 Aug 2024 16:47:46 +0200 Subject: [PATCH 057/121] panasonic_ac: Updated user_doc.rst and added plugin logo --- panasonic_ac/__init__.py | 2 +- panasonic_ac/assets/webif_tab1.jpg | Bin 0 -> 215620 bytes panasonic_ac/assets/webif_tab2.jpg | Bin 0 -> 92131 bytes panasonic_ac/assets/webif_tab3.jpg | Bin 0 -> 92225 bytes panasonic_ac/plugin.yaml | 2 +- panasonic_ac/user_doc.rst | 121 ++++++++++-------- panasonic_ac/webif/static/img/plugin_logo.jpg | Bin 0 -> 106075 bytes .../webif/static/img/plugin_logo.jpg.off | Bin 0 -> 29636 bytes panasonic_ac/webif/static/img/plugin_logo.png | Bin 27830 -> 0 bytes panasonic_ac/webif/templates/index.html | 24 ++-- 10 files changed, 80 insertions(+), 69 deletions(-) create mode 100644 panasonic_ac/assets/webif_tab1.jpg create mode 100644 panasonic_ac/assets/webif_tab2.jpg create mode 100644 panasonic_ac/assets/webif_tab3.jpg create mode 100644 panasonic_ac/webif/static/img/plugin_logo.jpg create mode 100644 panasonic_ac/webif/static/img/plugin_logo.jpg.off delete mode 100644 panasonic_ac/webif/static/img/plugin_logo.png diff --git a/panasonic_ac/__init__.py b/panasonic_ac/__init__.py index 360ac20e8..ecf29536a 100644 --- a/panasonic_ac/__init__.py +++ b/panasonic_ac/__init__.py @@ -63,7 +63,7 @@ class properties and methods (class variables and class functions) are already available! """ - PLUGIN_VERSION = '0.3.0' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release + PLUGIN_VERSION = '0.3.1' # (must match the version specified in plugin.yaml), use '1.0.0' for your initial plugin Release def __init__(self, sh): """ diff --git a/panasonic_ac/assets/webif_tab1.jpg b/panasonic_ac/assets/webif_tab1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68dc0ea23b2fd3df3000d252486ba7418a4d90b4 GIT binary patch literal 215620 zcmeFZ2UwKNvM~4#Lk^O27$oPMbC4`KgJhH>IZGHMDp62CKtMnRBuSPaIZ2eP}uI{=1e7yt^s4J@}10WCps3QM> z>t*1!Qh?Jv0MOI~H~;{^1W-V102m2@02Ba-aRN|(!T`X6OaTDpCCYCM>efr}Zy4-i2sA@q6$ zk4a5Y(Mm^8TUqV4($5Owk=S^6+(#h*09Q93FFh3ndJ|JKdW^5gIzoUOfFuC2vGewj z*U`ECgXiz`C;vYm$5Ti-kYSwr2d_Wq{~ADOf7ja%sR$h;xvjmoohuTqK}MLJhnEik zfL|f;jQ&0zKj1zjOyY&CAQGPY0XzJHVLxEopYTsTka!vBDFOg01ya?xHa1=k0D#Mc zq@nk-b3pRJy@iDN-0Yp)kTCK)Ami%lZjXd(kT4sP#sdle;4fzT7d{Sufo*JT|M0Z2 zarguOk_DL)S@10r=^^hr2R?ue`ca1-=o2{1i zZ@d#yy5F#)ud3m1*v?xKsmot^?QL#p{D$4`YyD{NpFHn5DXaa4oqW^`e#3XYmGpnZ zUhaB-%DU&JDE}L8e9)HPYXYQ0MS$W!DWFVH9;g^p32FefgFb>r zL0>@2pe@h==nMsnf{Q|eLW{zJ!iyq?B9EehVt`_S;(+3b@&F|gB@yL0NOVb8Z=Hc2{bh{6Ep`j zf3#?{G_*HpwP@XFlW6N`$LQ$jWazBuqUfsVrs&S-LFn=5+2|GMZRn%utLR4<7#Nfo zoEXv=+88z%J{Zv$85pG)tr%k%>lg@3983tN0H!jg8KxU%IA$8=Tg(>BG0aWOb1VWZ zW-JLTZ7h4NK&&LJS6Gc$BUtNL=h%eUtk}}n2H4Kn53!$Pmt%Kh&tV_p;NUReh~wzu zIN^lhJjZ#5(}%N+bBarZ%Ymzidk5DSHxaiGw+;6T?jar?9t)lvo*AAGULsyGUMJoH z9^wY^4W1inH|%dfZ)Dx5yD@fS4<8Gk8DAdX68`~y8h$nY2>uQM76A)^B7qG-C_xrM z1Hm-G5g{=lKcP0ED`7n0Tf&coUx_e?ScsH}?1>_Y3Wz$1R*AvHjKqq>cEl0HuZX*e z*GbSxSV`1KoJrzH%1DMuV5CH(LZn8d0i;=^A4r$Tz+^0BYGn7xlE|varpV67Y02fu z?~*?zFDD-(KcS$ekfXRu5lc})F-dVoNl&Rn=|Y)ISx-4ng-XRirAy^cl|$7{wL?up zElF)h9ZOwBJwt;+!$G4*6GT%$Ge~npOGm3h>p`1A+ey1aM@A<{=S=sEu7z$3LJX0C zI64B-qF4D*cGjAD%U7@skA zFv6H1Oqxu=OeIV+%vj7~%#O_I%)QJfEUYZXEHNzgEMHkESk+h`u$HjSvEi}FvU#%Q zvrVw0vx~92uxGP>;y~dL=5XZ5;uzsX;S}L?;>_k8npYIjlEI$#yI)6BSBmaQ_hk%_xmcWD{o}jWI zRPenZT!>TXu27E9XJKMtP2m{f4&h4?Q4ueZa*-`jCQ)nA7oyW*Bw{*Z@nU`AXyOXu zQ1NE*Qwb3XABif7eMxRf7s3?n=FqT9syywvo=0UXr1gv6gu$vm|>{)m57uKl`@qUlo^yA zluMLhD#9v3Ds8H0sv4@vs?%z;YIbVHYA|(C^$_(g4O|U#*v0>NM%1>+0xc>2B!p>jmre>J#f*>A%%KHc&7~GMF`F zGxRm=Fv2&wV^nN(VtmUu#dyht*CfPbz?90=$@IM$hMAGs8?z&GCG%AC^*h3MqVG&u zuv!FIe6*ypyl?rz3g61cs@fXO+SvN7^@WYLO}@>Mt(xr%TbP}qUAo=2y`23s`>ngu zcT?_e-jlwUa__5yw8K+}Ek{|$G{+q$MW;+BxU;HruJehDwo9SQ^?l>}6|R`B)~*e1 zL~br_UG8-50q&z7oF360OP&&*sh%(|4X-!eAa4ur_dX=Z#nGTIn{SlwvY(7!mLJ03 z$iF&(Fu*NfFpwkgap2|yT)VT#ZtRDv7=k?G-&8BN>zX2=$TEqv6Mbk24-$#oEXA z$MMCb#a+bP#rG%hCp=HMPP~^moFtNz^922g+moqe+2o=W!juOot4}qb);*(t7W?db zs#WU8G@-QIbnJBB^rh#wpTEyw%6O7-k?EK@nWd0b@q*^X;}<8{_Ss`OayjL>w7GG) zXD^*ze$G?LtIKD}Pb)w#@GIDOW%R1|wb<+8H&k!p-dq;C6)qL&6?GMh78k#zd7D&% zQsP^(ReGoNQ`xPu_vPH>c@<<8aqob4zVE(OT31e0X;gJoi&dA`FxR}OC9aLF1JwoA z!Rj6Bm)@JcA8k-?=xCH|tZm|HDr~;lob`e9LsAQNOJvJ+>x0&#HjlQQc8B)W4y%sY zPLs}wF5Rx-ZjJ7~9_60SUiseEKIy*Zj}jjn`o;R|2Sf(y289P}hlGb}hed|#Mnp&6 ze-i)HI4U*TGA29LK7MPwXF_daU{Y&xbjon*^YoqRrO)=Czka#@1wP|9b3Pk3hc*{G zPcWahK(mm)$i7&yB(n5j`PTB_ivG&%s@>}Ln)lkpdgKP)M%pH1v-qpP*XAwdtxw;~ zzpZb3Y@hE$?-K65*kjwP*_YcNgqgtB;a>3TgZM+r!@}=E-#dsBi|3c@myK5%S98~{*Vi64UN%1#0YB{(lzT`k=AaM&@Js=K7`cYP zHvhGz{z(D)Wd|T(&_Ci2^nZeXt-F6bK&~Nxa%A#jX{6W&0Pm2IB@6)Y>%Fn2>&6y zZUYE0!G5S)U=RaiiVDXiG_`WB&a1o8g*bW z3Mv>4?S~Np3PC;xPzlk9Zt}~c6YJVwFnE#(JWS5RWK^hZC)FE;F$vmwg=1lpkyB7o zF|)9;v2zFsi-?MeODNt_QdUt_Q`a{zG%`jm&+P2)-g9tta`yJ|_45x1d=L>C6&>^F zacs)dXQ^rF&oeUf3tqi`Q&?2|wyL_OwyyqtLt{s0S9ecu-^c#3@rlW)>CazgmRDBS z);Bi4ZheCv9DYAKK0%zG{g4X;fPabgN3#DU7a>wE6jW3&D#j1FKq&q{1SdpAyUCAE zB(IBM<4MdQ@DP(kAvv$I9g9&=4@PS1HHuBfB(%&7{~_8>$^L7Ch5z4@?2lmolxr4H z1WlpwSj5PHL!GwS;aJhdqG~T*7#e2RC?MT|0m%IwGy#`vyFF1Bd zKpN+wTGv3XN$Y;pHGnN!?X!P+6nBNNf|b?IDFJ$a zq5ZqSzg7CP=l$Jj{vH;8ziEEHc>jKv{+V(9o-O{vRTqyYa8n>{8|ln`griC-1LW*M>Zi&#a}w1cJ!H8 z6A}_H!tva#FpN^ORX_Kqv5?AOc+HdO-sxHXSt-(?tt+lkVdIH{@y)|oHQRImzp>rl zP8#txo_dnuPRiUCTe(Vmi{MzZ`agri3@?8Dv)C9*_;B*_KH=9E$`5$Q zWu6(W_8y74m$qkwz^^S3pv!6>ef8AxfVpU(7jJ69<(x)`z(+ol8P;7H+3PE^@=&ItXPWH9E z{(F;(V-2g*mE4oQf}OP1!*1FAWFxlx9WLYFq`JSKmyqcCk#Sz8OkV{QULLUher_1c z)`UZP_0i=TkkY?8B?$?eKSBLXMdK z&UoOoQVypm&nQUH*d}sa`fBTeX-kCRiYz-J zF6KQ8Ozts@I6i7ElyjwZyHdJxgZ?pUOb~Wf))tPOT6n7_oF~RJUvb$&VPCf)zKi>H zl;3GQ|3_AeA+OAehrud&!8c(WasF;20h#fFQtW+?&8%lb!;C**6KK)E*H2og=cPrZ zG7O*Ut)+`(%?t%n$q2w%dw63izg{slO)gL5O>U~P^V^d?R?weD!G{E(j zPaZYP2CZn?N^59MxhXYIZBrFJenCc!siR7<=iE4V*QV*fVY zKC>1+t=TdmyG(LUf+za)ZP$K{gL7o4cI{o|QkLRkWtkI!2RvC+28C=5Guj&FR?V%e zk`<5q7Z@c%(~CNn%ZS(_F6)%zo2%&Fk{*UhcYDj?1qZE&ONA5%;G0h_O|OQ7q(nV- zM}mt#qin{m1Z$HJ1nW#}B66Jq17Fp6UIPxL=9##-duRr$(h{+iutP0-#5hk$VL-@t zO@>u%b~mYa4?ChyA6W``FO=CV6$ySf*Z8pY=tt!8N&w;zr8UJ;CXGj`nh1p8z1k5XOi>x z#V^~ME#p$`MV(Nv+O0~FrK{&(Nzc~!dP#7kIdCFkTqN<<59)a%Y93Z zw?>-Ng{Y-vF3hz@EZNfRTCVepB|Dz77%eQ{a>zbad>_!pv2yO?EyvFWX~qbMLio{3 zU0TFi;YmGKF&ifejiaGMQeh#lDn{NXW~psicAiUbP0u()y5DS#dYN2NPQi&wLrD=~ zAj7)#AZoLFBqqJ?lhhM)P4?8bJ~{QHLM|oNxT0sIyWDQ)vQLx~v`g9>oP1w5_n*kH z!1ud&skiB2jA(2(>*cE!CLC`#=%6xDO0-aRL^*&qQN|wPSLGvw3;*YDJCw z9rJEi9-f2&_O8+;xiu3NGWfxAaPFzv!V%8`M6``BUKds~V~RiHjStSMWY?!sUG*$2->~K1% zPI}yF5E?pg={HSunVBwkP<4qaU)&lDywWBz9lf?ZAF!N z-jk>V2t+W&13)$6sa%CpUPva6bUjFl=W5olA7ijEV^sVAK5{&^ei1ZH&D%NTqK6P( zmOL8A+wF-F%@Tf^Xdj2+(Y|k{U;ogzu4-HgXGvSqAoCVc$QuxiMx!^$q!4lhEjf_dnZPNhq$ukARmlC^ zE8D-xeRV0WALn_Ac<4?aR?1E`_wXQ50(-ONW`M%Rw65sco!hYakagYBnA*T6#Tabz z-;Gs*mziEOPc*H%JT_PTZ!qCRnRC*mzlAQ~-dTr6f{UIui+OHim>qDm^1-NqKkYavb#ANE z>Zt*8ndy}kMWQQ*_HQDES?k&7tJGw(vtQ}ICH!{`{vX3TJITTi7WW_BC{FbOlx zU(a@ZWErnWT48BlWe>m0GRo01lMX@1zS6siDnb;mNI$rEAab9B;Q z8Gxk!(NaXeEdyh$Rd-6!NTb2BqK0$hjLEvydy96{zWzRj<0*R0siT6LC9HPsvddmH zo9FD%{no2mv5R|8j>dW~FRp=Y!kD3YhbD(Fh8ku6sZd6qado>5d z7t%e-ls(*sk>3X5^^u; z;`~DKmq6}_J7YJ{(`5HIuK}}%@Ju;Ap~|GQt$)Q5?;60#ylr>RW%r|U#TY%x7>w$= zND+SM>VvMzlq1r%Y3-6u(B8bjM@LFB#CnPHOO;3}oPR3L@=yu#54{o`3(Mxhq^PgR zrE%2cadgs2QP4ZNEt2dZcS^dZu0+5 zx`i~nQ}2@PliIs$adWE8vE|1OzWcpl@Gnx7?4hXISxzmAR0;||t4q|_vdg%_Yryv! z*x$*$FmxW(TDIdnC)d8(z6KV*$Rgg#R{C56d##t3Pp^U4GBc$0M``uB5qe^Eb@lNY z=+^yD*^%a1|5Z1#l^U;slVj~Gj-(w6+4Bly2e7{eR!WSJreKfpF;DL0^fl001%(?x zZIK$Ehn}CaUIV0NHAtgVp!Kgs7W}m`lRvD&)c;T<)!njMXLOzGKM8cbM$ZV|8D)iJOAHf|G)q0D>ZjIu1mBJMNV=Py1FtQ}8tERipi!#q>TF znPz#{cHD}XOGtNO2Bop(?*JHd-r&m;V7SDCIEy2^f-(^XipDsF0 zmlKaPD(S@}3`kc++RZ^nJYBv99!(jin_*_7<>_ooxe_m*+?O=Ri^j>3t9W^VU*`1SBuZ#V zQMEy%2$wL=UYeGKlbu!`)jHx1a|hewcl_LGH%?#phrpdo|h$`QG|vygkz@ zu2%ZSVHbDFx2ZFw53e7~3u=4QRPl(upkts1rKo}Dut49@Na6lbP*+)etAO)c2yZxR zTPO>{y$F$M#i)^Wr`n{9r;rJR-CrK+cMlsK^CR#?b)+L#Ej$F6t#I2su91JW@{PMWu$IA70a(GF2?#kgP2@WO=9@OI@hUX5G z?&yC7@-q`t*aAKafj zd+)PQUj+Rj_9_jh;KgE)TlZ-~Q0D;6UBfwc?!ulYQtKR=DW{vG#qq6k6%gJMUyb0`%4SRfw{Wam;$srb7wKM;VNCU)XRlkvM4UO zct=Sja?@1oQ;hO>2>FHrWBmhl^6YroFHHP8!yOIB(xe-$h#iR|6{ZGn$~mq`?AQdTqPxs#a%V!D$KACU@M?SkSP_l$GbB?P_g z2wzxPQ9$)*8E+Kz>PLB^$|d-42`+?f?<6Hr*>jcG(mx`__ey9BERGw9(y!Sw%~^OUybp?iJ$N4zH*|C$L2L z`LR?uygDHweke<7Q6o)Xkxlx`FZSC%_W!OFdOh*4rw%K+(D5tDm^K-lDPdy|ChK#O z%}utZaG1)1nB|A<9f^ycYal#c%@@)k?Y;A^xpx3H@=pDLw_B)wL`3|H)iA7GIY=U% z)1+IuFSnMq%<|og@&%n+Nwm{L>?tA?clNkU@Di!L-=Hryxi1eS#|2E^AZ(&J|%@ClIpuF%xyBKT)Cpg-Ltit#I&MXW%6c4<)y7(J(Fl5 zLSKK>{mXqW2U8;DVW+L&5=ZXx(`oZ`6m$Lf+1|n zn3%(5MA7FbZJ7>j_YjgHvhgzgEv)Qbw3dp;uQJpQh(^`s>;RVemlY>1y1o5py-}sY zyaXmn#b%tTT zu2~coC5Hi?G%ne_$JEE-@SfKzR?+WFzY&)mEo2COQDd{KQ;uEtZ;EjSm2Yt7DG&DNJ7<8-J(ejJ- zGnFUn`A?$d#MVGBXzVCn;*VYoT#lZOZCJ^lj=83vuO7Ufu2DBh=0c1Wgg;-v8I*r(^w28&mWn!)$uuL0C%cKC?O zCm(IHdCM93<=uPZcj`q1RH$o*GW&{BDD9q5^gY=F(6^##=PdIbx)+3Ct-Ji^1_Ara zd%3F>(VoG4FlhKuQ;6T*w^gIM3Aa7_+VdxEb<65*sNw3XPr6%l3fHs~rj7k9F)u6J zxZVUXTTy-u2>4tAI9LbLOz*n}w-$x=A zb|}gP2kc~*(+;k(I^A7C>g;DJJm$NrKGwg1r);jf+{cw{A61ZG!iM#JO^nS@!fio& zLIQt>6n1!fI+$_A)w^eb7cTO8$>rR9p8ZHulgF-JL)lp4Zocuy9k)y~u9Tsl|~#@am_mxbru zo{50##As#GRxXr|sgX99kM@^8yn`iGC38P27&rHAD0MIytiEHP7_r)&18My5z6pNy z`m#8*UYoj?=Ce&5kep|5nsnpN`KR^IL^7esUKNNa=n7@K%$?TAn#hhhe`aaFmz;y10p05Ru{UXNK zM~a17uo1#}0gx5Z1Q`~mv%2Cy=xR(|P?k$muE^NjEW07uXcU?}M)u(6ukUFLL`$e- z1!P?-@G25o-JlUySPj!|!IDnpPP$)){GE88O7uz(%}6L;l5XhP>dCJ+(xtY2%?yn@ zr@s1(l&fPAJ)lYF9gM7OIp};>GoyQO#=F->y1$>;Z90b~%8WtZ(-ehfZS(8mOvSMW z90U25p1B5=Yo`jAw`Z>b(3=G2H^tEV3ud!wjk}*t!`3ZK3+{?BiuExvd?;CV-@$@$ zF27h{`Ox61-96pHQJJgR^nR+`$jNAI%{7(L(JszK44@SEOnO$tM7?1|S}RK~!@Tw2 zMgnX=3$Y9v?`#pPop6{!O=+V4TKJkdBiD zcDV0w21$$SGyGzOQIj<0+`dP%6v*ok8BbCq2)wk%L~^T6!nm%41E8zT#%CGR7oxA; zB)EgpAeA2)6=3MeQn`E|>lD<8*oI~9c&G?56CZfDQAJ0ciy%gh(-HEb@xC9xZJE+9 z@Z5zXxEkXH2E1`S<8)?qG#RvPc|<&8Y(>IbM12btVM#qu^icW>epqho1))rB@$9C_ zG_Re*)KZ$Nnyf?8UJ2vHzzHP{xT!3(KOp89g5X~nAq~C-cX+fu$yNA++f5Q1}w zY)~?!iM#qKR53k*<)O{DuaR=7RtQxgWp=qB54S+s)PZZq32Wry&UH#hray=mcx3x9 zg1M@|{F$n}j;fLoDf0e0dYnAb zv5(Wwg=`hxFjO?yD_jZVDUgDlLYIR12 zdbKKm`&z`t7+a*ATmT1xwiv)La;$h+6)Xv>UfL@QULb?_JoGrrN%iz_FuWJNbt}>u zw>Zk)h|%M;$4`WFV?Xj5sLqYPx*6IfBaZm8LTjh*>e6BCpYU2Pouaz_T*~i2_4X~A z=GjO0P-!^q3S}!6F|GldipWLG3pMDfO|c6d2V8i!w9cv6MO4jGxJFRf7Rq?v5lkZK z&u2@{!S^a+48eLVk8oMy4ZC8v%<)OkFwW}CVF^lac%S3q_d(I;QPO&&TxC3FkErt8 zrudF`4Et$}Xq=S%%09tW7uEhF-q#;m8`AqTooK52xsNb!8u>|8lgg)fsja0rYd>5> zYZgCuakiptVGH(!OYhb-6j%SU#QBt3olJ~U}2w5t`n z^c6y%$*jCayZw|-lT~mw^x{KXgDmnSjWTu55%?t3YM|HBB@PjXoJ{u-{Hu$U+m?dk zN7>L0y?~62S{F^a12I=#+!&v%)f9IA4SwWRHUTwkwoQ4v#nyY-{NH&aGFieG3Cdd7 zSh6-|d&_64Bu1!I{gj8FYEkB)Dr+5h6i7`MTqsX*HA}-zy3Vd%7Y!B6IAvPwPE6N* zWy+iqv-S@xP{4GEN9lhrPLFHcb;Sx#@4^3)Vj;UUy>kd-S?#vO9_3mQnI2sba8I)x z;S28R&JE-m%2AWLk(&G_*!S|y6`M2JHGmh&cs1;A<`8*dvZ^+@mw9>IUbq;#vY}Wf zEM}6Ogo~d?>m2wX)%zf3#WMZn*G1f#{X__!l;mohg>s0sn(Jz|*A?5sZk)zJDZik| zZl9{JmseRDJ3}HxXx|!FBG&QH<^&RF9%i*9$Q%3w%b#p0{h9*QFp56MH zEIqtCxv&^H1oa9pZ2(ROgk%XG9Yfpy5hEtPa>)NBpFbKir{uc@Ort^F^`~OP-W#E8 z@L)(a`7U&2k!X9oF+g#}D$33GnX(rnRYgp`6H6lB9Lgtj??jD0v=HfY+0emj;0Dxz zme-&Lrf+HWP2&52OhK`;KGr@@wkYYsx1v0QCIXa1lN2@ag5yV*8CSGd;}YmUHv)(j z%~%rr+{!k`Unf%beCPj2=w$mW&BXe#PrTAM7T{g-paZ8f!wL^R$K7_34={tihvio7a%ph z@r_I-61lp7&1#NuCv{UNa4c#xG{#au>!{Ek?#;3=Y1ah5(GNGk&x(k>7)Knr!(-Z7 z$y-Q+6)#^`eBUPvKp*IWwZAcQ%&1Bk(9iWYo15$9x<8G=9A7t?s%-pexurGfSOH<` ziEQ{D%n6waJFTtz6{>PrBA(xC%*o5a!3!Fp<=i zA)IbY5qM$j)lGct{^~6<%bOEd(+*A?1iDlFRCk}fUCnOBMcxU2s(Sy2A@=MI(%)6Z z8hh3LzN=osrU5zCOND+Rw{kt_MEZZiqehk=x7rCyi1fs6Y~G@Rq)s#>Q}MFWUJXMi zb~Des*3g{yzlDAOC!Z#XPlA&NnlylbdWg!Ealf3zkA+>fzrO13W&954cl}WEck0y>h)c3HowbmJOc#QP= z3_>rIoSc!KF}pJ(CCRAAp_4_>5u|{N{MP`SB&0+K=_{;9X0bzdH4YB8_a^DPW-%9+ zt(S+8Yk*K?rWXJ79D2SXd&Q8C+%Nq*``@bjyW9LdWd8s6x*&c1dc>Exlk2diq~--d z!HdLDL-Z<6edHN|q_k_m;~L;s>gbhMBGCh1PyX9I!3jESRE8-{(y<+GsofWKbT+g8*3q_8Y#M>q8l&Ex=LZFL@!lUPN;tzGG(f@lD^_<+Hn= z@L5O`X`XULq79F~$9=ngLLg_@hM@?BqaWU?Ngr%5b%u-v5zFc8w4CBRA{dEPOqtesEdixH8qcvGOS!OO*a)~sgg6H8xz z%SSBZR9J9PTkj_F2T?o(iL4nMPNtm=l3Ut*w&3@Y)$8<;9Igjw zAAi+7CbJaBNBDW_Y8UqgS*(hRMPHMB`%?~4wzcHiXYbc8PtLgl7-l=^h!a=lScWv$ zTF|9jXFW*mI6k8mE8#Y*x%J-0MpP`T?3D37I+}$NAnM3m>ATUnU+jzCm8zf7D3;OX zOq1*wg#N|S5z}iq6d zP(e?)NY8@@PpvqDIZ#bMC!NJAT?4Oe!(`ilh8FGDezimZ^OaTz?*O8Vh{=p{(nve?0^(4J-Djo~;|y52Ir=^gwaf zrr(ar(P0$LJ3YbNIWlF4x1@daR3M2D-Vy!HOGR7P@+O@8*clN7rxw_zaujs%NY?14 zw@G-_NZ9IzK1%Pw-$r`u*1GJD7w~qdW}n!}sIj($#Ex`=UBE3NB5!6Xz65AHLPZfT z`vg$X7J&~auYsjpOVq20INgQ`;g6b?EX?MgyPW6czZ`qu)7!th9|r0V<6O9)JeLS? z=->}b{(=+tuqNXkoljZKx`sW68)^!s?hw-5rvwBa7CQ2M4(^X^_%c(b%3`hrkuKo^+IXoLas9gT4~p%;(suI-e;&_CY$WZ2oy<@y|iH-v{FUhfPx)lCoHFmF#??1QmK> zUhs{R=OgW^ku^9!B1#Im8x?yd&?0bIbAi8-1VKQT-Qt!_YHtcz5eTvG@d_2)uBY;? zdOW2u7ny8lB8q-UtIpoDCss(?HSBJ3StP?5K-%oP)EY}iIPT+4@im8FqS-Ki#Ll1j zDm{cY(!Eh!-h@Tj6zE&^+CoM+)LTe_Vfpf?G-eLQ1fg-ozbPHh6$n#7qQieHH+kHprCwT z!l3`<*NTIdu^TeP$Q|X@$CjW^SKtfHsb<2!VygfiR~< z$R2u-YvEq1wO{tY$ikKkAN;riex=FTF8bhJVc;qRW#Qv1vmTy3`gqZ7rl&Yo{I{}@ zKCJLtZiAGGScUyj-rq;M7i-;j;7ihOhn+b*@b0v%;;m_vsf`7B_7qC>lHg^XWsmK2 zS^8nY%##6)Ci8O>_tJYBaW0Xj3~3@a5)%bVO9e3L*9hB5Pxl>QifJ zr&B-a)AXIleBP|DT9JcJlrfJ(Sy@avpyKJ#;!F+lm~|~}Ok3(`nl`?8-FSs>{u)k* z+tq?gleu)O&uQ{4=@XF~{3DGQqEj`FEksTdSC5y@M=-bEeHMZ{a@%t6bL;U9t>LIv z$B3QkDw`ztXmK85Tv1jWr@-CYatVVCbGuko;W8gewO6}ggN<~%%G`Qm3dc{F$JMJD z9~Bs?UgSQj8}!mN(EG+R()tRq?EN}86b|idC04U}gV64{BCnawIkOlYpdOG)9@yv` z3k=oEw+F&D8@BGm#LBuRMJ!Mw9xlnE$Boo{4w#u**+0Kk3g>>M9yY@t^>UG6Uc?ZS z9uTD|MsJZo48XEh@q4Y}jAcoJrI*jQDax8}F7MiW=QisJxGx+xVE*QPx=NE13f|V! z=ZRkD8USDCVhcSYt1EN5YvjxH>U?3ClyW0a){PIYZ@1u1-gO19hlS7f+pB^Si<6!!dJ&S`PYDY=2trXUz6tExPB6YzF z<0)ItX1p`C+m*x7oYp_1x*66@+?FDgBqg#E7rSLBny22t=HFB}$W*w6tKA=45ox$3b~BTHw&O)S5&saqXi{q+NLKgf)-X{BjiOKfmf3Ep8vooMgr9;Yv*0aqcjAmDO=tVTZgeh7K~hy&_k=Fx;duXzPN>!L5Iyov z&*nb$#r=t9;niH?0Pro?ce6Knc>zWcqK?CF{GSy)#x1m+xpUNXUeO0i#+R;nhZj)F zRPe@Mh!)CZ*sahS^{P(_8z@JQxx9}|kxXhLV7~p{4Wv8b><50E|DKz=yRqyf?!q+B zxzYDSbo%o0y{YLED9PBJvMv_W+f)7EnMaR0P9skvHtvAT8lj0&@>&S{mxrK9G9bO95+LZA72PGvb;)@KUS`r5=vK($IX zj-hO@k;nNtP`q!)4{D6c?J~-E7_;p>C-3wOlAO~yr=_m8qPkRbw>(~QB zTTYGddp*NP_?YJ{4(T_5_2tVOZ(|QfsDq6WP}p8jkQIgVmW}jSdZH|k>7AEib4JWw z=mDrpG&DY?`1rx02$hv=+gbThDYwHyrLZM{Cv@q(jA zI$6;d{XA3VO*6f?hqr~CgV+sY(_Km~XLGuhNQ_hbNmoqiguFTLhDnpRE^8kwUOlM4 zlDs03zsi?AQ8_4fMB(MW1~yBetF6l_$X&I=$yWFxtl04-W;4=7k%Qc9Nkrb}p)*Hb z??D>7z0i&pt!XH77J9@BXY7ICUr`1_CZ&8K;pMLP_iJ%njWsG`cFB!A79LIVy500L z5})7);!i5_%a8V}5YTh)t{W{h;WLAl&Ul_o_NGOe2b_IaNhblW!&p}7jEtGU*2P~+ zWpE=BAmaisUigo^4yfp8jnBD(C~<&7m*(}pEq|}quKDs!q-Si)`nh}f(yfowr-I$* zeO9Ov@J@8_%yhT;c`ydsw{p0-JT1cgMMv8O+CA> zbKah9ZC%Ecd0Ld}d<_rsr#9ia!7(ew4_lYXN1KtZc99w7j0$<&K`}R{s(^f#KF|rk9#S*{lDva{BJkc{G0hsma|!VNj9#v zX7SOLKtO%lsG~qYkO8>;%&@0edct4lO%!{^9@ijYO^~?`^W^gbTT6gaViCX95sFpO zor@LhgeVNnp{{|;c7Fce;Kq}h0@)qnHF#zzN}<`~ri3sW^!C2K z5ylV3e5Ij+UsEoSw+EvvVCF0SNkX&sur&yQl-gyUf6lLsyRPcM(vxJ?~HN6qtx}==|smThqL?pw`xUn^A0K=xlxmtEix#WQzZcI>RGbVWPx|8e$V6QCJqoB-Oo&9G zy+3^51&x>BG|6dr@2W!_FV7Kv%-L052&cBU_qpU(q5;Mi)lEZQgM&dSTAEgfUDnQh zoPhD}v!m{Xh!t8F!_n_HzRR+tPR8gPr`9zFQ9gJIL`-{n$pac2w}3D>O9VTp8gG!|%EV04Xc20B^zW!N)49iTfp+>Ecy#SS(dZ{ht$WtXkkxj>J2y7Zj1zu9(W zKBWWQevY(G6~xvVtw*VaDSVFGU;df~-DoMIk0D#(iOEGXViPuesDVg@3${Z*J^?k7 zm(b;4ugf^K1g@zK6Jh1}UDfc4%&GMi{rD$xrP+)+2-4EfQE2s1Z@W9{T!6!lzVp=d zSWA*jN|AE1hh5F{Arh!#jsQ)XKFfj{G&gqwpU{!YA-0BArPHimD2?hBk-1i7A6z`q zkYm<9zNEk!)~^Weqo00^;5_z#jYI|;zYZ8MfU`adwkkqAV9y)VYAV918J}8~YA_>J zT^(qK-V79N8B_iy9%Z-5J7cFb8mtT7REI6pmXyc3`%p}cttbu3x`+~=s`2-IP7L4? zj7a3qJXEcvrS1roLIlC$6!_*xUT-&s3|E9u;NF|CwAUwbpB{LqriF=CrP^2VY2*_i<+AUUp82 zL1aVd^^ime<;rE6&;P;Rdxtf(z3ZZ(HwEdPpfr&ty%P~>B2tvz6p#*)9v~0}=^X?V zgeXNoq)C%8zbH+JmTCQBkq9 zP4lQB)5Ym0u4z5iRkY(hobXhKQ_T(!#T#l#$rq1*gS-*t&fi^5EIr7l`<7_*0#Y|z z_E6@jg{kq)DXkW-G94zZ~FMOLviE{Zo(QXSc;YIZd1I-U&qWuLK<3V z-d|(^6nolG1AV;)@w0?0TH=rNLh^grr%rXNK3wwTCG`DMxfk?-azngl)g4Dpb=Sg} zmGgB#sTv^lHRsgPsUzxsv|9TLG%gKha1w@rb}lov%+0&-D4di1oceg?c(}E`$Dg!B znp>E;P%H2_+|1rYu90^oxtTzLx5u$NuYiehR$Vv2G~AXOXs!A&4k}C3gvlojvGt-c z6VLRkmxhn7I1b~}W-CAZC|rM91uSIw{%|fJ{~*7U;(I^;8GD4*Wwkhv(`7c9v+G2! zWFAfYcuW3hEW+Tw^U)vMRR5>EzACXK^;Fm0L%JF4UIr6F8R~w#y_EdVZ~x`+U!JM3 z{61$MWu@~%{S4i9WAQ&JuKkl*M2T}_)S|}VchVIQ75^DXQC8M4EBF4gYW^#NashZc zn+Q?s`5W|E{sNcv8x&h`uC;ugj#!RG;IdmaIf%6X^gra!7nV&Ly=D5}_p1hbB1Qwh zVpJ`KndnMyfXa?vkdv7^@=zAsVy(OuSMZ*}Z~k`hD*zles%cx;@qbE({qL0X57zJp zDa4oi%Y??E-=KHeW*-uehm63Iu|gX0!)W5xz*H|>6Z;z!9mWcic!)iX=g~RN<&jNW z&DY?IbT;ps;zGWT45&M-+-v>~60F3M5L$VEgT_l-^o*qJq7n6wj*(c~>s1)9QPVjGkH ze8ueY;ahJ8C@8`40^I<<;SyYhIqgiQf<^7Gf2^ISOE}aGQbUz;>2clf{X*a_>De5w zUAyBeeflX5?}kgqiWtwdNrsK73E(6u5NnFH61yZ~f1ULfkVdy*r0E;lyC zcWvIPnamtUq4b_L+#4lcEfnoRN*?M8y>SK^u25^wf-#@rm)aSBf?pVT-+)Ej9Z#xx z`d;wRbWp1FG4D#1xSFA~jnK0;UI0*xh}?s_uIOv}+prvesam+9%jV1YYI!}wD2iQY zEdU*dKgDK6)*?9w^9cGDiHcjNa%Ri4cU!!_V5ONF>-~ueZgg)LjUG)D!JSuk_LRGR zgQ|MliZJL0=i;AHbXVrS<2v8ArRYK*3ACKwd~Jfe}~)C^p&XT(2&TDesfL{I+&TJDy{ra9QWPp+`-1 z46cYuz6_H?DOUJHnFgNLS9N@tzg8DNl!r1Nd}p7$ZmeZqQ$p?A|I#XK5=m15*MJuE zHNBdUkR>Uy+9`j0bIhrI!QqhMh?7c+_<40z6|b!G*I0x{iRq6uD;(9_Bx8rG>@9KX zWNT4g8xvxmnfUL>QGNUjbwkw)%{8oeOdr0iV z$2n^nr`a!3x<_;Che=1+7>1&N^o3NZpcJn5=pgiXjs=c)QZ>X6WQ~jzZI1 z>Ed}rum{ia8r(hCvxTCI<*wGwi%D@dW?Q98rr7JvvI>NA@g|bcXP?cW!lpP7*1RC4AHdL~`YdT=?DY618E|)^3 zGxL@dEqY7nd3rBoy+?2|86;VvXDmv%facQebS^md9ila z;PF%Izn(>=ST6gf-d?g>j%cREK}wd4F*Nv*w_=>BjNE+G9Cg`}Cg_o=o56N6&Cx63ct@kxP~zDeRGzRr!**Db7X z+Nq)BYoW``S*@I}8-s<_fYQ9xvx#Ac+g%EsysU6fL!GrGcdW*V+eBq8GUhjkO2q(R zThafp^gn57oPHGD!y26Q?qgvI2cm9P=3a_E>)`FQ4{Xlf!i_s&!%NJVz^LD#Cs2CA z7(u*srqVz0x?2FtM%Tq^=G@Y1=0Z`uMxn=^u+z&jrs36ZajLI;!KFM`aOEFi1=gc} zA??KOP5i;bi5F!8Q+tg%qU%R0?bY|^KQ*v%o}iU=6m}ZQaG_YCP6*Snu!`Oax4yrp zoJg#g<&z|xj%-n=_1%&MN8f~C4!kpt^~vfvz+e;Uv?UvR7kv-2oQzj3(JXg8L&OSB z^Xv`FyzB-{1;*_IfiBBRiB84sn3SGyze!oXtgPf(H82D8MMiSC{~qyt_uTuKLpKh&)JhxoypZ>IZ5wi~=oE?7 z-V|*lUA^UD+OYSybX!S#FBt>y@a|%Ble^Df;Z*}zEhg>b+})oJ`A|3A`}X3CntFQY z<0RMXZfc%%mB$8i2HM!n^tU@pAk6#L%I|Jxs2>ds0_nFssO|)OZnU_0hnnljnLqm% z&;_5}%ika-yxvDQES$8l6_@xJ%Yi1a$i8HB%f77vg*L!1iOq^Fru#&ZuB}C}Gaqdf zW9D_S23^QGPE=+Yq9hmE*(TAhq@Hy6FlwrIGGx)=L2XSLnW)w+llgGrLkh|B25b2k z0zXcu%T@?;5CPXHEzhe#^=GR}E%ftoW;IYd5~bd)7~MJjwy9#a1_tN|%DqY>(Jij1 zA*ep~%~Kp0;1FVd5K;R^01SC&7h(g6mRw@?pzV zzKtz|chq@WMcubZ?X^o>-&_dgH8ZQkAAA8^Y35ht)wX{&TP-4ta5F_qxMHJt^C>}` zn9s!WZJUB7$QIixnYaXaZ#Tn3$2X{?TbmF=H3q!?C_xghG6vL?*Ai+%e>&4FN~BIR z_l|Lk5G-*~%Yur{p-(O2#}qCeQ`R$wh@jYwM}ZmKbPImyQN$MHZJ>-Uf2y&*7r&u= z<*iq0kQceHHER!)SsPV(84sO?%YJM*VO^$|L4LBC|8|$H@Z5!tktDo0yzr?O-QBQ3 z#gXM;5$t&{57V?72#&x-0OLJeK0C$JYISJ8d|l69Y+>B(#%{Zd@mkG&IhF@s7kVTp zn2#a>sx_r2;8rP}17aU>b({wrZiH(SPFS&zSr5LlBZrCdT$b0&%ml9SW!>)C-_gCL zIP&9m9G(b^#PjpWWBYq68DIl8cq~UlmqGwWhD!XosjNCxl=KGWl``lhxW{yu zK)n38C*HeUuasMz#hgXNpymP@{DqCXx=@pxYFI7-Rs1qg)v6OxOYs8k`*9IwKXZ55 zFEKzwaMJVm!m@K$T*8xS@5u8-uTdY0Y5irm*6yX$A9+OBwn!{r=b2q1US@wTtc>+l z{EgjAIa6VN;}Ri{)TU1gU%Q3bRowyZB1anB9+$1A=WUt(F z>8-iz`ETETP0xeR3)N%dM|e9GM=po5Yo7SFsd?I}!}PLEoj0euM6` zf?$A5U!8MjGc-~O(kEQU`TQL&eEs7wF0#39Z>sCwsFbFc)UE~}xOkF8b$O5dU1g$* z0cPhOW~sUamfXz)=HU&|S&jGa@v8nbe}^=3aP14L>pf>)hNpCE6uakwt(c-PT6hqy z2FlZb85x=MD_8t9EF%&tUL?4M28o~K3_j}j{$7|MrA0A;WZjYoopDU)%6>eo@cMOHR-4_Ls4g9pQ#UDm_Xs76k@JM#Z^jU%3hdqDd>lypee-npgxXuXL3zIqVvy))t%{s>Uro zciOAQRGYW3Sl8R*E^HMYAVr!S%u4b+>vA|p_Os8)3wPJyP?TOJB}~r*E1Q`dgrYkB z&f!>9WgxY^Qi-{f`~8eIlt{%4t=)AYxYHGOwONei3LJTh!XavVn-a3{pVGvPp3e3ofa5TC&W)n+WRDt{OWs6ZZsbspMetetV z3by8#A%;@B{VRd~^{0U`tTr%tlrDjBIe{Nx;lDvTQz}AbKTJ(IOy0kri`=~msEi2y z63fWQkyanqWbdS?A7A|ME`tJbtnYrz755=|;8$zn=9asZ`Xih^HvyU)VWCb#6{s{; zUA1XyBt&TA`%`&l3exf*WOQZi0S%#>1 z;#0rGH<}N0HyJ3yG}Y97k)vYXz-H7*{K|zvcedEEuZ^3Ua61c&>B)9E-%YJd3v=n~ z^coz}UuOAfL87-Hd5G%i)WB7PBwOCm7K!p7k~q^HEvr~RF;?rQnQP(ejxhq`Loy)` z8=Wqx4C1>Jh#Iz`tn~7G&X{e^qw^RQ`N{z&D)gSdY_{(6owan_LxpYQVAh`ob6Kka zsg;Ya@_ME9uZx!x#Sg5Ly3EGPE8{5#8J38uEo{Oi*G-bxBZJ!_(2J~gSZEBM{*tei z+Y+0*gE!N5*+yp|`1R!;0gHw^JxY$N5e38r;bFSC$Z{ATs;@opTorbEQ*X3|zR0$a zAl7CUKBri05jo&mA4eUPXK~BDRGU6rT~O;#D~ z=YSLC!Z#;sjWpE(R?GimiVLFug|4CaLzU-0hGw4q2D#qy?ufIypjL8W5B^j)M4^F| z%kX{&OTS+~RXbnB;%?Pw3=;rF5usD0sz^z`Y!j7R3KN<3^$qBKYMAhwhwX{(@##E{ zVxsS7@_tql69+N@a`asy>Znj`V}(mYX_sJkaC*8NJntkwf39M=Nn=s$!6?$j=%5pj z{3daM7RI~?kPWOPI!8bjTO1kD5i;?16n1WMf*o#_Vjo$|s=CxxlFUcB_&}Za&G=I7 z6milgs#AUk>^Qq#FufYDE~*(qcK5Dq| z8tTEO_sz}79D=%jB}BM%Wp;F+`W|VoLTEHmy6`6~iH_O$(a=nN zO;0pyvKJ*(xR_>x?eOiMSi&>!Ui(@2O`QAtdAF0`z!%3kR){MO=XVu8ij7GG?DOqW zmyk9td|%Eho}BU3WF;Aq0Wxn=<01KWICl+9K(a&OcDLtxq1+XIR*kCcn>mgoRgc^s z!zot$KaWApx|^OhVkT|1GI(pF!;tjq;4HU!U+P_=fc{!fkY7bboHzx`@?`>dU1sI` zhRE6iR}|IUrg8MHVNl@2=Y{cwiov^UhZdduN7*&262vf}4ml~;Hi}VE6^OeuG);uY zr95@vlGkI73Utz@U{lleMjl_3L3E0Ho(iV2M_GBgYDz`+q!CQjD!vOfr1D(je3_t= z`XzCA+Mr2;@!Sc5zB(J>~a*785!rDnyUZ zRbS}{w)o4-3!Fa5Mc+x(8f6aj7waz=)A@r}L;Vj6_4r~pCAej1t?@!nQY!Bg@weej zj`VqTzUL3_0j$MF!~w8kPJD15b*~o-4Tp217e&tL`pyLMT8sB-d0U^0^U*3?B8iA% zUrBOR5ne%%<5=%1fy0kQ#Apnh#78Zx`k|bXY@!2fM6Z$x*RtmclM|55eegN19j$%x z{`GGV1Yp7~1Kt*g>En0(FzBi_5U-+eJK7;IV3Jbvw*#-f$TAJMX*JmS`H`*6$PaT}$I^p8SlLbeotAqhaT>T&Lyjd-~FZFu>7n zk0;0=++2}Ir*s#1J8ZEN2bbw{O#G^LB-G<4w zpo}7v5-x-D$KV&_pTi~I!-6WXo7`%{oDPDDM1yyWp6$|+)JgZ+(X_GS_7Ax=`Uv#n zDjNP!a0XLTVr1(=ERUXt>1(#guR8fS zRy*HWGS^F+>&aoY^z5y5PZ=B4P0E5nYx!@AE2iOiMO<*UQ+d8nvrO5>gnO^524;lGYfU+f+v|ub>Hpe1$fq(GJ*vCV+8ty}|nR5kMqvtEunxlZG#KEj3Xn%KF5z_5N$0Dkw-m zkGyRt{;^*`>SRWKoktT061Xv--JD|xjBqh*RFUULSWOp)R!ykmhm?^Z2hu~W_)`zU`R%*q8?y{6tjyno+ecp|;m^S`Rh zK0CZ1&guiqYoxO&E8;jLm*l>$Km}4Dvf1+1TBaD2fZSDqSbjNsFm?Hl}%m|x3bJE@Lr$H6V|IlFIxol_rMK_?X%!+rJlu=xC z@5p1DhI;+%tBxA6YpPDaZ)3l79X+|ykOIyWDSxj?-S3mIcX?Q3O`r2bYWfKl9DNXW z6|Rl!r&X#9P?V7dM)C!I&TW)P-yc->3hV;0^}5n=u7@{ zPZf!%?%P39wXxuG?BaXF_%KnaAKVT1W<&fPNM}f#7cS86pjo&ebe*3O&a4ZAOR{6@ zE|`7auG(0eZe#`PMbC$`-JXa%x)kz~yQn&aR-v5k&$s_m{T4ZKuoJj|Sp?5rjBvnd zYuAkK%Gb6#S2rZhfh7q?9U7@CzL;_P&J&CeC`{Yj@ zGiIyP6IPy*1wj7Xa(}XETJPQk#})ilU4QKN`KjRzg$6>!iWc-_7)AI~o1(O}*lu)S z)VZz%Pa*!I=JE%{eeEz6YCMQ8KxUhg6@Tr_4$sHCc`mYR7#bmHAWu-O53<`)Lo}bZ zd9_T^!zFOeRE;-r+B(U5nIc6a^<4`=_0;c7r!YtvqK8EEzd#^|=QhrNInE|(SfU}t zcnF3ky#xq`nMEDXi(K3oHMdyMa0XmTi*TKW$SD%-E28&%uLi03KBVhvkGj12OND=l zZfrPO7LWny*g64{NrgoG28m^J=UjRddMJHhgZRq_{xPs^YcR>HW-)c~$yZ3ar*XE!1pihc}oTYj6HQz(C>bMri3lRA*DHFYF-_D^m9ALcXBZjE@% zL6<>f7=01}Oap^xkMm7AxOh#EcTsBg7<6l2zr#7YnW4xtRnJU4i;`Bk?8!nyTIgQ}}X8yj490uakpRqhJbFK>^LyeQ%_W%-fS97ul>`H9rCH>PHZ zVLa-n5qg+*Z~aVpvD?y>wvbl>R2x*D`}Q{%t(5ldG9CFYPuZAMc}Fb#$x{5MIcCEQ z2?`_KX7ycO#w%bJhlU1fKMjZ{J00rsq*hG!nKbf`v(oVZtB&~1X zNd7OnGOL8qL&X36z2k3w{x8e}u-uw70splPu+1;|Yg@+drE;iLY8FYx)j31 z(zR)ExO5pVdNrW#El#@6cYc1$a$>~YPq%erYJZP6AbU>nIa|O2D_;Q7kTTgw#r4W7 zIB)Dox0+IpMb%_c?WjMue2-=Qb5B>jIe#KFez8wzv<5`+<6<8ChFbJsJd8WSH%t7t%!A|L5mKciILA`*P} zVJrB~?NHN0&g-!qL51d%$g(RUj0DDZQp9L?F=E9Ug0~4GuOA7;iac;fE``46BvmAk z-s~49nGjt=c8?chH{<<~(Rnn}c;8Bfrm#q7*|pBFBhedH_M2sroDT!}sX;%Jarg1` zzd;(bxxYcrf%P-i578C4l6=PIcYvpf{tb$+B*M94A9T0V6S!P|gBTuivkdRH_;$m0 zm@>2I?|H6;+xL+9XWU&u3vu~~e42sl0c+&TR@T-#!I7l@Ox(kzU?U*n11kn=s$ zVc$w44`MQ>1L6TM4m!cdH@B8?taQIYtlD^?FiHT(6b&Q`>u5yanlP25coU37GMvN} zE2KTS8gb`yq+pQvlQ?P57V_Dr8lU=BZhp%|UuzB(dN}TXPWe-x`t0)EZyvjOb0*n> z{S>q>O8cwxI|7$6xi~&T6u0tk5Haw}P(S|TldX|2Lq*mAp+&;9EfKbTj|v5THs^ai z=We`b(FVRhBoNxTbUR}DYGP}kN6IlxW(qiD>#QQHtjBr0@@i`g7i*Mes`^>!waS^{ zIX>0oo^r|Z_P`Ug3ZnyVRD=I5*jkDh0=7Ka3+Y+n!j!+7gd2RAb2&*^C7ns{hqC*Q zY3O{D3ZcH`Ic6@ubOY1(8gS`L-B|)sZ2BfN_#5c^((c5u=d!;dH!-H-dqyb~cXYsm z_s2N{?qD`F(LdLoCRfO;rT$Z0_PEOv3)_t+FO=k8eHiN-dd=mCC)znS6P8HA z(@{PfLUAie`meDC+H|Rm)H$lxh#dtrs@bZY&@Rjz%&pYDLb}{tdVfuGYC3QF- zG&I*GK5m3LI0WQe6X=Q(nPPhhxhbPo7G1og9i*D^ReT-F|o|+4=9>o_|%5`~UUPVLN;O zbwRDsQuAx)#r6J6#s(r}eJH+Iiz+E8@8_vIR?OcJgV^JV_LDl01YXcm8-KnH!HKl} z2BDb{T~7)TBW(r1mT&U`LDI&AiRc@k75on;wg2%u%u`0UxGMkg+~4>4_d5NbSwGD0 z-oqmFrZ&>%#@2LEhC@skJ*ePB$D{NclI&<+`}(q%Ex$pjXx-C^I=TyP=Ud!1|3+f_ zXIUVUENsik^wQ-SFA15T7Ii3h6Gng$qNx?;k$GQr|?y0z<;&Vdv-)Xfp zq>VfnBxnD!%%+h>oFX-JfzCtr&LwvwlA!4N6(DYx)#f&PI8IaM7~NQ+g1n9+9&=cA z7fEXM#Cva$UrFgQW;>LEn3N`Rd70#DFe#EIteijCpaCbJA^#{vpgzuX1;U{2mtENp zrF2}CYmS&`?f7P zj(+83ZR3=Re#vFaGv$p`PspcZExl;H2!`6{g2=B4k9@zN)6}Y`R3IN*3D-Yk$&pTT ziFLn0nTP$H`S;TL!d0G&Tw}gbhQAd~N1@{J8|1!?JQj6>oF4Z72Bnn>%7y9TH9p~D zS8_?v@?8gt8=E9S2o$ys&v_9BiPW}R>wuVb1!U2JBf3bk+IVcg#-6(CjZ?wGh=Da&q z@vA+%2QLJ5lI)x3Zkc5)=H@8kX-WYz7 zD6GAA!?XY8C#lOH(Y$$eC!JIN*Qk*+ZfsudE!CR%vh5$k7bhC>=|h$FA?B5 zJX9o$;rMpD&_R)#?k>I_z}2;qSYNysz$rdC2PD0_)vjWO2Geb~M^)Q)*dFtiFL*S; zJ(ir-4xfIr3O*#)xiBWoOhJB_Pa+Q10hphFsaRcZd0uk}D#U=)rn_w0uoLR*_r614 z#(T3`P!xZfk^@o?Hm)grE&9Rtgg8d>$WcGk1s@-&^xC%4qQUX73lbjQb z^~mGKB==mf9N*3?T{ zcB^nY-Z*Q3d!wb&_RS^d>NU$JeGd?yHx$VANGE&YP>cDe3*}B7pt@=JU_WCW!FcOr4iRmiM$k7lACMYwC0_4p5!q-AAXbZPa!$ofd7{e>n3lF|MR1D6cw3Cv zB)#10wa^$lbL+i%C2PTV*UCf_qms<;z9hLs)mfRXz1xa~>SE}+RlU&K&23E>r?Sdn zxMb1Jq4|>emeLZbTxNFu0Lx|#eF^WE58K1fWBQ=C4Qk2}uFFvkVHBUSwKuol>#6Dj zplQVJ)^IgX2~Nai`mtUtyUwGqjio^Uk2}e8-K(~_E3I~YO}myANqIbzo-(cXyH2`v z>S#pbQF+?0pTx)a`rM_6m6WN$mqr*Fl!|8*{VVqsTBhlir%M6x_?!k^~BJN1~ zZA;biny$}N;zF?1oS@qdaZm5vWgVi3+wg2i~mG#DTAFb)sm-SD{)ky|gEY8ig+|kGvO$`xVvX>3I+Qb{MqJhCSdD#=& z50JzpEz=huZEWc>=*#bntbPP5YO>JQW!C&6x*Az9L?FCb>_z! za1xkFu$gU+LJ2m(P=5BiY2p|y<|uCy86YT+e~dNjMZBzxV+!V*L7mYpak|03iZ0@))Hr?!O`s38uG z0RCdD{dzlY5&Wn3wt%R5+}|JmT?vbS_XJ>Q{9OruSHeFV$-jHT-#y{)p73uz!~fWD z(7VLY)0>CVjgpL_q?lPZk#&j_X8}|!QvP{UZ|vX1a7m5^%r9C*RXD+Y@yqNkRoZmj z`fkgb6W8K`SoDoi=shwclSWC9F8_2){FOAP=m7SDgouRW$~e1~)1D!y=EbMuW(18JF}l|>;ekt6h{aVK zb^A2b?djxe9|jK5WK>^y1DQMFw}3FrZx9QN;v^6k|72x^?MEi^lGt$k$Jsg87};j4 z7|-$o){}rhYLt0yfkJ8u_@%RJCx=hI*T}@Hb~nWA88o^YZW7&>nFh0T@0lQcFO{SU z%sNEn z?luKKn)%lsse7O2-JEVC$8}p_Mk3(uA@T_-&ILAK$v;1#pA;{sw`x~9+GQ~mxs|+0 zC;8?|KPit?SUZGb-|hqjF#lt~o%i9A{+Nq$&k7*LTJm@PRxtnu%9W$?{#6fqKtP%LftAJ9ly8$*i!k?|qll5WpP<_+0b$;s%g<;C8&rOYqj464qX!>Pf z5qQSaE`l%yx?8;prNs^Cwv7JNv-rtyl7-Rr^yUDvPgeXWGV#AeyPEn%k=uQbI;u_> zX!m~t3;zKa{{P}d>)dB6Jb`CiwRzART#OHKO;H&v2w$ViplGh9RviM;T#CS5r)BM8 zvPYuuW&smOrmq5PWUn%k_SBv3Q0$j!(NcKEQCMD(O?`BjteuaiGIF#?DK#_v78;{M zZ|l7ZrgcqmL!C@_EzRz!4-{TcpdoJj0!libpx-_=(!7ei88&Xm*rtHLF(!zGc7w^c zmIO!rLY0n;rZ!vK&P6k^b5~#f@=kIbbn8#@kz8s4)av&F(K)|C<6T}nZd*d#1Izv9o7qGb7)JMTk6VynO9W|T|rM^nX1Pd1tv)(e}*qp_e7V+uV z>hfo=)Vh45Hx*KO^X9QSKS>A9{iNsOHQo-DDAAe6js2OpQC&_+_)0sQa(DdV3%)^Ne-%uo#d3?)h zj%4lj2IOtZ~>{d%G+J`!J9)WwEP=_m?^C>>9^lfy15l^U!de zekq|Zzd=UJSgz^T$GBj2)I`QUTjt9H& zF5^mKf3_8Op;FmTM@1MQNKQdK5G**>o;hX|T%fjPMnw;*Jg?IfIyBwxe7QPKaTZ<= zgD>0={`Si=kIdS0^4_`B!AY@RWg-FCeqWszvGAi~#68BFmAYjyGM$sZ8PnyLxEPeY z#a0a>rzi@h=tcrbLql+Ns8Tp1=CrRqyS{-ffTdF~vWPz6bL9I(77vi_;lNX>A5@hC z$mfU}`F}_pn3d$5ocrwv+`?JKjn2R~y1M*9M--gA5EQ%b~NrC;1oz5Rn^5 zzVgEm@nrkVoov#thVTA~y@i04&jee(_L1o|w2N{uPcXoCJSWBTPN|5UOKt}{$KnM9 zBT$LPM-OS664Nbh6;er`W*((F&L%M(_jxZhN#KM$ctkLjopQJbXau>t8uN4upYP3V z&%1;3^S1Hxu^Sq$!oNY*d|ZkoBsbKrmdey|kJSFh&NVS6L0Ep2COH^;2$>ATYs)61 zO7`imHb=&&A69YKe330){ox#c->lKz@<9v9cOE3#P}&DDcXWwx`E#ouU(uC`mfAfr zdo7Bq$QM~kjugh^7TG3~wmOqvIry%?do?-L&lfP#K)jT0E`EizSAxx81q{LsPnge? zqh0lD-ETneusblNUF#Pg8z6P!bylT?NVkc=Z{XgQd?(Bb7vmu2O;9E~n9Xr23-alv zkyXgv5vP6$9Ls?pc37?=!qO`#nKuLcCZ8=NJ@B5PlH2J4OH zpY`LcxDC1wVgQNN;#GG$4jhdkVRB5uQZsx_Q&dFgWiMCBYxau8)lt_I{+GMlJ_5W^gdTAQcqZvLXTE|3Ynwsbpnenfho^2=+J#e@|-J0M+1 zO;XK89mb$b3i4tfPbZd}VCwT@CbP!ktFyqTU-#nDKYUKeSV;2yti!)Qq!(AMn>Qs> zN@^ezO~WP{7T)MdXOXFIs{V9YY<4BUlv@(#ZBsX>FWZt*;OT5kbj)u?_D(ZI?gL+H z49i*;UZEf5MjkBi2c}AdFarj|wa-;xB)72!k$&!;fkrd3^ET|fO&R$KoF&dFYU`WI z`6PKabt~wA4W~UbbO>GiIG*LA01pO~WGq8(c)EQ%n>W-eqw*BXm~#+$6J*6A%<1hF z6p%R9Z2lm*c9F=JO`m}9#bC#t&j+KvYy~c7+2Dq;C zJ03k$0c07rmuZhBIPs6Fre!4(z)ZA}-j9pu=!47*`x!~vu9w&E`g|LD;Zer$P-1w# z+_GmolpPT2D7`?2iR1DN$l=P^qW6{M*P7!WrQbdjbr83g)(W>NNMFxfVH{<&Xyd%7 zLO4~jx>3D{A^Pj(<>}o0gF7=_GL=IFobz;T&8O@inh2dn`jBgBhPePKtVjBw^uQEP zqVfb~a2eLV8_Zq)v#r4U11|V7*kGbq&L=v2PFW$!I&gY5JbkwGKWSsJ}AAaP!742(%WyU?>E%NmVCaR1ACD})3Z`K~n2cA5flGyc|H;9>o z(7G5rb7HC34U zhR43XA7NqKHPKgL%D9NNA4U&$26*u9`pXdo~bFg7Wqk|dRxGT;gXZ}U28|~WJsUSOuGdStG$i_p-KYOnxb6eU@C) zG2kw5Tof%y>nXp%yY?!=A=$`|82<=IgoX6kF(BM>!5vGSEuZSJ;u)*6GM_bT<}J3R zX5;nZUwLlasMh6UTQg>E`*pQC-4xOmy*_WVCqT~9 z_O<={*%dj%7Y(}N1)^)bq|Bs}Ag_W9ve}>g9pHJys&?N987{iFsi;SdA-$`2y}hBS z(UpbsW~Y6lYl4xWfW3*~lbSxNYpo(3Ky*HE_a~N54@+LX(#~P!CP!ec$uXSN&QOu+ zboDAKLVnGZNktnfI-0U~Y~O;sJTvQTPvq#|jl^=E1jlz+F6E$d>3YEP>hEhMEw-og zd`K=AbzXH&4R}o~u1?*oYft7M2F7`!B*3<~iM}&LwA!_i5@3Un?q{ojAXY5${hKsh z)dZ|&vDMtXRbd0aa;Ef?v=}RPmN0^P>Zd$v%->BArW*De+ZT3?c4<{5<<2j!&Pj( zqal3hAmo>~$d$JlHvJ@`L@IIEk-3mw#1#PV0S#Abf1$#NDeKGQY{%@aXIAqoEoGg6 z_c&n|=rkJ7L6n8rAj2;h|BDsIXP(-RtykmRViaAZik)ZKEbi`tLEshL zl~?R+nv{p@u}|jtKUT*QGm}N>`#>(WdjZS5Hd&koYi}}(TPjZfYLgY`x$d1)t>JB_ zSMz=2+MuN;ag4k-8cD)^X8@RM1msd(Wek|UqgZ)Qn??m9qG}8jCDD5jzFaMjzj`Lo1!gqBQgq8pyQWx;6zKCC4$Nh^UNOz3a|xJfap1XxoODn5pGr<@dJEmHy~%D!OPu10YR;QFrx60mLCmIVT`H z=rrg`EPn*GMH{7zpxbIN7RVix`ctPKYTZolo^qe56illZUlzaNl%OfW7K`?`FxB`R z(Dxw3@ex;CE2aQ_&_$O-Z>NW8=oxh{f1klBc|(tMjAD4&#aU;K`n!T>qr%F}`hE^Z z(jWQUj=65pphFEZ&5QM_DPD?^(9g|mm8tiX^t?USrvNg(_g2N@qa(XW;^)fHiv&)z z{!A|&yB)6EV5+i($3qNVJp(s-*ID2`u^^lfL_2UkACsyA8x9I=TCZz2FZE&<3*mqQwNwy73Oq}>khShMksN3!r*ePb6-0b%x9AZ-?OB=bDJlTWy>$z7EUv8NkRgy0i&mGvob9C7$?Y zG29w!$yc>>jQG&qwFaEa&+9&lUDl&sZ&W^G+y z-e4zFz(jvWW2`k{@8$kuK1o9&q0Qv~7v3jF9AoOa9+vN<1V@C4uxS9gM>4ym%UKV$ z5a~24b9n8G)O!Gq6ZANmFUUG6*en%qaM1z?s7T^USM8!3PMUmU3^3UUQ3=74+z_zBwE`Va-C-})0I^47Io}eB8P$x zC8XuaN;pjRm`hs2_fjAW0O7s+ZfrEl)Rdi+zLV5X7N?J126%Z_;1XC7v{@wH>Micb zq#74nHW2z4z%kS#-Ghp^GaDFHs`~@|RixNBE~DRdJ<2VhwmNZMrHgXL*mVI`MQdFs z&W2O>Q?1K>k?v+fH+69qe|4m`W8e5U;x~@o4+I{+4;J&b6`EX2aw!-4UZ!;?LAw%@ zykk@TH1E1ziTL;`*Ds%hP|7`H>B_2Rs~v@ktVDF`4{g#gf1jG;MuGaODB| zFob&aEpeM8AO)=F< z^J0B*LN-{|s1=M_?yALssApgPN#FoqpJQtpZwcw+)WR(4LWXa&)Kfn1Y>@hHb`9X& zvO;$=AZ0i_6iQq0nm5Db|ge32XEpf&b{E`z?8vS9yS7K2@MrP zW<6zz52cp2=~oQgs($eeNZx#EgLL;$P>K7+*JrDUg(SDTPN0?nDGGRC=n5qIs0jzX zH;z}79hA@dvVOlHVBg*heBH$)@RH}+WBl-W?-CX6c@Yo3ul5$5g!3USsx57b)gH;i<9c>2%Px+F{W4>%9V@gy~J*CLO@o%HWd9ws|C9hk4 z3g0_$#q$82Uo;H|Pr~WHgzVis9cb&w&kOSLf+|jY_)ryLAljNH;>;G(>zwfP9Oo9< zq|Y=fxc!)hkc5H7YY5{l8UrwTxRiu1)u8KjRt#T9hl~po9?Z&%FUbv4oU_Jf(pTNG zj{AP`PcrDiyoK7Sk-TGUpy|@elKN{29p6INB!E?{U!YEU{m~)&?J?S}fkA-IO9j7m z{P4#s5-+^nvIWNH;UGA<*xi;imQXwhJD=z7ky@JFW(8CzESbZ;zB|MG_zFAL2XPMH z_2V&G)FTa{Ih~RSLpgcxRo}|GI$av&4K;U-7XszJ6FQO|Qr=r#3@}fpE2sdmhkK?( zIG+2OfTV!O^mB0VOP@*@!CY0Iztw-A73Jn{mI_;YEL12EaYbP61(Ej$5}^=uDo3zf z9~k)IVuV925Swdjo0FY+24O+qbG6s#kFFkKDeKy)FFl@Gr$tGvA4CuwI1#6pc z@NC38lTt$O1LzEy11RXB*8FtYCD>){wm5n1NS3@3cA?Ri1R;Ei4gqv}6+X5DET)Ep zx6L-+KQ}$Je-s*YOXbCrG_3&kjA?ftadOx6M{FOx>CfMhD5zm3O#^55FRP4#glfhl zDPdgITwM8&Mzgytd;J^H`n_TI?_1`o%3M&V5yq|V8R6*qPQEMWV33@PpE>z2PD_93 z0P8XIFXjxh#T%^a$wggcfsgI(LRTetdBgr746IouDvdu5lLt8C)>gS!M%O`H9&=_| zHP!<^3Iq7+m%p79IMqjSFY}D?%pYD@i;zn{cH9fXF!*(8GNUPgErap#LBJ{_Fe{CW zdpwMc*0kP&@%w82NyW{v^p9$5$IsE2OSZDLn(T+(t|m=}Qy{K_5;kU0x7q9A=Rypj z51xDT?3%6083sV>@)qayc~s6M`t(`2#$5F;88$9v?)A<#Ej&yUIehgT6}#a?>{Sm(_Nw5RBjpfx*)k6 z*YFjC!Y<#1UZvGeox`B?Dh>I*nAj?7jA(-b)l)FUomQkKp4&mIB1Tu_61lg!9>tx+ zeJO92{ROq>q}@lL@#n z?};z|@2CFT694TJ|F^G>ilT@MhR>f~e!74Ee94K@cz8E-ZS^~&RruN+N1{0hmjhQJ zMb3faB&!N2#M8f(oUkPVB$lUOZd~i2N7-?gF=Fw{Z+Vh~TH$ZN#Y6T#HK@EjgC|pxw&6)@ET4c?Blh z@HoeBf5l*I_sEvS0I&j+Kyb%fILFA{=)7Wx91RpYv!cc8l-{72I1+s6m1xR;!HX(3 zqa|V&xSpx1m^`#p18Vink>Q`zflU9gX*mAQUu{I@|B{T1oXl)a;a@#BZTbJhM{aAg z_d@B1_;@PRFR%SHlqFE>`g?o+?_{SL7A^W)hy9No%;=b|Uin%R@_*QefC|K7SLYZ0 zTA-h|qLc8NY`dby{lB03f5{TBg(EKHZ6uvVm2e37`s~=95JguQ{v><(HHJh9T)Op{ z{%7YqGi7Qw{{PK~EE(~fn9c~aB;nT1+f}B9zL_s-vvHeE5aC$0>)0D=MfZH7P2eyj zd+C23^nZ$Auti&KcPM*l{cbeH9i5#h(??gg9^d|dDxl4<)oNccm8U+ZO`o`y9Nl{G zUkK3uogn^SOO-3UrS|ssfUB@Lw+PpJX(}e~KK0O7l}FI5Mb5VVN%pTT4m4q%@`R17 zoWqB9CKHv`)C`T>#$0^7_oR9!y5z1hGN9^F%=NkKmoJE4lSn-G-TPl>@UNPvjZ6(F z6xbjpa*6wP$uZNG>VNO30;--80Q=!z<84PdA>rCgnHN;zc4J4zomPD}g9~B;EZ5U~ z(x^f|0sSKXj>Tg9sU-9B_T>=)Xh9d01{#d+hkx<4d}sdkpbzwoIY9FiXgw`XLth7= zTZcTW$^2)h{MFRMP4d0GLY7?{n2`L!$=^ia+4t;-ugjG0t}b`I0*HpoyEM4kW+j!V zTGMv)+8XL)7vD}9`UQt_=COCQiT;sxze|Xm><}^^HEvUcb_5D*13B8bf!=%pTvf9T zM>D3fLt1SSdVOlBtucQ8kWB=WVa1-t5n842`Mgfhf_pzuMfuE7t?dVS1+i2W%@eH_ z<~%mDHBw3pUIvR3mRc`#bx}M5^dv0RE z^L)?6T`lTSSB=LR86GF$&SwR@?(^|_tzHuuDiU29!k(CBV?E6CL=<8vAZ>jAsjNO( z;sHEo-SP=o4$G6mFbJ#g4B|$}EqSs)j(zQW%6r6?Qhf1|wd|C*Gtf?Dxls1N4Y=q^u<54eRs#rLmYs@~SAOMCws*;9CtZJZ$tu^W}X$=6XxgQk;|I)JQHu z!vG|cKKLpoiOv|Wx`P(RYm~!8)))0k#X6ZJ_8|Ey%ceq4RED|0Edwi8YtoL^PaX-p z!EJ0794|YPSl-pRP8HQPqy?8MM_G7`xX{-`)pk9iYiwqYm*iBhk6#DVt#{C2Ig&)* z6w+cj=2#Z&v;tVQd|-aGy{U#gzBR2}yf~fUTaWRf{*?t6kcrYIsI<907LV^nr6}CL|y_aUl`OK7bYs^r~2g1y=5!)0*xl|GvO%|DG>ZX?8 zsuN5dt)UDz?o%Djk7LL$FB^WXBl#VXbu7Qo7s`8%Yk|sVjBUS%QZ2Y&g0-ih!Tk}! zSUL87b)&u{SX+K!-f_ulko{Sfab7KL3cs^JMZe_(p+GHwZZKqq{ir;OBr{y@@$k49 zyB<(!H-N2n)SKF?f`WJAgqreZ#WJrvbHC^5yRT=QU9bBv9Y6jy4kMAmF$NRhnCmeN zp>9kKVB8~5YN~`KjY?3_pZ&;wb?l75hO^&?3gm_NBru%w4iIi&nWx$A}r^w$b@I({8^YTJA1Vz9=>QX!4q4 z4EY1$2Ok91*{q}=&iq$VtLPUK;U6i>!{-__zmp-ey|$~VN!M(GeuQWP3D!BX*&arJF?Wu%WiID_a4s05T=f+nkE7C z#W(;&^=v~IV0TUvY#T{qx%Stf>p3IM`q@ttO44MqFEDRxMC&pDG)BlzNajhjI^58n zkQ9pR(U}SeS++H3i&8?#?ekL`Ar!2s!$Dc?em0;)KLsPv}wmdvkE$FuP+EZ~Irr7d4-ly$1d0okRIIoy!4RLCW6rumX zQxDAGr3!kgZ86KE{RJGA>#+#6ss)Dynwr9SuK?lWbRGG+xE^6>h5(?mz#>uaxL}O= zhW|r@H*MT1dUESFx1wtAi~EBmgB4kFRkXgkv=FL3Xgi(%-K#&t*o2~G?MM6JA(a<3<$veIAn7G%`MZjZs@gCiyJ zS2U4Ck91RCEkTa7$~{@GMH>h;=U-Eif8A_Ows+A$v1-8Yla0j3MdL*k4Eu6f-s>Of z_6avm=I&`i4q}ic003$Ben1N2GU2u{ zX$CdbXr#U;^Wtq9Wv|@T-dHR4clV<)kAy8tiK>L8QsN(zqH_&KDrR-H)@QqUL03|!VvmeApnpk0U5(i z?>!fk>a?tG&A3wc2sgRUMLBKmiU&R|zO>&vd)o5G8ZkcNj_RrCDf~$bPDJwm$RXofEKS2?Cf7 zObWmR10;-D-IF(+lY^n&0BG${!dgx zk;;Vf5Iog*4~+&7-GzY5`*)K=mTyEt%ja_&P5@1ykuoKaI2D&LOJJk}E$#q2&?k}S zIDqD8Xqh5Os6<00%cs7o%iX>ejXm70??@aIenEz(WKj^}s|a&lx(2AqAB(Ub73{9B zBV$*Tr%*5`US}6%jhl<}ZpejHY~QczgFi1tmO3i5q69e#f0qxk6 z9;oH5DMiNU=DD^|bVk5kZ{b;yNf*Y4t}RFGh4hQX3B!E4wzJ8mcBc>H3B~=6{9ps5 zR~U62ZY$1zJbHx(4~w4arLEcF*zrv_x_rG2;c&n9y0HG@NV*?~#L2ZTrQOvxyf4etp!8N0FtOHuMNVsBCGPPu5Z=Yb*XSe>|CY~d`+_1*8ne*~ymvdS7O##<;BXjb^RGAa=4~b-u!>I8$VpXq@-bL?0 zrV;Z#EU7G`HwJKiXUj0j^UdJ!VA-b6A)QY>E}2Tr^1}ZDW!eAb${A&5Olt=AZhginx4Re-q1 z-8QDRAs%Z@m-Ad%Fx%X>$NQGNob4(X`l^@1dAw#!7#A|Rp@3HQy~|8vILnHS)p@P=j<57m|pfdy2@N|nw#C032$DWcsSIRtc zj|qGAg`n+V*6Trz&2vK!g5MsCXUhJ2j3QV9^dU0IXfl<2Nz`1!1)1=M^+-dU>f2`S z=u%gacM*cpX5k8ZEM=}df@05m+!>+)<-ZvnL?K=Q3+p$gC)xoDo~~t9+i04Z##4Lt zebI1@6H~7Q18E2S)CaXX%!va>uJextD0dbS{`ix4qI5nR4%Tzr9ND{ab;k0K#@4pP zQHI>htmA&ZcPSD5$zo&El4tJ9KcFe<@>}shCJPXn@s4j<5P#rg;)`SYS444Par~Xd z#t=^^k6B~x&3wCA{F^9vS?sIB@v#yos-4HUo^{IQUU5oB zzF4vSg`q3vf5>Tj9A3&6-qm07t$1y~C5W2Bqzp)$YDTYs@p^rgr(P9de4Zx@_u3kt zIsBpeEHHt~S}4Zvj08B};KtoD)5cryb3LtJ?b#2JyQ_flkSR(B{M#V*YJm2BlqFQz z)=P{jx*^i6krP5O7DlWqetq@rw@8=xgyUy~9DF)vxe;ByrY_&V+H-sx5)PX_a2!o@ z>+CettY5UPk$uOjfA}tNEy~1Jn78eNC4lHb`w;*pE4p=4JVy=Ua?SS2SzP|(^3oyC zpv?DVW}2UF`Mmv(j{doWU1M*@02Li#`1j55=T+j%0Ca{JruoB9 zbHs$&@gMpz|EAsYUn_<%rUV>3b`Zp0>IGe}Ug=JSYnwE6qNhraSbcZ6^BR=vl_!Fb z85`h7d*t%GtfQurBrc*^{ZBBS!0NV~1AJw;SH)((v@|C+Ys~O@{?u?Xv|mR43WckD zmoLpbrQ;}eyY4zXES|)NxJihiP6FG9U{-f?Mm;<@EL3fm76%nUMBWphXhrd!6U_hwga90$&=z|$_A#0A|KsBe2k#_Jl(e!|Y?N12i-qX9UF zbY(?oyuGx?-VG|f>nX?2&%JaK5{S$YeqcO5GX?};TuPj@Ks%lz#^tp!l=RxL(xV)H zN=y>0bL@@+jvK3QevHR|0dO|w6}{9c?6yzZTzi&R3{`ujXoeRf&rohy54%X5=v|7m z%X)-3gJ;#p2c}XbV0Kn1JWO*6t8DTd)b<2DKI+m)N)4mx>Ym0>-`Bb(5IKo{XMXN% z#amu7HM93492zzRcXxbB7dLebi5*0F=? z@kE~j#$~I{gfEp6q2ep2=JvXutuIDn_*`4k+!q$}Zqet(1zfxU#&LQMe6dtWS$DQ~T>|BU5(I&A1IU}A=+qy?Ij4pf`=l6b;)VqjXU!|536UeNiUwCe?22qiafrRh0$ z55;Zwn89iPkmt}zW63wrh|o`c_VMYp5=kYN^IuGXdCE^@U3xlI~IZzCM55wiHk)$WwIFz)6l^3M(z@gie|uAF%^ zN7W_m5jyEUp_R{cuiWKHbh+}6smLV>5>&8288#)LfR(@`Roa`+RA5w(3QDBx^r{mt zn>0LY!opiK;gi0AD!4)L!{0ULnkN(+zHgM`2(f3hXh=S=o1L@B2;hmGxPA(rHy2z% z1$!Mi9q61WN{RCLQe+n_YJ~-8VR}vQvwH7+fH`vhF=CPBaPI)6(Z7Jg&|az) zMc4kEXMEntBa+ni!+>6!fQ+Nn*Ji#71+h;Wcr=l2oV1tTOMUDloc?I%M>lt; zmUw<)`3KKg-|dCc2FuL!q-AGx^Zlqde>AH5{A0u z4q^FV|D%ZxVzF;f6ph_5J(D_=39UTr?|DV7QHq;-YqL(+(!+yd!91@#4m|=-K-)IR z*s__xaaEdZsf$OOJQv*k^jROt%3t~Jaszh|Lsana2l$7#GuzBxLHi~2&1g>e6`Ev= zH3-!Jk)2afytwXvbb0>WZ}u;{RE@0_X>s3H5kE zgK?x1h+Q)pCP`%W!}d;zw1ziq+tf}fRm&SNUmtdtFg&Y@J~tF?{;Ax{B}EWMMw-aX zKS%Jwss|b6^q~cCu(+B=j;YdVsar*guP`kUG#e;CKGYmjRYFHI*>c>DV~FXU5($20 zTx8dZFm617B(|gaHRZz^UM801dtYK|Yvz*GqIOH{d_49>K#vnX*C`@&CH}H9kUIW* zXNF}jh*MFM7hYh3XN&=d;xvs3IZ(ABMFz7mFEtnThRBl#OC8e*J}^n6yvgps+NVIQN297$g!Qt9q`!r=1u&3n% z@Ax|fuf>Y6b-3OFY7HR}r*c`F-uKIdI#675Xc6N#iX?%BoWx+@TEqVE8~jLZZ*eqO zx(*j|9EDduPU4Jj;B&E@{W8vO{aNKj14-C;7a}PzP4W7^e`B8M&7R06ee-7$P{|FQ zu$S+Dy50b0m63Q8$G6pwWd8)jpqkPe&&u#Z8>JEE>f=$370RW}Yl4nS7eKPF-U)j+3{vaYXiOElwRSCjS}Hp@Hy=nk`OU!X0_k_iN3RRA+6kESr7EtBL;ZA z1}W0Q2>%vQV;+Zb5vKI;t`QRy3aXS>>kd&bWnm4jI$ zlVLptv>E~s62*_a2Cr&~VH8ee8PzrthM?Z9cmeID_vMX~lXy+qt=m$U_u5naxoOi8 zU~6-|_L^%cT`APdpRnq--C+`MroUbRpZXn#Sk-u!1gpd;YjEhOnY`h}$)CSWbR>`i zHMF*YzTsu@34xh?ghYkMX-!`(I$g&?h3Q+JO3s<93-z*gau(ENsVAjBcc0N}?X{pJ zge3+R%h{yK*l5pbGYyssD7I2=$HvUcc)PWx3VR5#*c-vv$okKpq*^t}zPva0sVoI5 zlAr1V5#NOi#Bu3f5r4@%z-y7jmRqCtA2a}rQ6$lM1QcMnr)ZfYaXJvPfW$1yH93@j zXztD=CYtmun6o*~$2WPH8ED$3wIe@jxB@$PfAiuKZqS=gxjaB9*9lvhIk)0Kee%6S=b)+ z`%z5UFfy<^73?csl19l(EN?mWjrY`YIh?W}P~yWKr@|zRoiPkI`@rUy5I(g(BCY1u zXOLyB(fEy2xBQT73O&dYo52R95Cf_$rLhLZg}tl^v6sP81Uh}{+=Mq55QvyM7Mebk1x2%WxSS(-#~jXiIpwvX0qIalxwCKC{=m z*%1)CvOQLnu0xT=%wjl{x=Bnde6}u#vRsP+?hML-QZr|-Fz!w`J+MhbGLJ783eS$r z1-<`cQi=FE302w_;1Sv6Qnzi*O3!-3GmKy z*6@c5-3Cw#BaQv3IO)uVZ<*AmH=*twdto5r#m?F4FIP(< zdz7Dge!sq8^>$TaMb*HcYkw2ednXujh`g!<>NlQ`!Il*Q<-6v53VcIb#%fBwZMJ{* zeaV@9vNHk=oNun&OTO^-#!md7*JSSma%(}kST{ah|HXCe6@IkBcz8u zaMNN?=)?z+@HlF{Mqvf*Gl!;wC-ghW5_TZ3dLt zlW#S+RN1b18N9x8^Rk`$)@JG&D1l$1kzj~hze5P%jI1Vcc|HvNvC<&D=h~E^*eC8% zt8X^F< z4CR}5>z5jeqWfI1QaqS~eg>6eBOI(tO4AssU_V_HsXtr8fnCazFwXd}@<)}hpylBk zZkhzXF-9^LjTh4>ks3%C2)7yye2g-Qr+#ApzM1~VM$XHkS@)F(Js#D6yZ@Pu_Nqj3 z?#G9GY|0!On)qH#R)iW>lYhI-xZ9TT!Uy=QSYJ({i%4loE@HHLNJ$3p`Gr;aN%{z3M)n&uEg8w zxCgL~HqHkj4rPXNa`LIp2qh8YB0@(aS&Yy?Tmdg{043-kn}(Z_r*&2C3}lyxYc=dJ2^6q<1nAsE?hr0}`9&i=z2$7>Uc z;m$hsZeCA<&$a#(gJ06Pt943>M*t_|lfKcA<1D;pk1^wRJ~NJeEVU9IwPDO8m0Vrk zxRT+{vv;`Xp0Do`yl+CRuMlL;SJ_j#t{JI%(rX-}!HqLYP6)tx4s>xpN6oO_X#*7L#M24Jw0MOU`+;4@6tc2V6@U8-`Fd;Z{KtlxTXfQig@Noz# zB|v%)hYF>PNsx0!?w~j`9SDAj+sI$z*qZKgCZqSyGlWTbvtm^<8?}!H283n5Wg;|!mROW zOdN|uf!f2CIOWuhH5S#}HRQNWXM~0nQ5(L^ikj3^@LQh59w<@zRy}6HI4Pl_YVQ^V(D+LB{<^$Wh#I zrjbdy-)XONILuuC!neBAf}o;!%L_o`@*|0$9zgDd@F&?<0K6+_W&TTj=;e9z|HQ}4 z{5wA82_c*9HeARo`5w!~L*FWS#)I@aWV+@X#no?LVn{!b1k2e~7R`@_)8Tt5q@r;;1a{eUKcnZjp0=)9)etlXD&{84N@@GJ*2dmy@s(g<*z9U~J&VM@MfkM}q;0GBfQ$eMh@NX44q<-D6M#OlLZ;0bqS?WSk)5H}A5#}Q_}!qwsp`ed^@KSMMutdj*jcWH7Wix-<~mr za7#+$9sGV^1q833Z1@+v;xL``gf!sW%rccTueeBMGwa$83sd?QA5aP-uaZ+W3lgH% z22pC|gnB6p(McuiqUOeyp8J{B9}itj`+X-7#X{8_d}HZ<84qaw1&sI|xLFUIBT9@Q zNnnJ)WMj}oBZzH&aaG56)>n5UCPTHUyIy(u)zJewrjjiRY$M_}-VcL}2&e=YtB}3Q zZQjNHm4ez5yss6YmQNqu@H9S?TX6yuv|C6cQ{h{apwr_{jNV^I_zH8t^a(BT9tzgM z>@=XKVCf^#{$q1?si{vHO}FiWV$iZgIlLPRA%|IIUReF?h`P*nKRg7`m8m++eU!G> zHP-!sEdQC6bWp)BBM|%Lg$|+a_+pGbQ06v{s3ID`cYCOBh$X#O!10ApUz zhE!ia;gFA!ZkFpek&Ar^?d4)~@-q#6kOax=ogxgbPl9iHr zLcYB6wO7R7ex*38E-{Ky#|MtPCm~T#AFt96aIap*;>CA`T^Wd+phV_O83}L+*`s?d zbko3f=+5fzCXcoPVS&E@RJIztg1>YqgU)~c&Ypyt!!O;nYP{l}e*6*EdhIZ0xRFzJSbnka(vgQFmJHFWBT$n*-fp z0!`D>Qi4YSkxkOa$Vl-Rx;9z3bgN^SN3Gp{_&-CL>5uHPKgq^RG*9-zyIW-F5H1Ag zb$c5l#_koN#%WlwUixrS>`H2`Lci4+!B?M6YF~464#1vhh|cUb@X_@tX9X<)Ax|&K z@XSSt_Pcc^?f9{g{o|tU*6rUqzYJzI$>xRzKb|obeQ{;MW46sQ_NzL}ax{LkI(Lul z+GXi+3Gt1H^AGe*C2919y@G4(RVqkTljIr^LhOx;X!g|o>pF8 zv0A&m-wq1KfN&r{*2#U*ym*FcOns9|7MYx}Am!btjTO$}uG>#P2>9yKC<2#{t++)j z``7;Lwb8J}JME3eO4!8u&a6E2-I#Os3jG^HACmz=E9szL1Jkd|+O8leOoYISnGQ@@ zw|C={Tzw-+E4rCx7Aa}Yq`=h5lPoJQAB-RFzx(%55BO zYz@7b=Mas0(|YpmlrTS+Ck}F>>U~41-+Rs!b;4!ic(Yd zGmTqZozC_02vH7gUEMY#^Z*UeJ;r1XwpQ@YUgZVo=u()!PpdmjEH^ljNqKslXOV6$ zpMisYZ0EPpUOdFM(reKJd4I!TZ=d;Ivi=8o)hk2wwQCXQwfJ;=NQ#8iHCT*~&G5SV zT|R}qkwNd2){oU#V(nQS>dK2w>Yi$vNuYBFL^A}tJJ#!JK&pZ* z4SBBO&a6l}zpcL7%e(XIT)^`Er1`t>C%2}#!1&hTYAgN^=H&rrP}Ekgf#k?AZ{`C@ zSKv}au)n=zu^~6ny|8AutCK^64{?n%rkndKy2$@4qiV!^luz@;p?B;}yblNqp};Kt zcHN>8Gk=nK98iAd9*=5pjIo)CZuF_#Y5ERbKx|~rT5Y*!@|!60E7`9;cPpn&otKJ#T}kM-xY?ujSz{lRsqtVdHj|-=)J)lD=i00RAYEpvD zm`j}dDW@)wV$p@4eklMF1?eOp#{&dtPPR3>Q9SD^10gbz`5J`KQh3__z#q7QxNso( zYe2$OVq$ichb7OlR8egUJLTMLy&q+AVa~k}kaN?-YM%WJV)>H7VP%NP6Umc-IT&A- z%9DHcT%Wx-Jo`aaVY2R-cOzEC?>p=KVc&4gz;ZVrv4!9Lw0eYr0ohPGQDTe)BKi^x z@HfZJ(6~$SQcC&_0gMmH6=h&Nmz{-Q$?Dw^VBt+FA&=|=Akbu__aF+AnAhY_5*?ZP zStb2q$PH!%2?G|sYGJjR8}G_1b*&g}0HEyczZq=h@k2sNHNuvl^^&NCr{_3ch^~N9 zq%KreNgRi?zO*l#U(85k%>-|~#C8-t+IaMml2XI6#*Y@<$%jylv!|F?mh{BD8K*=X8 zhR`JxcOW@%Lf^ILAt9vxBs-W4j{^PW#fJxUqzOkVgja>lc0L>4yr$eI6n@M>(tlbq zC)Yj4zzb^-VvpmBqj(&lkS|d>O}b1}d#@L_Xf?NY|8`dbxfi3_H^@C{7nYt)1*b4P zjrbYikLSzTtj@pry{THlqyHqkK;lB!H+8fTe6T#BQ2(tPt@_Qvwb^GbZ|Vq^#PgoL zlhNa}mdpL6?b_eig6&N9kA&jz@uw;@v|4zN7{U=bT6_LO7OHg3%w$r zL|5WAo1s`3BP2|?UN3poyfI>5=_xe8U_!qz#_dcif5R@t>R%@HKY5dY!moe$>q5B| zPd4(4dzt=X&T3ULa)n<_`q$On@aZM}_^&4Yt7G-yJ5#`XI3TOzm&C6iVMP#*KJ$HP zN6&q~D0ALSH!!7%!LCH&BuNu-VHUyqil|$Q4~>Ke>B6MH?;C&%y)IoyzV}q3xM_`@ zOIuEW3`k-4bAI`{zf87+Ip$zNXstY+C{Y8Sx`v0AuB2j~9VuPa{StBoXQepN7H4jC z_Rg~&r}Zm}Km$qGL?Hi3-^@swzpnWB{AQx^SH3$=Gp(H- z$OtAl0U?{yZ$XE3c_2x8XNUJ+mKtd}d@}Gd7EQ!AgH4^(Eq`xsRC%9cgCjryL;-Im zF~KR1Hjr8n&=)@V>(u?~E$kZ(96yS}XyA+ejB7FgO1bCu z+?}CH7;C{w18C=lJd@D#mlU;0Ibd$u3@ZyA&|G^t#3+LWH87QaD$P%h^E+lahi~ol+X-B>;exsJkdU zDB{a0pV95^Ny7@g(DN_Hp7!wi37hHpbkHmi+0_o&?zNx!@+aBvLmND37mo*owmAJ$ zIPKtbJ*jZZJGg;ZX@B>+HCKw-S^1(nWjy%pxTR}Vzo7)QNLRmj&ksr^@9YI z!8(WzPPgWrQRc~s7R6!VuV9VC+0grQw=!n0q$xHhTMK6=oPFygxR&M@|E1tDh<#r} zmcZ}G{F&Qkn>#bF!V#h}p(EScd}>i37j4;`OL^pcS3c?S_M&jD-B;t^haSRQ?`nfe z{{b!J5OHUYNatFHxRPL%VCZGrT5}Q4)%;@bzDozk-i0R;LtT?BL5lbIK3uISKCbvt zT0=VPVcLh`V+R{yAyPF;S0z`pc`e%Ppa1x=wEHr{Y&TW+%F5p36aC_6(xE!PGk{U+ zj7$B*#_YaO>LguY3ml^@{6rik9)HzMos2iV4^fUgJua9Hs62>o24t~>WFq{LF*dHz z^-q^>)_-Pn31*m{Z%NY7-<Ztyp#R?f`(=U#0Jr$&^{lwBjj#S)SLh23!HK8eV~*A4h>eoiyeE6 z=z7EuI(O$RCp=H)YJ=rD7ZT1iHx)cwOTWezS4$!6mY<^y5A5|2GCqAIx6ug z(f2RmCha^ent5yo(+CZ5Jk>>i)FjE7XxOcNyaxn|oWFVhZGadvCE-Mq9bvvE6VnvN z5X~JGQD174q4AzwV&uE^7c+z`{F{LlZailUnv>wr160wT z^dIPx=4W9_hJ(;AM$$8UPEwl`EW=xN9Lda4SIkC}ANWR%1AVf;q-uZNOCt#e*i|n0 zKzDTo{75T7Yo@x!wb|Lrw>i@?wTwO}FrAD}OFp;et0e`K9+yUe{@-n%Q$>&dZr4VO z0~pXfO8grDrd)?NG{^Bp74+vN2N9Qn0>!xoPaAVuE4Z9~>mV;$h(v7V$SUPH|kTo|#~= zou@!A(dyrPLOJAApODqn=HA<(vd;e)#u!K9-iB189x9GYm6ey@N>lV*%$~WISe4q~ z8$)rK=dl0|eV(v`w_xOl({*>U5!H$o=ZQwmZ3PB7+S4Maw3%usde&^)HD31fd;Dhi zP3pJ(G@e&={62U7K71S&#FOL*#l@as{J5-c7(Jl4*>+^t(3V`}mZCr9)M$QpPX1c* zrhy@4v|L25oG_)*7($G&EfWXkS~v2-MpYxD4@kV8mThjWBB)X2g0_C)ynTVx498)Q zkGC72sph`o>Nh;U1!&{`a$@;4$G05qd=oyf%0h%qgWX|R5IsPEA^U>jn9-H>odecZ z>gMWR;G~_*jnu+>ZdxaXOu_7&dyJ=A@HcB;W}m`cx-7Xj8hjzuuRMd!OMN-j58#=p zPh$QC*{92WKH0>ss5B(w55`>b#Vw z?zFYqiNo&48jz0+T+?T>3i{hfqBtGy0ZUrpjk!rk!LFXQhJY9wL%kXJB=<#I5_+HN z1^6B7OK&v8d#?obd=ogJdAkozZLA_f2&1nNx9~xc8vNGq>ryH`s!=uBY;B}h0q%am z0ZJJiO9^ZbY)5$9^Eo36ydj72TeF(K&zbu>Qyh)(qS9W@P#>`M9 z?xoj*G%W&Q`0UQWC~f(}2l~`o9%ywU6h|swOLE6rKK9>Z?QzhYm1^*^y@K3Gx3A4S z37=ot=Ztdp=2Q@dJ%2oSiGtF3>HERogfovR;AAhu;py1Gk%cRAq_l(Z^UMg%S}dA( z`mpzXTu{(`1Lo)vfe!0a_+F*bIufIc2zy=l^6w%>{9n&e;(h1>XOB%oLyy9{vNr`^ z2~9n=`1C&&DH#9!hd^@-ArM$8q{&x@+)v6_O~m_24hY z3`8&drKp1%88`gBh0V(1>UO0s%H7LVQWtD%&yRXnq=}zt-xNOIc(TEMub==8#+}4! z2zTLy`X8!9HA>iDvU8;W(C6jnDy(7?IqzvYsNU5mG(yX3)a)lVbXolj(vr}O$@4zJ zQq_f)KrB_5><}7rk`zy`?Wa6A>~(IYe;Fx+=wm(Lf1o-y?VQat@bx|dev!7$ZucYbF7E6^pV%u0#bSahlLVanE z^J85w3k|YvXP9V@bHNlOH@eio3vSp}p2TVizfzptlQhf~MjO7&(1kO$1Qx9YU)tTn zQcaQQiORUv_prt@Rohwad$KpU#~20PP_#U&Jsg=|1X25p-#j{qwg{7$MBT=uoP%rM znk&YA&2Bf$kzw98MILis$d$1EY|A<#^3L2G5_IvZfS=<7$oJ6XbQf3|BoMz~y`WpC zO@EW+%DPsRx#=S^UNZ3wGK4Lz=5VWLg$`HHg=9h~Ps@DmVEL}tlI8`5q=IiN#+Vt- zS;QqXK5sXEZk0g}o`oJnDB%H}L(thu>(ZYg! z|B+T-=%9{(h9G{XJmrfcJMqS>KOe{T9ulRE`Nn;yuIS0y=gyJCZ4}@_@p@1(_2%nr z^U!RU#N?|yQsn*L$D>K=%frrCh8sKIHS%BhFVy5qA!QcSLY~2-X)eaz<9p2{ic{Vv zdn)loIzEUTD-+AjnSfBo8|&l8bFg$$;yXPS-LdZfw_jz zS~sSL7u?1(_R{U}aeuc`0LO`>f+<2T#!3Yg^LaG>|2 zGE`8?KnA^9)K?1m?w_{3pFly!#@y7sCMG$OD?o2GX9SQN+Sdd8v@kvUbD#P2ZsMt; z0s>&i5DqV=X4e{#4CVc6?9ba}$emAlt@xi=JH=Li`8_Eq*(h-${^Hd>>3wzzKAC3@(@ z{L#Fy7B=O+iQ-z*muu8wcZh58KX_~W`ofrTbIFPjgeJB_L*39gBV)w(p7X?pwY9#h zt2&py&}_nETzHb=0XxD9-`Tsv$J&#{&pV@(Lb)K)(YSa$D@enBJ$Fnn$ZEwT>3@*- zo>5V4UAtf*QL^MDAX!O@AUTO-5y?47E}}>(q@c(-BOoXsIfLY!lYoHa9I6085U3)S z0(E+y@ASQ&=N+%7Z}&HD-#fa0)EHG`!`f@jz1Ey-&-Kjbp(vN}#^nZW_;is9Usk#y z$RgUcd{|;#3z3aEkmlL?Ht9!8i*}JpE(RlwK~;2#gnYJ1Y9WbavYHSdr<2(-<6vR;z?cS2)lt`Sg=082J&I zh;7Z>scQ++XMD?Fq{{xLe{50@b>h7JN>+6@Zv!fyfAz2pH57*7*lXlR;LSEPd`ug; zFmGs@-pc#~q(;JDUt=;rOd4ci^|1BEW|5qTx(;$Z_UZ*v6M>)5;6`S?gw7eFZ4}6S z(UfxV(g=J(-iM>a!En?+hTY$ZGb^Q!5uDlXs`XQ+ z^Z|09EAmXLV5tS0$j5rAq0P?=KH@O@0+K);{TNF0JA}XSA=xdkv2xVum=LdFyLgfD z-j}e3#$(Qp>0^y=hZ<^iLgPXxUb|m(P+@Jc5CK)LM98B)3`JYlB~GhLf%`jur9Et4 z0``~j*7P@9u!xRhiLKgq23kKNcLon+5$|7{%|~6y3WRYb>^LBSVob&!kDcU6g_hni zM%5M@xCSK>2MslrDQ-pQkmc(yLH_Gs)O!-;k6x zA1>YemW+?rS(OK@56wOc635nyNU59Pk_0+MKRAyqmfPv5B#G=(x7xSNB3b--;K`E) z8d3i~;stDxDJXAy3-Tfu(Bg3FXCY}Ix^{cg87IJzIl*9~Ni}w7Ci!eO&pPC*!1Pxd zPPqw@a{$Ig7A$2AXQAK2FaqaX=Y3CRlyXb=C-={j`CBge&ypG0CKX<8^iJQRwBj1( zunc|}%!23CMPZK;$SoX0jK&#j5;-zF6*WE!t4aYmeSEZp8^3mwLvqlEZ{t3ltjy;^#&L_^N<*OuvmU`DF=MFeE(@qM5gZD5|~MiGLDr@D3b zFYP#7|0sCE51RU%pQj;zfD|jCJ{DFGYm8zgH>QL2574BO*7K4a&VcD#P7L;gvccxk zjY#ie#5H&C{OgsA{UW1|t+aBHOl7KND$82&m6(f#(&L_&xX^Ei9303I0^nckmyOG2 zK?;OZN4%ZwsFPzFG>^X3RRd%uOSR3H)oz1`3G)SDl$u>O4((7Tk+vc^;wJlq`pX@u-ajcGetzOog@ABrTwIX0!q>^oqhQqjD}ROc=NDA* zZL_)`nKnZ##GsD>CB2Cl4V1?a1Ab_`LcyXs0*3EIbDpw5;6D^2&0)^RDUGrrBe=N!F=sfTB#owkVEjLvx2u z%S6G+NjQO&QK*(0EBXdxXvI@YwJq+y!OL&_i^?3?cy)Y$%q*ZtqmvF#kCUR@WI^k# zF(%)wR?2|yU(TX95#--)rqM+c5g6Xscx2~H*80%mg9KMDX=UDmW8?X>(SV;D)iCa7 zem5y~HoTjB&#;yDNa^Wr6p$+^Is$m}KX3fv|EwVVQwkk_gry!6RSf{Y!eQj62YCAf z#s)#qC#~^?MTP_C?~1|>ih=}Q1bXnLO{vu4nl@=mFaU*qvP%}!!n5ccI7?`2R%K3v zlo|ZM{Myx7v8ozja$87T`!S!bZU$!bYyM+S@f0v2-hG%7PqDk0C*t^nD!gFPgB2mG5c3a;56C8{JW1jrDyoVv~NaJL5#KK^ zA2`fm7}`U$fwvPWajz1S%7Pf_=pU5VRVJa!t*rDDnd2bbPS=JFfTgmUp$8sw6xb!FZOS){G40 z+LbxlZ<8fVVm|)x$_-(rp#SlbE7SFK)qGTYaZBC<(vur#614q^yIT_^8!`-fmbk z7=wl4{DD>k!aeY+L(V>fVX-h;QMZ;!C^p>9w3)kAJIuDH?hxnh_Q*)jI$M>~V3eY} z!6V?#bh~v?z^8eD6&lD{5J447(VMDWt0FTDx^;%nbaZKzwZl>JM#hE-grTF!x>eo% z0Yj6TZ%uO*sdTCST?vve?O+1Mel{)gy^pa^sOyz|;hHItwAK7G?@Lb!uU-jt>2@B` z@lR?Y3__h#M$hy+ovz37tT`O%ky(LU4(aRIcB#uvRM0YL8B5Si9eQdN$kK&Y$xG18_X5OarXy|`^r zOdtHbEC&j-b#YvqMYb+pPs9e0ev@VB1WGe{sjHlphT_h$>~)c|gE=oa$Knv^Agr7G zQz2A%OLi|r-;ai3g)8x%Mn^+?tdEC=WC@(FM5>??$flt#LZ@sIv@e_m&k+3D&w<=* zRT;14Mx+}@i~5Zwh0jsXpA20Xej5CqbjVVRDMoZfFyl{2QCC7F0q_V-6XN!>^}gq0 zj?Dfn@=qv@0il-LJs(D+s6`J0^bB`00WJro;GwULH7LpF=mgZeOD9Bnpp+X~NIMCv zt?#EsSGX5FqO&t}!?7uu!-Oz*VlfXV44|=IN#x}|4@PrdDh~lbQVh@3U<4uz)z44t zuIL7nX<$xuJ6q;;Ne`rmE5wk&VKL{rpVM|{RSxnsgfo5=8oR{4qJ{PptZ21 z;BVY`w$AtTX~_1VkI#f7`eI<6P|9fYh!Ha_!_rujJuXO(wwC`mb;KRF+0fh)KX`R}tqW`iY$6$a4gu5qG1Cx)mmqS}4%MFnh{jxV58%1S!=3MKC`ShGO z9=GP1IzGLzeqEo;jnJHKeTyJ|Ld87xYdrt#bz zZMX_@jmX277A!K|;v#2yU7_vr$f52vQlwx{rKDzx=A}WTox{o=jC-k>CZD4dA|W%6T#8Vh7G` zZf%gilLaj;j8vnL;~1qN!mFNLx_d%6G%Q<-A9hwvf>s#tAD|9Kg)gbnJ zRRex~*mnFH=f`$cuEY`T znm;SdX>}SO(fWL)-)?QsU~ScNK))HnR>}A9=%yaH;z6>63bVxIke+ES_QB_4q`3Km zC&;1?y)M;9{p_ZRiJy|XWuzX)4GwD7UbTz|JheX4j}{NJMoh$Z)zkKi0d+O60RFt< zz;?YCmB5j2Siw8_W?+wbT`#yj>fr@;<&<&9_o{!|VZh19SSEpVCvwNk-pKKQ(W}}I(!E?KqTtt-9MKULU8e@?+*~O z=R^WPDj<>&^+-amF{7+Aal+l!&5z1Jr>}T`$!_3K#voc4mwT8)?f!is-bt8KCB7+2 zu!rb?yD!sgyd3(oDu}HIOf+#<#mh;R!!jv&S&KR&{yr6ObldqhFSU25m!*NWUy#gn)4{Df27D!|#>E%B_m7#)P- z?lBif%JvCTLDV-nUJs<~8_<3+gV`7xRGz+^Jz8_ylb0CQZnW25){c_zbPhiIzXlS zADhzu4C0T@$GXqX)N582Zxi_4z?qk|r^=CyD-2Ga$kW_80G@;PMr>RD)rI?WY;CtYRDQ?cUN#<@^K-USGp7 z_Y$}{iEnSnzD%$gI@^Sx=lpF2oOU+1URi+BsvfQMW*q4wr>MN-{#hZesvb|O7 zG^uNx1!6(+3W$t58q@;7&ZkvjNBcm1gRbv?8pnDVkwdKLb33%PBhyTSS1w7U-%2U< z8N>a;T0`bCacZke^Dn1uZB8WWT8;HB?UI(!;AC-5>HE?BG_rCI*b2WshLU60w}P&V zGk{a0%r@&FJJ;p*K`3fvc|<7rNzY_NiR~xZFs*tNUPy_>3Rs^ZwPEa_3cAad!EA_p z>64c{I>8jA*gg46BmZgeI%^bZkU-f_D&wrWJ+0V=vd`gObmLDg51BlsniFn7wI`)) z9S%Itf9j#SI%}h+)Zc)MXbi~k-$p-vKvpLP+AMnZQ0c>K&0kHb^@~aLdUm_W`9)-@ zAftU3iG`suWSLXq0tIiKQ(Y^ycieGxl_&$&40q^1ebYo4^j8x?EV`F+62`7*e|l%( z)Rs17KcZ}RFjQm=)@`!}*~Q5nLjV0-kpZ-!B>B~QT46C#CIUGhU=EZGc=@;cS(Cl1 zv-LUcht{5piPMXXd5gB5hZPN`I7-AdRJ>A}Dpwa`b(m*-Ps4sslsG$teJ)P!%fB^L zUQJic%mI7Xv@3gC<6B$Ku>PdxZ>hE%w@yWR(H02K_;w7R%VVC?av0%WEjU5AE-sk> z9&RC=Mo1~+x>}H(<=ZWS-SuyqZV!e?`K~l& zES~O=-IrO){&iUmr~3*qupBYJS&jxd5Osu17J{GGd`?6$MycUTgWvVtt!%I3RtG#A8|i;W}vUsa#F>|Wkn z@{a>K{KoS&n7Ju^aS8j{0&^=iNh9QFNxk!U=)=ku@uf~xgFn~MGl`Ca?=_pFrQ@U* z3Bxhtr2z+DBOnu&ECEkU8k!5_|Hb?K;+l1Uq-4bJyh&!X0FKOW9)<3V*2_Ep){aF$ zevIt5-4~%UCHVKNw6r8|L*W(H@;fRZ2SRo?R2Uxs%QCu1ztZpAj1US#me6X8j&@yKvRkW`x@nR?yGAv*v8ZK zD>Hk?OiWmAsKi{pOK}wSYQUtBVv7~MH{+*I&*iNbyh>c!HQ;T_jMC30^y{-Hv=SAa zggpi)HSdg5aO0B>fxERe@uvbsu3BVw_5jC!!w!g*yT`!Da>}c(ZUWhen2O~tat8_D zHj`_$rG*8hnXN97p#Qi}sh1GXK(q7eCcMT!Rj5S}e(I_5lF*D4^1V5Y4WQvP<32Q$pe8 z+KaTSIv78AsEqTfwc&G3zrv@|xE5@2v7$uNpVp%(n1HG-B$pdTAVEO$$In(kh9IL0 z-8%8bt3$AVi8%)$P2uiU?AVAhGkMy<5$;LGNPK=zzS3Au@wX31T@m#Em5b1>P1z;= zqrTRNXl~?1ke46@#Hl-(uHo8v%@3l#xEW`cWf`nbVM@ntIpwm1FQ;kz(?E5W&ui@7 zsL9mUNyQm~b#o9>30?SncR?z)6hG4i%k6h2PmBJ4-=^}g5<{@QhoeN{XMOLq{Z7>) zuXtgt)r?4xUy3e(BqZNuXiUBCC*0E)kw78ayJTMyh6?qrz#erD6?lH5?( z?{ni3W}j~HBV^YB+y3`v0>%9Q_1Z=N7xZxx+U4IEm8n5`446ptglCNgXFGTwcIT z%^BPKJLu-_cUqb7zd6DDUu@hSve7gsDHz8f(^kH?>w{sDps9S6%Gp=|kO`o3!!3{K zxJNndOmoF?$ekwIEiRwR1eHTRp#6Fz(xafF8>%=mtg{E4K@?MpifOjt^mM8(T{bPJ zod?%n1ErVGfAx)%e{(A4B#8C+;TMS8EC7Ur6e{YoS?OUsHQ2(WpR&Bv!~Ayq7Pnta z`jhKTCbrm=koTlcO=u(3Lltx-+!~=FFtwwRwzP-nJs&yHbLAIwUKx_2aN#S+CoSv0 z74n^kbSL-scedbO!OUtcyP|X;+}g6FlscGUH;Nx$a47xrzh}R!*?xKY4)B+X|18@OK-SV zOcz!;j`=^kiyqPNCqBIUlISizwpd=b|E<3n^4IVQo!?krqBKJR=Qt5n4Vn=Q<1ug; ztqS{81DN?{pKr7|O>1LVripHblGgiM$=J8v&l?~Fmx$oNpN$@ogMBE`(Iy(kE?9i^ zqrTj&IR-)M(gol^(Xvvj{!l_9ngJT>3Gi|-v8quU~$(8kXa}H!aj4QBO2u8D}|M}-k9TqxP3m%(E5~r z&EbGS-kq~m%{znIx91)K)f7>V?mw&mS#&iZ+t3t_x$Ce_=u$s={p?a}hILw?nk}+0 zE^hWSi3hRvQ#2WGf7>}1f;Npk(9a@zor+I}X4hNv7(CCgR$8&vR|J0TQq&{8{r$&X zIxt}#kDk4cb%UO&bP#|U7W~D-{B7^?V@L5i@6_ScjnVWjx4fIuM@B>4#|g&a(f>GA+EW0cYu_vUl3f`ZD7rF}ofp(^;^ss>|L>Ahe%c40#Sf6Cwb zKzDu=WAB{AYDDPS{-@Jw@^x@^8RJb8k0BRQoX=BS*8R>eRa{@He8QQJfY_hsC75<0 zzvh>sA8Jpw({8Fy7Vl=B^?#&0w14xl=>*_G`NBD@ea8;p+u)7@wqo$k4CZ&cqWOuR z1_z9A7r#v=76MN*A7GK$_Jf#eSe$;#3+PK_!EUC?1V?G@7-!(N; zuYE)CSD&+v+-qGMZUq-G8zEE+61{F`MUGj`9Yy>B+63_6`xzLkK#89K%h#VwU$TFE z`5#rYA)64SfvB6$U6~;gLK!56DIktV+Ni9Ms>k$)FXFyinZ7m~ed#{ET~=pih^~7w z6(9oX9b4~N<3WKtW;T-_a)65$mqk)TOh4e?=a@XQ?5_SS#CvKUj728`Mb!Lg*ffOR zt#(+as_tZZ9JS#h-~Ir*BahCxct)v^JgYL0u;msNg^-ssWy;v=gnl;?h+i+W1d2K# z%yZeFZRr9&*Ju|##ThQnu%A(9LpY2?&?kq>sJCO7Vyra+f+WcPc_}_C1F(pkc^C^$JL_ z8UXdhJaP5i{{SDza zCa~sjb=$`TQq3&l6sb|iJFxvpX^U`?9n6%nZ^o6&62=$63K`r)T!g!}E4U*hHDoVI z^`^JGC`~F$3>q6DlLPH}{+}1`j^guwnO_mvX68k!7ejK4o99_kcn!H4{uBz{nJz~u z)V-|t+9p$bKkCFy9yt5ts_^-yf4@MilYNuLm$?m5*J??Y)-juQ&77j!Ov=`4;5%{g z(k>e*xYZn+ogspN5mZTusB#0wwla7v#C1DG=G&FS~E?D%by9< z$>q2@&jH%Vdf3ffeHq3t{9#2|Mg1S;$vF<5&|z~VYnE~(`%2Ay(!==(qMZ3T1+W3# zxZ-I+^8<)8Zg>LaH%-z0Wfwi;L!Mms3a+BMhpO&UqYa-Y%o-#M>P7Lu6^b1;{c{ zj0Uw+P)(HVrRLMR8NMn&GpE@uMcc?``uohc8nKqvc#296hY?!v9d)5Yu`1F}-;9Ss zWMnJ&iyV!i66QHxZd~syo$F6z7Y}uTy8Z-iSyntfx3^Wwl>u)P>ByDj59C0^q@cpj zG_o?CY~M01B4jww0WL15{6u{u@Dju{AiA)0^38 z7C5^Gz&m-*Dy5MZiaCX(II7A8cQ$3Y-|EEWGpospaNKs$(W)EwHeug~>MnZf!FjBR z+?_&+95KTmPH?U+xMqYqdRo0R5uEASDFMi+?^;JK31=P>`NUJH%tX$#Jz&ByJ)=fE zI&@P4OUU!&dZr7xn`O+5!*vk+$2euzdeHS;^!@Gxev#45vhkCLl z(s&anpi0@RbB2W|x8RK10Km|p;uC8;0edchL(C>YcI@=RVr95mr&&`B69#D?OHPFx zJizH_B4kfL`hfK=jQxm3$0Bb=2YA}l=*_2J0&YW}pyZIn;V+WM+Oeflz-g-IHmwiL zeptT@#ZF=*H-s*~!un=z0GB2ImIh!xu9bVs^Y*nA;4b$5#o>J1AEVv4#0u4wv$m3o z(pjys%!qC%nG#;}6zJ*EJqPU%b#SuQ{HR0D8&0~(+=JeCBN_d`rs`5UB;6TQJm&0@ zT7Qvfvbr+Lo>4&BvNXbY&+u%=sn?tgIT(jj*yxHD9urzyHhR}D!KnH=sRW=QU8URB ze?#)UBEogbJrGZf`X?Q=futNgAkQfWJr7NboHT1bNyU}XJc(EItZa85#PjA z_pOm)fG>=}(8`AN*rvj7fd>#`N58%)VnMKXLFOPbK>#+F4nNVYpXRd(UUR|*)1R={ zaIRMNS0um1SdN}-Qvb9YsUQUKgM%$@bZ7-lR8_)tLW!xF(f2fQcyn!5<>8%7_Eb-= zLumrm_`G<4@rt%z>)X@PKY&~?v`U5p0y9SzM|#>Ps7@Wu9WKjF3h>oAd}`CjC}lsdO6IXKU>wC`+F{>k z!M%Doc|}{YzMqEygbe1qIO6H6&&2yL@aSG?b1s!c2G%&=BTh-l=AERb_Nd2d-r8(A z%m0tXXVRZ37}d#+)*PyH%+xi%IpaxhpwT{mI$SM$)Y#B-67j|+WF!M|v!P(Gtebd_ zGd|;k12Z$)z69O--AR_DvH>`lwn^)C@|VGDwm7{pcOqPQZj(5|X3zO@#3NpFH-=5( zz>Byu)J8jzj`~I7>k#!nZ;QwXpj=&az}!K0rt_e|EYYDq_xXficABgWw(;;g+ZtsW zAJrZcEDo`5itNeaavwR?j2|WfhORwD;5=Ls@=_GtU$zGKsaHp5QBOkml zuHmm96xY02beCF>>K>>}0;@as6Q4vhPwAgLSfJm145p3At^(_iex3udR*w3FAINb2 z0s2Jr><>`21MCz_jzs|a3fU9^yco+v$)}{Wiu=k8u>mcEB{Pu`IbbS)73sA*Q=bpFAnm(7^P)W?+SK&W;ceh;EKVN*r42bL zgANU(0i3_-;Kc`YfFJ?bT0NmhG#zhd>4~LZjNiT=q$pXa%_hkzQ<1d0k?zF8^IlnC z*?oKv=}bKkO5b)_e@b575J-7Ju#B6QUuE(*E1AyAq#zEzwgg1lZPl5T7yT5#d1-^H zCnPPRI?2(uWrs9zt`u^eczO zo4mGW#GYoDy2hsVkcU%sPyPU{^OTGmjRp6&=&`_o62Ue6iH*}T@)s367`@`PD2`$; zbAP}H&-Qo<={nzhTr|2KRMhHF>d*$PQDec63zX6#O?upDF5S>=B%04j3`Q9c) zj6u4)3o0snv`OYY5+q%9NMB=_7K93+hVxg9`#4h+`Zb()i4|oRd6cBf?V?>GfxG;T zLsf?Ry5XSVt}x3D3sY4W+A!d#g=%Rg{ruG)15_*0hUR!}S%E`?v-PvVO983}+KbrK z$FqYG#VZh&!8LXyrVfmNVXZ>a`$T2f>ydK_9>ri5%LafX$f2Q(zL?Tg^KR7>ZV;EI zPQrXI7M)axN?A!RIj&YWcHq4?0OzS70$`)Zfm&~{MAE|Q;uAR6_&h`BtE8lH62TV| zHU^(gmf8BXMO#hW?l*oKjf{G{a!|DnKhH(R09CQ+p$%?}Wou)eF0%47Rw$028cmB8 zvD$R9b-C{x+34pKy$N9uDkh}Z<=HKEvyxn0Z|oaM>?%FCN}j87V>4wJ#an&^Hkbv! zV*LKZ`Tgq$KNV_$4$Hq1N4CGtiQJsIGbKVl5H^=zTy#D)1*aowQ5ofM=q7=iI8k$0 zi=1$OUv|$OMb!;{+aM!1y;49{9=@}7_d-a^U6(=5#0qWIaoP!DguypY9^XT-xxY`b zfatJCi#=DSz|s1`(|r{t8Hrwov&6RxA`b!{0_2^v;X|6Tx2jy81mESlKAWw|p;B+; z^!||*IDTI;k#{7bKY{Ry6*)iT7H%$@{vC67&a1=w50L6~tn2h1a{m)a&Y~{%4vqKm zTP|S z*Nr)Fzi-Z8(ll790%q%JbtGC z1?n>UT2~HH)@Vb?B)7`sN?}jbp3Hr-+5??D$AwmrVRuRz3}8f%gEgD7444^nD&#S} zm$XiAD>2PvwGpS2nS!(R>cl8|e%({Md1Ax@w9kwmp@&xB?g8+948{FzV~+>Ps1_!C zd?ZQ4pI4O#9&m9n+BQi&p{8`gB_PA=Qf?P4cl@~;-^A4fFO?EIHxAqGXdUIye%H(n z7o_TcHhG_W+wHC8PEFM!|G{SJAqA(A&EO6p_9F?KGqKXrCIHyw4;4j+l%es@+0lM1 zVN@E3b&4hVe23>bh#5B<&$x+8@~Us|vmfP-`Sa#N%q@a*NZT1VEbt-TZq&+PAF#a_ z+bET`@=l%fT=CtvSN$r+N_vt}u3}q$VN~ykSl-D}=K{3V>ycel0-fBDh<;`|l-|T* zHD8E@sS!s-npPj~c+&0svXxG2T{gMzx77}?#SvZ__w3PO$mJo~2D^O!54*;HfC3Lr zuOGjV`su0dq~F9ZvAnL~Z0*oA@F0DZFM6s7fY4OW8o5*!L$)$GlHihwwZXbSOQp%C zvV`xiJo&PsR6BS@ZAq^=!%zjt84P_-DcLhl2!X1eJMACn^vwM{STUzcHz{Q zF0@I%NPb+KLBkEL(Td$5{8^i%%R`tCdI>sE*B%7$n5=|X{nxp`R+tt+n(E_ai{N+C zKw%<$sc1Su9h#3wlzvX_+%Nd~ik&fX2R`@YP0?!HtV;EC>x<}ztZzODdX%!42pVZpdiUqT>hBjNuQo-_~eQt=0K(!o@#Zx%(n~eT)`A85I;ikJ=(2N z3Lz}JQNrA<#dN>lEKgD4&L}x(#b2`$0BUW?#^f~o3DqKi3IpKT7${?@iMcuC+ji5J z!cHR>7ph*2?uCK56<-u?8`H<-j&W*y$%sS}5PEbY66w}0ZA=!mX)-S}yIGE|&acUN z{t%0Q#)&3mmsYb0#>ha;KF+n%(uN|$ z2bD%1^{kRRWn(9g?-6*l`6BKw6eben_90Y_eW_D3l-F>YnmQ)cHUeDsWb2iC*5d#i zl8dvH=B7FcOLsQ$48l+HrVX!0wd34(snRV|TNhR+UE&FE+~-Guyn=}6fQeSwXqkIs zo0{b4m_Bn!zkI)<-ivvfIL06*Hc0L?ofL;uDpn+>TuTH-{)@qn|cRJwL>Zi1Hd z_G-+d$p)1{vCXeMovZ+dwP;UuNY@Q1;emFq;ysz$4Kww*jFofA!uYZ4nlD4`I5auJ z_%E7BTE2EY+oNu3Qq6|5>7~hGi#?|g+`YNy{`iIUSBpr3(n}pE3qh{(xn}e;&=99{ z)V0$d;eSI`nsl-NIvN0WtIGaIbZ_N486f_<`3GqJ;ne`h>r_h-^d~49_Q&L3iJV|` z7H!ssiyHs-!>!}yPuIt+yTbT z<ZbPQk&@g!Em0qQ?aAVr>Qt#-E za@E#DgrsM;omSxSjg`}JA&9gWQ;C2r%r@u8C2+QW2S8d;{rZEtT=9SMb<>34D{vv? zNUeEJ81glm(P^}ehO(@j-ubHo2P^OWB_max_enF@13)*N{{a2ApK-b9X|?$)b%ZA2 zqOy<~+T6s!HB(WGv0=8=lsipUnkCu0PI6c*D)~NZYlrz!<2twKw3!J#A1?9I8IZi z9l3g#W9^xi$WiQ_&-ALGM5_C#RzcuM|BYrb9OC)nNuV)R%=*vpsDjUe8b*#jpWZkE3t`59-U8ceRc_aS6 zvY6q}VCHia;!5)PD`v_?c(3ieI%Vr-D5H<-YM1-?qs`kV170()KD4A>r@?j(-L8PQ z(|)aAzUx;~{cm#BU~Y{GxgrGdQTOl>;|F7j9sdE^0mQ_uI?bzVq#gh)19Jdk$vhFZ z+fb5uSE%}mog6l?J@nVa2gq5h$bMWk|K-9wXY{SbbI?jaomI{I752U2^?tw`COW5A#O<&kIDV^m% z{Ag8XH0Ub~P8WiK3CRAnYJTkyZ&CI8E7+wLR0!>bFw^XlsF0}2!>~ltpOsgNKFdCz zR*v3qjXNrgyv3}u(@oqXV+Hye8hGyZk9}UL>8P8js64wZW7E>m8pj!KxFDp;7&tQ0 z7fiq!ED_xx$29S4Cji#$UkBhh^Bn)-znDSx*AbYBP+@%rN>%>T3<-bhkAa3F$V2vT zX2}1W38|;G{+D+FVJ>!E)=38;>zAksD>2#o8OzM%kZwHVdGN?@Oi~prXDKMaPWPB9 z+%h*hoCe@X{O3OC-$(^Lh!EIaiU;=Tx7_5;0&Fy6hK*b^KS z(uH5--Cd`6tf}%ulSFeRO`K}Cm>w{F!L#_MX*9cKK@}i^mW4|#JIko)3za=(A=^nUS>!-7 z=H-67tbF`a!HrvHNvb<-SVw04uAFhU=J8nc7avGH{x-p>V81Mm|GJOJ3;bW~rFap7 zK%xW6hg^o_57JU7I8G}%5x-R692gk;e9*|l8qR5-Y{VF*hjYdaqBJ!KfjL-&V+14m z@xD4$6Ro1PIhto(L@tel+hWwgy7eBPny{4L^gLst5Zht0T_98YyZ`wY2Pz3C8_WS+ zz4~${NCA~-OS{mRI=d;n$eDJNbzN9%=&p^?jT$9Yt<*?*yk<8>%q$BL6c^A@?8_{3 zOiw~XxO$ZO+wev97`RWdM6SqIwWm?279UN_j7JSh86l6DcEcNSyU(KubtOWz<_T=$r|N==Ty@Rb~CKc1ApGpAa#4~n+eg6fLjZIcYwoN-J7 zz7=^C2T!}3Yn9w5tc?rViVGsHz+!pIppUKm``OZd?@v-zk*+Ax*!{9BPA|dl-YC#? zkp#sa<5gGPyYJ2u8JD#S3yg8A`|apS)C7FKTaah~u^xFb;RN;&YYG=*1Mj416fWC1 z4HHBgy~_(4{+4dpvhol9TK{6Ha-euQtN*3*2~o=u^@I1xuR(z9A5`KxPln=3f9A@l z7|m#G_Lv2afbn_z!mxdh31LL3a`w*5AoG zbE?=RnzN9}xmF0MT0h`ZJ2uJ3wNrS{q~8>Wk%2ty_6`8xTOVhS0E(2y_S2{8w`H8- zj8!ISRMnRP83cLmQ}~8?XG?5p+2I%b>SO*Mfb|9YETYyZ&^7QulxQed>P9faMDne` z3qPlF^)`{CHgsTa{gTdK`vRLb?B9w4ObYsk}70VxRPCTa`dHC=`Ozfmarv z*6*)vM|abj)+{4?_>r1-)jsvmw2>#=2Ihmje_yQwrXGgIspjAuvZ&*<8 zpK#nVTF!s^rM}Euwq7XszST1j2MxBK4|(!`m6DJ7I&~QkJzsX~aTQ$pjVEHv*>q#2fk{%tU*=8<#p`U{yVw<0;oQg)84s zdx?*OmHm`6ZVWe&9=DJ*eH|1Bv=!$6VMCaR&OycW!U!qVn&#l0GiPS6jqXg>cifvj z@*)}x^BJ)Um+|gzw)eiHdn+Xws{`4GC=LZO65ejNAkP3WaaCbWM?-tif$R5b)}eQ9 z9>jsduejt{&sl@z^SV9%aZ}cF#e+UVZf=@QL>^$6EmTXm!wv_sW(K)bvXf>D#92yO{QGkZ2mG@l-m0P|Ds8ogzrnkHfl4b4>dgDiH8o z-5tQVg!Vq}VH{j3Rsv003jThL z7=$hd%yr$YYFBs38*D9^s&1exFo6Wse+v(j$!#AxhaA;>tVyIIi3(&J4b z?V_9fPTgN1KK&Th8K@Le8M-78=2I$9oKDW!?=ZET&tuIL{nGbqygsdT!tW~_BohH$n)leVwnouDn1 z)|!#Y;bFCh=OnOgEDD&FzKGNrdbI?Z)|(Xo5Mnq`1=Q(EQG&OE(@T;8a9o6nJdtk!ehO?lR zVQ_LxUk#?0sP0ns`=oyg*k$3^`}^Rbdwly@Imgu{I>M>w+iT?k*$52wOi_BTesS+q zUg79UQBH~JUJ_qT4|`qeN^~vt%Ht+E34;Q3UtgLm@s$Oliwq*;RIVYZkHo97b-%8&EfR z*Ubeqqb@+L>O#7AajDtgZQ@uv<|Ea6E?z&(-5jvWHtZMitULH3*k z1XZp=KOVV+?+jW*QLe3%kpoMO;d855uel~&?W6ekV4m4t;`fIm??d7P4HPT(n6c*q zIMCxen*m*q5`2r6z{hA`nPvX;bQjK#KG_58o+rhkj6ont*XM7CsL%5u7JY&*O2C>0 zMh)>5$LhXVO|b(W9P9hcHp`7lFa+OC>HW`A_Qb}K#hXM@9_;k%ET~4YB2u)wQDVG?iJ_{vSd)WDs?3#({9%23y z@kS$7_x=C`pl)MD`ou!nTHlmb?ParyPA;KCROhzaA`D=6w}CR8us7>Mfp|V#dQ7Bp z)wNeF$mQMu;xAXydEhzKmuqxQPENe#p{fGbMtGdOTq~vN=<^L`U`T$-XxN4(nd9{S zPy7hs15usU)vv22#tOto&D%{X(R5qKzFWr**qha%b1+g7!J80eglJT|g|F@AwWf|@ zTb5g)sLp!*_qgI=2lWq;*AskC9+eOio>hsovloM5xK}19yFtVu3#9{wbB3$7S-mvL zWmd#LoHNQ)xu(Zuqjbe^=flt%5`x{`xW)#fU55BPi!n<~p5F(Pw{nKxe2BZAEXFMr z-{9YV7j8f0Doi{1efeoy(st&|@}Om!GE5|EbGacSXNMy;ZROjbnf>8=?{?~7lhq=JRPoLk9jkqT&N+>a(t?uMj0>R!~4|QX0iqJsL zrp6s)#ifwID<C0wr%$M z)=j4444)Ith!J094>e6=pX-D&6kb_Sw(I0W1`0o-!LUuq(9X8Tlou)5j~a9An%XTD zlpPl6ZQ<|3UeStQA~Xpg&&&qRXofG=stOhxW550Y5xoj9x5JX7@ZSLKM0;W=kqxtH z`9&+cb2`RO6!g`(evl=}QbQg=A?p6v$2^=tVp_XA*oYjaF_H8b87@X{hX;rwoZ`b_ z&BQ(%LR37#CUfd2q`Z%Obb~B9he8K{m!p^?Hi?=~mrJVc6TUkLvOca+s@{f&#_{toXu2vlhk9GZZAUsa66W2O_FVlwM}Y8@{KOJ0PCq{GQ#~YSCZ%`tl&jqvqx@3j!R+Jjqj?}t4zn! zN|FcMxie!ZOkZBo`{0cdA7IOVn3o`p@b%*=^eHyc^x>~FYM&q!j&t7ecx)Q`c6cjS zQgw$KNJxmG{opMWZe$Sar_&q>V}x^aFMCJ0L26h}aPSFpJLEv8R9MJ~zAU+2;Dijr zL_)E|&&2|$IcuBULx;Oq9sj&#Gd;xr-8qIrEJ>cY;G{(vI~1uLb^w({mT&Cz?=voM zBFxlHCM#X5w%W6<_SN@8aMBJK#S=!4KY?(WV$V2oT7=!gf%9xh&-5kyu=*oU2^segv!W#|`WG{BUoM-I(9i zFWexzvCrVSBvT1rl{oJGx_!hM?4)AO-I|!@yGXAh^njx-sX6(T6aN3f-dhL7)wO$~ zO|S%q;BG;J1-FKV5G(|U;hNy?&_LtC9fAi4794^!?(PH+u7Tj)5TJwQ?)S_+x03JN z_notpyBx3=K&V} zMap^nMGBg?L|6)#z?>g-F`9&Nmcw2qu8F{f&tg*6z8fxedOSEFPZVCfH(C~T+@0P9{z7 zlquRWo9vYsKt@zcn*G!J=P|QxbVMrLhi47gq63HYc9V2 zJZnxNa=|;Hdd_ccY-*WpPnOnpjGbpe3neeuh_r%d6J9GLS{tjfHIa6$O@Qb=W3rv* zHe1W1QTqjJ#t1PHEF~z|V47DN)7coCCa*M5|OWUgKoE_UNrk1NyYN zzckAnjLFOgak|OZ6MpOv8U~i!z&AJGUFTzu(xFjQlqI!Z$5Qh!QixM-U@l^9u5eS&7P??3lI(p~+vV+f1_6%=%p&hf0 z8TwyZ2;J806bNnL<4k7P=mO%HM)|m6>2_}eC|75F z8@>8HLrcy3mQMa8ork$L8aU1(U0TMSTgFama9{yGXpz1psrhnXJi+?1@@?`2Pbrsk zM;FDL!TtDA3aXR-ZXb${Tbi39VSK_4ioQ*?pf1_0C4<&2_v+R?lCTrWB)n$*PmZ|u zw-2ub8r2C=_dDtDyQXyAGEDslClLwOot)*Md@@3p=*%Ih0_t&P&w|f<`3veN^RKdC zWasQ5kS*Owzx_$V8*(^u`Co((0v68$V416QKPafB=np{!b?s{pAg*sYild(pa{Jc|_J(HH%2VewGZSt;3_BHbw6 z*vibg3k|%4{T9vM=Ir6|;|abq6@2F__40(FK z7w-2>SkGm{(V<5AS5LgugTcZ?-YS8^4vrR$7C0|HSHe@=#Sd)0@U~lwgZi)3zFzYs z`xyG+?$Z-B&B7%Cz~w1@PO;8Pw+-EB=jlbwL4&i5aY0~a?+f;mR)w86aY+i4-a&h-$U66SfjsPT$(PMXdTF0nuQk^& zHSv(kj-@R6ZVFA0?Oxd{pqG>e{^CT8wO5Va7WiC2zQ&)6NM9)zEm; zsqL*`dc0L3jSzmWZp^iNIW*zQbH6(ow*cy`UO#z|^zFT@4T&sKf`yf-aoC+B4|8|1 zVSd&VRqN@hU_eRmPP-J0Vq+W|N~3a9BvcueQg^QP#pbf2zzu&oB0=?&(e4*qft*G& zw^!6?AL`K)BW`dirI3J+a`1}C95+mLlklFE?n?P5JMuY??)EZmh)5B)L9C$o5yVqt?Pl0oD3llY_ddA(%Fmq#w6o=0#5?wVG(uwJ=G4+P-F zmRM_5m^L&uO&siGSxD(B(=(6sPbb(+)gDT!PYPWbuq(qOhWdtBfwMyS7>>8mkMo^! z{Yy$dVm=3fj9@0RBI;!88Qfh*@cfm-dJefp%jKaB4 zefVNVX89teB+lkr_QK=+uJ#3n`yUZ2$%CTZ_{_6itIua(dM*W-4=oQcW>@E_Vf=%v zlXL?fBWt6412k4_?K;ACxKHLb>p~5t4$mkLj+mMQ?B6KA(-Jvs!H*bvotT zBC<)GyCu2iMjAE%t}C(FmY01MRl*G|EmMa_yx`=QpCAHH%okqAM{Q!nyqn>?m$&W! zIF=}(ZjiAr7)b5;;F1uOObff$Y?YDzLdRT6v9sZN@`}7?4z`HSR%hW6~rMHAz{&`7^xMvYI^WJ%L+Bewx>C!yL}Le z6vXq_e^Nr}uS7M5{S-F9?P~xNs6zS$0YdBQq<~`cmjin@Bf`C@rG-UQ(`gTdFKfKe zMb?UYZ3A3hC$I6fyPH{uxmqe&ID&evXKEQnhf2)byA;W+fQH;o?^8#uJZhO1#`ls; z1w<)T3kGeOb>b3+0+8s82K~o#d-cJ-q}PuSyagU*DE2N1g@7WXKwe&MZew`*bF~e+ z4f(hCb}%-_f>#jdqwD?t)Y$-&&P1jUrs~zcb>M=^& zRfRf3QUjWA;Sf^*ut^T~-0)7l<<+;=Ey&!HE*4P5o7m&S@G`DCed4P)A9x)i7%bUI zqp-R-3_<+}y6L?I=%JOJenFS#^`0B5E*%T>d8O9;srL3qu8UTMSt2w#rtzEHYn2%M z;)V*rx#r6SV;sg z7c~ZsYb0|@j|-C1uyBPej1AoT>486UlHGapP;zxabb~#>m(_Z~uUGF-OJ<7CVwPk@ zY^@$1xGf*5k~K=LO&qlCw&N1i*#GpZS^tYG_HAT44zK@=2vq6pSa#v#K!yPf` z7N)kT1qO95!lC>44oqU|+awHqeHqf7?9!gWD&bS6S#)ATllr8Ds;W<2Y$N@xv>7nC zaWmj&86{@nyHwH_xByELfRc{X690g{X-(rfY75dD_OlU$8Vk;-VX$lkdz1#nO`T zGU6GRc0u`6K7h!kivKnkCul0Et@~(cS0!po^UVHOFOE#Y-SCm6 z)F1b=u2>b1gN(%6Yj7;8EosdF$LBVP(+zoeYjzI<-fkOf$V6mUdUU?lV>VmU!u0am z4|tnY$C~9Z^ff&WS3MgD!R{CN^*)QqNl7IQTdL_&8ed*+dFxXhbj9Dq;yB6@x7z(bC2Zl0N)vj zi*UDwTR~{X2DbmT^0!@Gv$@b~9>+SgAElDaT8CbdW2The{m;=Kn!VA`&m1+%OY|Kx zs+s8>URi?i0lO2D#`rBelHSbB7!`7U(*#fU3Y%$o`F(jLTasH2G@@(u^_0W&eyQ?l zW$=Bby=q0qheJMhnX?}qxMUGe+reDcxM)Td6`Q6G;d*kkv(z7$xdLd=X;>$)j715Y z5|fzQaFIKql{5x7D;&&Gn ztA%inM^NA}Dbe}Hw@c&d(UUQv&d6q**MVj z{XSnxgl~NQ{#^6*{=OdNn?}D!-%gns9X^~aUH2!Z|9FXhr%ZqlgW4_-pcxWet2?}F z|Ew6$^2)^F{Knpg7Z;y?z z$bJ^Uy{=tm_I#+)Ho(2BLl88828v&r(~)~)a$4A{O90@eNTfwjHBV7Ho@>!2>Iq{f zKGJvX%2{d>7M}$(CLABeE6v48d^Bd}_)xu7&ocC*Rj=~3F-dmue|*v-HX9;#rBFq>YgXp}jWU37CcN zX#?>(AI&J$s5U#|nt!7`=(0WX8hbOE8;UL0{CWmPdanFNq|nm}rrP@<@lMKf6`_wr zfwYaF1dYMUsfE+h_IC_8eAjvmv6V2IJoOFjOjA|W1D*whQUlJdr|MM-t%$QEwN-dF z80I(m!`qJxMmAR_H1OkU*`xo`Zgl@{5__`7WF^ZhNwj2F`PWqJAdpyzUoAys3n!!p z5|qU*(VDpv+QPS~TzmLdse(kQOqjFw=SH1gLVj45xPk0yU_ppeywMwLFM`nWVH4?^ zLOr9wf+e2iJpIQB@YbPH=22qDr%Gh=aut+VEMja`_P+EeoYSYXLR^*jyVbjGOle1M zT;{d!y1l^}9>b0q?!zp0`Za<5=vkPO!Lfj#xFI{@I!p2<2WC*X8@^FVxJ!R$@<*oO z$~|p4kvyrd!w=q5#TmZ!;Z9C{Z0ZN!80f1GX>0`BI~dz#hNNy=v7g3K1j$D@qh zHOw4%+K$SDEah*Z>T5e)MreRJbblb2BF~ay&ibX!0XJh^(nQSDlxWgJ_L3iejh6O} z@qYDpfIfIzC{j}Ob_5tYCAG8kXiH9$mM((znO7(U?n|z-xh9f(R!gS0kOq`%NZ)mN<^K z9FuS3G1+87m6)hFAirwZq*2(bPODd#>F2c7mF}L6WukZU_*ID8J4sy2)Py6)Jo|$~ zC=gWdwo%2tYbW@j3b;ZVSb6#8IOOglbs`(=p1i1Bq}S^iT7*xq8q*4zNuixV?q8Fk z#5a9(&VGTYP=vB`XX0cpD&Y9MvwQBZDK!LCX!vo&(7{?}>&N&gk1h$R_D~pS!NT_@ zjnp@o1@G_KIS{$?E;2_wrKe5$I1;%+)b1E-Vkq;Z-i!9dD&{k#-& ztXN1Lv)Ven;6oR*cK`1CA|gc*`VBKmi=RX{+u+p*p;k%txi)dsPWLYm9Uqvz+;Snv znq;?1&dv(Ie1sZy?`})X348Jh=Sx=N)Oi!zxHQ|juTHnX>N~B)})M*Ja3llLWEw!+}a4-D{C`Dn&^zjLlNuJ2`lM0xSHS2Xc}!F-F2+lw-PEOBH4(S4XZ85sRq!;F3w^w-qC3txPkWDjr?3G?3^ z?0gds=;z*nWWqwvGZk;W106^bdHirQK;ojs%-(WE$(h`s(1d3#g;m+aw!8^h0CikmAyo-ZLn zJoSL@w=ZIUtI7jXm%qQg>-!fYU0pYY0Dvni-fy+;FTukrwY*;-DP}-O*ABpuPHIww zwy5MnD~1ddWg4$ET14+JG#|M3%uZ4k=!?BNX^Q_~e1-v3pv=MNA1*UXkg;KKUu#y_ z2V1@IwDJdgh~(FEiJ$6V6(JKBPpOjTWd!gaCLL^G4ew(PCv4zQ?- z<-Hsm@d06^i>GS0|IrV5zc-2gMey~jT-3OHP>(cyvho3M%{OqIioG^x-{WUtD5;nw z;(>vRz!-D#zi-ITwMIa1FlW53pE&ZJV2gI26*V!bOmYrO5P)^qcMTMB#gFT61D6nO z`>hjQ|9JBK!09$N6uWZcblEYSB+-h(-CgwQ=V#lJk!K3^>PZA1p|2X9OS4mBY+CZt zf`k8T?ukDhFF!ZNToU(>ul{&qWqH~W?!W7Jl_xoYYn`J`3=@2_F?XzYw@XOu>oef|to<+Qm*t;D>u6H{)|VD4zRI>M$r)u&n9M;mw^`VbVENY~KG}6O?KZPah zy*8okpSAzjhw*3fHxtK>lji{${_`J3lcITFxd3dA*}2>s6~2^os^dN8mVma_Bvi;Ia|fc#m^lm9QT0un-g4MT~pso$4hJElO@ z^{dQ2I61|raG^}y5EKNx@RaWegV5iiW0g-?jHQ>(e*s;R+drAf_LUO>EynQk}+L1UrC&^Sf_r59dRJ22SY) z_6F*E5}wNRnBF7)JQ#P6#vhQ{0szyZe@wzZr(^|T=dBdNqK8IuRI!tKw4%|fa9P<5 zJezROL?tXqXLF^61ig4IK2W^kRj(0U8zG9Nmk1@;o5fu?ErWo`=`+5=@Hw;eJz4AAS+uX0?S+F`ZRp5S6;nHIJoL%d2SH6WzT5r zQBep;OMU?x=%E=)qe@IbK%O! zi?wNKF$Vl^NVh;#)EJNNkSPR-P$Z(_{}%4;>0Q4QWf5@zcGx_|f{A3#Py zj%PYOGI~qJAo<*b+ffbMSY5{IE5XG-v}D;2w|Md=l1cuImqGfmp|^}s_Fk_sIIv~0 zac|1XTknsNQlRmC5^a5Q^tGC7jsI&m)xbm9PE!Ble-3#_K0KH&ihmPADjImrvxyIR zIV&6YpylPx=%ZhtGNMMb2V8;b$)o=FzH?2Ul{+XRRubJRP=V)kqH0x9!&1FHk)$t} z&&=H!(ar!ove7^G3{;{1O8%Mn|zbX%N*VaR++d~ zGsZvTFXH|AlIGhhf9Joj&gFlud;hu&_CtXKeG24CupQvuW}ST5m*h^E{^tHGf zFD9xmkl-$uIQ{$Xe=aV~#jnD=QG%bKJupZ+GA08%4E(KLBw7rwrhPd)@-njV-gbB4VngsJy+Gy74_O)$G;)*}G;~hfhu6Pkf-_Qpgv)+jW8?DFQ zv7|8MM8*_i#pln1%o-aU<104E6x21}%4krregLV0+#&zi)1HMvz8u$5sDO|9sKU<6 zn~H*huQdfDdsmpv3Zi1E(z9VD*Zojhm#OqVgnB>Z9Ru;1XDtIdw7Oeo`i&e@;5}LQ zXf@nE0&ov@;9#7T)mK8(e|$Bt8N2pGvUIHCxu%|t)9*jBoOMxR3jf6JT3ye?B9FQJ z`CeBb;9}gmc1(>@-yjHZ*oein{%k|7F}9XMe%$E6+0A)WRv1`@M%T+I>F~!}!=LXE z4TQKbS%iA@v9LGc-F=aT+DpvN+IBsU)pO5I#=b45xYs{^6hhnkZ*dS9H#}G#U06tOS@O+R|AlVAf z+R>Dg(p=ri|xI_2IVUuw=umwmD#MF-Q|V^nFOljB$&|Q63;j z{6k==BnjF)?kvrw_3Nq+>YXLrfQXj|7sZ`Aw!R@T85W2Md9+y5C)oQ6>32v2AY-MO znh=FiEf&Bjb!$qpd0p0H0JtU}FA5@DQNWXmmP9-L=aFHeZKP|REOMEg{czmIgIbj| za;U`_G}Qey@NOmVgtXK(6_P!YFrb!?a<_ir=px)M_k*A^tIkS%{j$40Z3&5@)6;(( z<)m2<#A`!=(DluI&2~hhI=X(vhX+_YgN7>aX^Vc02`)5U{(pTP(!J!(D_$`GYmMVF zK2}VjVH#6(KJ1rH8rA$eea-2APcs19Y-ZlVfAdIp)*8?_JMLxpN$dvsH;?p$w#Od{ zgTQ*o`x|QSo3aCHRn@HAR`1z(eX4XG@lX{BHX$+W#q}!7-4j!+n3O8jz39K?K_<7b ze1XY36>A!Q~GdDpm_aCt-qYFEuI@QNSIbTx%`ve4pOwu0}ivmzC8Hu*mlo{yDU z)`@Cirl#4&Z7Dp@Og&?g6$2%J${0|u+VR`hG2tj{q1W_uRel01t0O6{0uEN-f$i1p z(hzgW?xU@!jmk8@CCrG-zC|_nyAiQ8IV6nNZBYsgHNWjUx-Od&Nif)pXOZKMDBZ^U z7%z$!EH=1r7`#DKN%0p{IiB$yM?oh zNH~urYUT-bX8EGRY|o(a1;-cW+|(fn6Y&S3E37-hwX5{d67~7h_2aPZSvHBf;&LnZ zdJ4>6Aa4QXqMy!v{cX?5SiVqSx?K~lds1`{oa-3jRM%x(4MH3>@7qHncWxHReqMN; zZ8yl@WJ?oQd`hqWZmL4Iu}7@Hd-RTInLE%ygaf1PwQ0lP*N?<;3X`(paST%XqJBi| z2oHeHO9Y*JUE73kZ!F=>u)5!WE*~;D^Ug(K;8{dRQl~9PAC^njv^Agv)I&%IT|REH z@?6LD!|F7hb)QX;r+r$pP(Hl8TvoK=Y8LaQ5Qj`@&&BW;h`l=&OXcJK3RR-oSjL9c zk-acw%dVdv&kA^Zx!u{m|H(saW3K)-)@ueNVK_{KpfP_}nsS!0zCMEHRk_~ic_dq; zSa%PlzT$^R%1%Ka=vD81lsNYHx&{2$0u3qW;LTGW#VY_mL0`?;p!@UEP7D^7Pib!( z9V)2rmL6#yYKIimm@7YP)jE8N96h%g#np&YILVz?#uhmME%}!uU`-|1EVtZ8ICQO$ z0&3ePUfB!ju|alhaCoyb-~qy5Q@8e0wvL!xhEinW%T9WBw;x6u6;x5XeolyvvD?LL zn?@sHd&ANWwjO!~78pRvHINV{mv(k+Mrg@ESG(`F7;RMEMOkcO6(Gqs#;$amlq?_@naCRQa{Wx>2wpJE|}jmG>N;4V-7@o?pW3C_h8} z;Ln`%C*JcgCAwtxR=4x=2W6L*-C|yIPhBfzJ*@rGW!UTNV4^PVVna9OY_9T(J+9hx z>yeD?mhq|H$+ZCD$qHFJ;#s=AtAU!Qy*6aQ%eBCYFiYq5XH!q@47n>x)s@ z`Y0D014Vr*@_b8W=e(n$W9 zY@NxLpKi?=Hp6Kcr;1FCHDPiXRZU|25(;;MsPvSybeptHv;^-@VV*?63q}4X3P+&Q1+^TcMXyACw*xk+#-JT>GkVt6Cd^)&ux!tp zSCxQfe=tGyHs)oSXwo1?+aq;`rvTI*;(u+s0BgF=NdsCOSvf-^4wqcAGI~MpP zE~o#YcFc`uX_I2N5&CzM?=IgLUc1|^H}KjWfa@JU_q)Fy6FKTm!?~l=E#mBw;OxTm zb;B2XFMaFMFX9?fxtC~zzn)EGuC9hM{4jw(ZM(*SL*@@%xfYaDHv3coLH3QByddaTtY-EuYmvDBNgI}$~>tPN)_d6VEv|^U2 zcc|T1pdJ6@~bk2{pF;7@e3MloZ z-p^y9?R0~edwu8M7B-wGm#j)6?UfABI z^R~#bRL?m9rRtg^!ba!sh1PK`CX}Oq%vHy+aJk94V$`)t&fcxiv^_u~mS>&CU&*an zQL*x%>yO`l91(_-*%8)uw1)lk8;pN5`#wsvpZr|+C})F9iuy`s`uNBU$v4mD=1mqO z>xLmRc1-O*+Qha%2*L}BEpw{`K_NZ5(X-{{yTzU^-i6Xh{P7=3=ap_vp1g7&)64w2 zBuTyHYUB^N;QYXyD4Mq%<@r*VH9XSP)Yxoda2TZEE)feI+R zh5!l%3IJHxE33e7gHG>RRcmbnbYbw&Vfzso@jL917a^y(BzjU|=(~IbFl?0Gx`AQ0 z(L`i)E(pd6pwY21;Dhf2iaqzcp6@QYRuU}f z8EDJxJcFf`GmHe+r>rdMeGq57z<4ndZ1N2sVvsQWWae7Ztl0Xzeqk=AMS%tWB{v+7 zJ8-xYzx5>DU3*P_J#!G761})=S#kpsYk@dLJv`N!7Bb83t3vslaqZOmJRR%Pd*k#? zXu=_M++xIrO%JW=!oPn;qOj91om*8B| z>223RHfqz3QOiY$Zk`J# z?f|M73AWn={F{MwOjT47sj(WKi?|3rJmz6(bInqbER9T#N_$!oYN|HLLDiuhAWp=u;&AQSc}0S>2ww zkj|M0@AYs^D7DOt^T{EPs6cUd*;%?%o}SH1+!znQdY_7Yl*{ zx-<%RD{X{0lC_Iw{-_{9fDilk1p^P1k>XkU6x8*$D#?KGO%T=B} zJtlzyR&0OG#Ax)Fh@J)eLqW}UJGDIqi8tE@tzFAhqS#A~X>xWiDWXgqNZ$hv=w8t~ z(O`oDK>8viOJrII7ufMoCw?pNVwmq>u-JQDj8~yz*NnPW$!%&-kp>g6 ztLAPR1B`|&4&Es3ul9^F@)0i|Nnd`5A&@shdUOI_BfetO<#0b@D+OR}@*Sz}1fQ49 z{v1za3fNqw_Y0_&Kn~_^R3Sx!uJtxa0eyczbjy0`_ef~qaZkNg&39Eh%)_~PzI-O* zgm05#Gf}~-*zRBMTvV3(Vi`iuRcUmlz;1Lq*D7%Am@Wea6)V21(Rdy5t%c^Cc*@T_ zaoWZkbrsTguOBra;GscSVN8>?#orohl+4pP?LMU~C|a5;l6=xx3^wNPdjz6$cn2n1 zk44*6&4X$V=;$ePjk6hiOVG)t$xqOs%2E`gGJop``i@B;r?h988xaq{k!1jL^Z)qs zFG9dZ0{@caM?1CX7Y7@Z!VFL^*7yifS-(Kt5*}fG^=i47-ps1p?#UBM5#`JhrS_~? zi%A$s-NvzGRmkxkKD<5mEc8aC>Ul9%F!Gi9m*n%^>1BN@4=<-C@;QNrMTsP0G-1QW z1YK>1Ghn*)R*Rl=_Q*3Xh@=)`t%>~p&jh{%J|acPKpAm;Nx*4wzb*Bq+iCzH<)Biy ze`yZSi3|g)`;iy>T>YF`xx`S~%n~P;K{Rn&qlFd9?YSsN^bfVu`?5h-Wm{2agrV^L zNC-*+CiqeRYgm5&Jg>cZ!>IwJX)H%@k6$+tV5XA5+~FuiNUsq|TXZ?p+0Om^b@DQA z+q{(oTnSv0PSwT|TgF)uoUicK2QF$_RaTm(pG&%1 z#EzybFQ|4j{_MtL!2u*my)sae-3h26hA={lOy89*0|Ciw0X6RJBsS=E>x#}ue5pNG zg9&IYUL6SY>^G?`FE$_)Fv^KD2>EJVaf#m2o;%6%0GVl~E#D42MiB?J^76rw)8OE- z;f?hVqnA{0PB%F4@Isi>(Ks9qU&DBDYJ^ZvL{kbs*nL+bdE1LS(AbxL{ipjPVPu>4d~ z!jyW5BvfwTq#@?P-JJZmn#K-b>h%}1o~l++1Auj=-)A0!fbV2_Xe6^=NILerYgxDT zdL%={;g9|141p}C+Jv#gE%{Tj6n5i>Pva=DI{Ub`Z%kBC*>6mcW3z-2zAPuV#Ionh z#8IEAOg)8qrz2Jd-Yh!@#3q+~MRS>skNsjq43+H8;%b_35Wa%IRoC~wO_%A7#DgBaN*a0CP?0W zZRDK}XVU&D*?o~Eh60Y(j_>cL+)B*AD9qNS7-W-Y@yV6SN9H8pVJ*o=YXdQ+(SRKQ z{bN`mcq>bEeWyn~3=l8CU+w0@vdQ9ZMYK??&`iTKAD=3i!W(DoZQTeDP+dv`T|#CI zmS*x)_)&I!(iKRDgGGc*-^te{o6GxYwO1TJrV|#yW4)(b#gcLSzw(UHjj5&4;YY*s7z79>uXWj1xB-bI{J6AN$7R{{A&xg2{n5LTw{T)*(wW*mv3*ZZa!w zAI_uldb6mInQe`Ll*|(G{L-!fG-11IQzLw9rJh*n{FO=@cS)@+ie=l-W`YM|K^T}U zjXNVvzW_C){DJ@W(Ba6gWp~Vhj3wGa881XBAD85MqWVMNs%L6e_WECFFBnhEHB?Z| z#_CK%a4?{=;x87t};yK&tTMqKCqe$Vt&G2O?;WFm@`ZlbwUTbk99|_k2leKDnV&BTZ`tj9bdR zCc~QC)$eU~DPh8{a}Ie|@*SsejuCnDfjF}MM23NU_TdXp@lIsu!8*dbKZ`B9H;Zk8 z+I#Vs<*=A;W`rCts5b9UJRgSUYuk$$qiu6Jg2X{MrNObWFis~VLd^vcCP%WcA(Ue)27`gGj8T4WGBG z`btAIFG^~2J$ti8&>ZUIPv$_Cj3@YB{js;$Fd}s z?J4nax~vxcrSU9*+UfEoM;1&oR3CTo@QMj#N@~>4wx}0ldA?F*{%+){EEJr1e3Va% z-_uVl-nB*usG(X{hWp~8f+0QM!A71uB`E5leMQ7}8c)F>1$BmhN_u2NRF6nhr@an} zW;MEFZs~>ZKs%bEka3vfE%oQ@a_DzM=gyO{~l>+M|fsav;f-g$|z6pmDgHq}U8 zo~NYDM(a`vW%PF>box5iDX|(-dmK`E^&C)oP0pl`b@&7oeM0_afp?YLC-w#JT5M{Y zD8I7``Bs8wCd(m=sh8U$5R%~F4^olToJ>(Y4W`!3()Oi>pl52{-&r;jR+ZeyeR!F}k z;o-G&z^kx;cHaEtXzqNA?err#7;&{G8tZCjKPMj!Rx~Mq=KcabZw=Y-JYtG9jD9*+ zk;tz}oHEoOL?mWL1)8mY2aLPeAo_bq;mU|>sT{DDmsL{P+vKC|joD6s@~Ume^C@ZF zzs~PlIvxQ9dc*}Qye^tGh9imm<;c2>!N@A~gNM|(E)7O(q%Ss1ZpxR;w}+k-xm^_E zHYKF-Xt8j$u`xk2<}MLl!sO6UP|VNrNK=#NH?9m zuTr?A!z4?k#0G~h6BXe+r^`o+@95r^H5miC%D{f(N)6c`dgddk2A=~+!+OI!YR6B* zI<1NgJe`#6ugmIFxbljUL}?`wvmB)}DkM9yK#0(82mwGp!mt|rWc9gbuu3O$MTfvp znKiCvkcFY;s5iFqPBV4#qudBikk*dhA%1@DHPy^f8@=9lQ9o?6ChDwqp6KquGIeGn z&y;Z+mZN$>&*DIXiztDLyU!s{xH{Yj=l#~)iz`PrXjbu$yi2Qmq&_w`8Ay14mh+ta z%AGs2^m&knc*)Z?%R0AboDDUQ#e(XHvuDP79O=tX%mLnLi3#f_o0T!5ZKPjr8DTIVReQ)f zLkof}<)*d{^QB4AxKwXzJx>ye(6b=kyNlCgWELu)Q8nw8kqqeezEXOYPCD~ED`|NQ z1SuqnMf%#+7_B5lBMPL((&O()jN|L=e1g{<0wH)UEbF5dT}|#$^_tQw+zUDF$$CF4 zhk3it!z*-^S<#@NI#Sm=pTSgD!q>D2`evIARdo3zIKDJ{;E>Aje)GB+X(W7d-u_ek z8%$roR*cqy_SH67GmLp8dAu zIzJ#W$)K<8#T*<$agB|VEH&DH02AM2h{|G3KXQjhsATvNKDcBaW;%JD##&z=`QSb5 z0>AIIdE5JGKpwg>ps78yMQ~kjoNz>F9NXvPE`N8i(IULK$>$`VRrKzGPBY-i6wFA% zH336Mi~}n=o~?cDDz>Z(21UX>Qwq@wj{|+)75XEqGzDyVMvp!#13c8!xg0=q++8KR zWrtF%CrycD)Iaf&0@8O2+IB+UbFy@(wWoFj`MHho4pcyj!b`VbSI`jeR_?T*;+;PA zi-xT?A2CG&bz`;R=Li~}*ewxY*JtORe+dH3pSWXG34(+;<$ z*-lv#O8vvAEZgv&VeEMhOL++eG*g^K4Gp9?YlRPT-e(Ceg=w zz*JLDLZ0UT{2D!(F~Sgpazae4ivyg<(;lyGebki96dc!w=3=U^{z*@H(W7AX zF#MDB?Y7LDA=JKO4A{4fs3vuKXrXdbhGFtD5B1*a>uKA?fv9gLi#ARq!Q*1+iGzdN zt$bZ8_=w{mAr(SGOY);+0AFEuI{}_^VN4osKR%*Y)?>|X*@+_X2k8^W0v2HqTqs~8 z1u;K^l6u74;hbDo=2(IxNNbOKPJ zirj#7S5iamaV>>KS`&u!+}jegK2McziqzNKdwa!3!YHHRPIGZT3g^s6^jrajo2Q@A z$}Znt8()Dxbr-&DKwN0PG&aB|Qe`qYAbza3B6yR2O9;K{(K6&f(#LWcTB}g6mctKc z@+tNSi6a{rlUTAXH+l2pPd@kqq>31e?kpjc;^y!#5Z1LKlCE*3K0Ng28XxY_J=%*Jr5eGkfCTtqUG~Mg=5i# zg-SO3aHQHkWyn1KOHkse)+)c1#A_mXJNys)-53yyXmVkkIAcN{8OnS9SQGu|5lB_+ za}(5q^-7|7Xrdwl8RNt2-c((maI|MbBN@V)W_PfaRKDS;{O~1z?MZ`BA}+mFDFE1~ zzk5^aOQVl6QH>efF@z)CZmu_ZghzW;O`Io{dTK!0@AMDlf4)ZR^~u)n%K@UBb=$*< zYdb%S?R{Mg(dRn)o(X(Obl+tq4m9sBUe|tmA0L}SCBC~W@^69skAv9Hr5=A_`+$wr z86axRFGJ_^e`jYHe$D**xomEAdw`qpyE{9B9VP(){M+9z2^M^Ar*i8l_Y35w_X`v~ z_cfaYvZXTZcNlvc{q(0{1%D$?s-O9E`_6mF#aTOQnc=t3_Aj&sf>OJ$L<)X^z&QYB zvmD3_fB&<%cT`wnmClo4Ez!FrLS8gbE_A~EA|yVLk-G<^^YvlWi4uCE*7q&hkvBIP zfQV++a{zY`Tn&C7YHgjaU= z%`|b)C{Te2qu(C?_ogtYn-Llw9v+n#JE1zNJ(q6`5hu$J$bbHw^|^C!TnL&!U@`Y_ zX-dukcS&EOm-MO5a+rq3iv(pb2>X>HGl2=i!9vRQN$Bk@fPkmT`@K+HtLpa{$(xm4 zA1X{^r`e}qrgO-qykRZ}_lFL?l*OQj3YY(N1;XF|>5nUv59OEveUP}USZFBcdM-%g z9ULL=f;W^C<&4R06KwWCjo_}>cFYsnUcTR&^!Mh?ZALl-yZ+>T+cs-kzA|QMx1+<$ zB1d|`NNM7zZRd%8C&E#iNWsMb|9@#8HNOF#qGasSU`sU8PAK^#)l>(=<+1x9jUBs9 zp5u@wcHqOC|D_dJ$^{y!k6|9p_wyU!&5gsYsN!#*_1?eu@6LvO=Lm5k7fQds8AbLl z{(C*i@n1hv{zD@e0f4n9GppT~CMBIAaQ{;-4_QDBcYg=x>V7xF36B-zY!~}BQa4L!?LeH`B|M-u*D_ou1!}gmSzgtm*QBt&Szc)6JWnc!iSN;`<{0t4X@pD%bjQG$|&}FSbTc-HGF^4~%nBkA= z9$9`Q`2XqM>)aT~p<0SFJVL5QI@MLv4#;yGt0R^O&uT|-!+emmIxYIB>wmU?CS+*; z3rENQx%>a*yv!UOZOv>+(Q?iwdR&mDXk1hx&YI{CIulk@niDc2(ur1-o#r{?digZG zyg)f`Ng{ecY2YJ)&EHM4rbQ`5^=a@g&}aQ#mvbv!Dp;Y`$+u_r3JYqpR8-jU>0|9k z^$^$cms4^x!rqcnz+v^RRQacd81d~LhUVy0!P9=w7c7R~XYQY2#?`B@H3#K2ZMc6K zU6*D*LZs{_@HjYDwsC+;Iw=qD%TiYg`3ZN`)13G(A(}4*?gGvU-!0K2|6%5rCnGI_ zwG8RW?92yjo;CM|H^d$S_TtQRi^v$4m=Ckbc_mg&Qh7#u7mi;Sna~Qdk_1u=`8QL>s-l~i4qTj#l)mdt$S0l#L1eD;#rhgFrHvkv4ha2 z6J==s2z!4vp0xpVyz~5#o`KL!#`W%3F zNn>YGm&B%FYk`r&NbCNQz~S$@91iEBL@6pX+0Ys3S5-AWzZc=1n(Ii&m}HKfHDmG$ zDH`&_fYa^JLZkNc6fLJ-Y4~Qu9a=M`_^+Hx5-#J`pw55jn~JWnlkpeuh%5Ch$qcJ$ zAoJWgG5zD=aGdNJnfZSz9`~m7WYL9cs}ri0==PTkH786IM(GQj4TNQG`I4dq{C%(5 zjpY^2t2khO8+oqmeyh*LQc_zPbtt36?KFlhw7vQK$8aN# z;E{D-cG2!7{JlhSk_*{kL04#mr2&xCm%?dExO;)+FV6Q5%iUhtnStQ*^zD_d6Ph&J z)!|I$c!ONOqlTAHvrU53+ooM_1c+UV^?R*|&16m``U*a*YjMjAg3(s~`LYXhFs*e5 z-125`c2v$x`#Z9eZGW~ysPRn@I_p%B4iWiQErU;7F|W*w3%w>>)~XLE5?xLBho&JNuFuroQP0cGa9 zW{yY+f%)wr4uNs4?JVyksRRmF3P#ueT>mzkg{1r2x*jfs1%A9Ot0*z3x?i+q!TC2V z_T+y8iw)t!*>?MfLD&JDZi5TsV#^nJqybyZD^kCpeC|f?OEgiFNB6QrUA>27kVOP5 z+-W5ya)<^mb_h5ic%kgSY#Ars6VS0ME^Z$4cd24LO0B!aR!P=7h0F>f()e@jw9ws* zG4%k`*)PqcJW;%bymP&9&Dd#q|64@2%jMfq*VX|jPT6Hsgu)1s)6`;{3AZVy>;L*Y z|8{+pT-5%$i_pE`*wkLt`+0g-HXV8(@9=GyEbuHK+G?RBnES)Z9L!X)>p2D6S1D$c zHcP8b4Et)sS(8lpV_6N4ZVi%@0f;*oPN$XaARHluQFfiObU}-1qL}0)rc1JG>dIe) z@CM8)+0_3K@p1{ute(?6hro?2XrGVCM4LaIC?jYqN1e@PjE_{WX3bRcpW?pLT}hC! z>6(aot1sWi85`lpyvZdtx$m*}5VNfq*+hS)`Z|1=ySmxNo`YNiVkac~d5>dF_l}Zr z2bVOyE^m8@>O_SFYGhWLjw4h0zMEaawBBn}0j*7w4=PXGhxxYBxPEy{T6GZVCsl`M zSa#;>bD^6!SuoeFs`tO;DQzqVpfTfayI<~Z<;z}gDzyqRqbkQs%4TkGk-PhdNcv&( zD$QaQSAR8Y2(HZ?y*S1+&Z^90wU?)}%iw5gr7Z*vVxN_dHcv25@0Idp?SZ)O9N5fj z&F)WAdB#lxxv;b$lu9%d-4eER{dnPB*ODIgX63iJwgTn%af{|A^~u_{i`NjL*Pgk> zC+KM?mSdJ8tgb7;Fh%;LP+3c^F$70Ve3)1X*_hS*ym`r(_fQkno%g-(^GyzCRv0&r z5M0=P;&q&V;Nm*g1V6fcdWh@jUhftlDz#lbJPl^~2$vFtbI!Iy z%re!})m!aO+tS!r6!OuhP7JO^=ibWv5+m#f`~yt2c%LGIj89QpKGZmcxl)TuMH%4u zots~F7Pos$06ooUq}P`wLH{m@C?)W$=tsSaj;Xf&^8C325nQ8F^i!Z=Zhb4xMh$jF zT&YuahGpAZiS)T7pU5lSCM=t}JGM^mNghpWbZXM|Z!? zRYr}ctJi)*Q7xAmMKKndrjxnjGVGi>#^a72PwT3ar4t4Z^lO;Pm7A?!263r{lLplu zuiS8Kv0b8S%ZHD)pPNmb$P;{^)cFNo9!ds!53ce%;OMSAg;mf+=Yg(WisE<=_sm&2 zV8zQB4Exb-z56vr-|-;QpmyA`QBf?1KLn$pB|14euJ9o2o7d0z@>Ul4k|se+XqP_Q zJV6P2QXCIAHo#qGn;u`Q8_N|W3dCOPRAM?Km$Q;F=dlWCiSvB#+udnJ;hsh3Q12Dy zJ^+1+U98TZiB_`QO4LXgu%kg!^@+G@Y_p5BaPx$7Pb{#b1w-9+0Z%p*GxAK%k_p+2 z4A^&4I?Fm&kIzp)FX%MvM=^E?vGTFBf>^W22+uIFbX%Exqm9-s)Tae#nA47yPwlHi zrY#<=JncvKerN~_QIT|MG9IOTAoA=m0f=S-6dT!Y*qIMiIEyF~2S0Am9qzCv%PUAt z-dqxy60br_eR$qr^8#G(3a^_x+jN}fR9G#X6P0ESY`&+_Do=UL5T$hKZ+N@47G$n$*!eB;I4<>7W=i;Rt$9vTS*C zVqY4}hS0AIVfR7$-@q%(M}FhWaFJ>H?AT@kqao@20s7v?!kdeuMIA(YT5{p0P!>|A zV_g}`OLX)0KkO4`oX#!kpKHCY$k z5w{aN3j|L3#i{Ph-<#ayLIq15f*bV?pShx_jMbo6+xFAUE7-F4IrWVS{vX6eoJ;t{ z!JiLJbT%FcKrBb|YF`xQ#(ePjLYr}H#j*r9Y|rG6)WuOFLj}hheK0JKMoo3UTPTS3 zNA=m;UaAD_-1!3w>-cRV61DI)S2bXRTnWLHdnHlQO!3hCwFd9)MlJn5zKg4k#Vc}imfQ2%Y=6z!kr$#x1jhQWKguVRiNR`+8JlXPkFo+{I zj3|cC#p@Kq=k#!%dG;7MLJs{+##}IF+)R3G-a`87X|RUl%UYf*WS|!$H>-&(z^5ZU zaHQJU$p&u^3{1|fy4EK$6uY+`l?EHX!vBn3FQP)7dp@#ZW1d`VkP1u#A-AyFSrndB z^8Tv-G7lfA9jTQ(^<;do=$i$4QCm!~&gez%rT8wo!QuCW0#+atcMZWA+99{~%>rL( zHDx(DtD~dd(rBb(vI=5SkGOl*^B&|a_hS#}=;1hk$aOstV;o2zTlO_r(u_ng)fZ2B zG0aisN0~;tMzb+JHa`n~t0&(>0Rqc|nGjI>$-g&!GO0wtC;$UUp+mvRj)H9mL zQniOu_GI@qxN$MgPw`jL>A#!+dv^STe;*=@{%Xr31G!OvHA!w>G(r2MFYaxW7W!%8@peBUX@|5DW}tmZ@3gLL>s3%7pLdV^(o(P z2tq%G*y5k4hVDdWIO|WXr)^{8UniXD%~iU2tF5}$d9ZZ74ZZiiLxh9NS4sgsBm6 zP=#i3x+5NtYH=gtERL(X5Z}HoEMIl*y!UA( zLv+sxWSYcucZDqXG2;SKg2T2%{DU8bedhIX_nu{(EHHc4a~eH zv{Oes!iti}Mv$5ml?Lj&_sWnK3ucr72ec$-F$gzu| z-|YjIHD}!XmD?X#sUR4m-_nv+^Ur4OJ#1n9f)^D>QS;uMlD?|+z(eR*M$&%ib*+v) zOcZT@`Y`55<3S>Q+p`c!)zXs|8}Q{tdrPGsINAs$^->FhW_d;V#6g)@$>M5WPy=ds zuf}BMu5aG`8?!mrXfVEm&Y=n%OE*d&VoGPECY2+l($Zm)zNvH1pR2p}8^8Gjx}$WP zWC>|6Z}3*Fo;3U-9m0)I!FaVTfIIypu5&sMjf*&Q%}~AV6U*?C`IK}$u}XV2l<)54 zxVxP`C2zr0%^aKl*AN2CK}w3sot+uDeS zJ8>{mr3%@;6rAkoCxji$%BE#UnBzM|!gDXGG?f>#Y6QZac(YzWb=X18PHqW*Kw>6H zaQ4Jue);*Wt=}oA5`RohI*zaNH&-1-Am-LU^u+rZVYvUqT*rnk)Tvc@U#vyB9ycIoKPhpS8~W^=_0?D~n?qayC#odw4& zd#_|ei@uh)$VcD*^jhd!zGm0@?LtXC5QkCHeqJ}FB-iJj5cA8!@_Fgc7>OW|DN!zV*@A})H4PQ{m9 z%sYssi(rK=;}}MzUE07UiJtZ5Pa{>oq%(NCA0{9As_%Y@V5+abJ;jhZek<7^BEkmY z^r|q2f0?AykY4)Rzli_A(!^1S96lN?fcMTJNH$lERf{v@zb*|LTrzc~f=?eqe;NZ2G?B;F%U1G4~*K$X>}s+u99%nzUOgGS@4>Pc6(OzJ6=v zn*0F3QS|aTHy}I#@HrnKVy3guJ&f?dRXZlO#l*rT{5xNA*p!^OvxtGI#cl8Sz%Sk0 zk7-P`Up4x0wcyNG#9Ld~MZhaIs;V zzE#r9FDGXzQs|Hp*;YIUz~vXAdeUY+2KYfR3`*O{S0YZbICurCmU z5h6cvIFqUd{|v{60pP|h*X^trszs%A>qko5(GKq2lR4r*?bLl^?+?1wHb*4$b$pzf zv3SGN8hkgVwkkG4K^mEsnO@|uLrdWGB+`#(@{e+D&V4d8GwIiN%GS8 z?=e8@Q{ffVC|H8`ByOOHJUE)f<(@~gG_i%2-H@#O-s|1$T}AZTJi#o*H%-BPobqko z-D78r+h=J?qrc*js2T`!dp5YK(R}uPd0udc8S+6U41H6Npgf2lsmdC4C6xd$ZxVT?q&;z7VJRr2m#myU>}|@udnK0*_+LbNnR^&%%%h~J=c8PjWiLK90=A$2 zD6=T8n^W%8;I3GXOrDXdxzw-a6I{}Cm0E0q$$TOX^wyLGsX6N#{0)Gb`9Mb33nLy4 zp@YX*UFyW8qv#QG<@FQN)auK~sxhGC%qGa-sEl;(*O^!eT~xMjWCJ&iU+zU%vaoZI zSG^iVdGnaeg(c+xYf_+{F}3{1t#~HUdLg!9i&8|%BA2hlCIRy6n&)I0yEMuDqDkq`I@TRHw8^IsGHs7q5q&gKk@q5eFBUu~cjv5RR#nO)H%V zv6J*IHbUi&*Hrm0*PcQ|YF2dKy(=V70@NJZwSF063fjs-ukV=2wTTDMx=b|Ra+qD^ zx;d_-dpAZ`U$e>_?IyD}f#JO)_GQjot!qsgV$t2#;t14j!*y1~GH~J@XN&yhRl^X* znIW>$&u8)KZbk5nMCgzAwegrA(GfzFAsOl4Vw#&qfQO+dPP0>g7S^bUGwhm$ZG@Ml zKOScjZo2zvT?y3x+eiL^RnmDXNI?{PVsrvUcK_JkD z@`$^Ia7X>Kn(*vdb!%(8bl#9FlS0ED=<8&x{D}dRwO1h5;p$4}E3wR$SIbuMLDtp< z4#_EGY~yn#-AWI?1u%xDCe|jp)xf(JT@%?=lpvgNr!jOpf(~aJ+03;0n>@78YHQL= zuQGQ@3!%n%T5#0_Y{bhl2|u6&%U>@2X8w7P^<428ULGAAX2~$wC{c=EGQ#Oa3x&WG3kORPpyXd#h5QjxfhPR> zLMPz1BmJUStbteiwd}TM+lCg=WGivNm+}iCceM|#K&ym$_QvHBtmL5%~ihzB1>`*pN8b0sWme>X8-)Mlf2W&L3= zIwUFZ+mq=2dpi+3#4Cs!HCUB)82Q2TiP#Pcw@yobBu?}s^W(5RR;IJ>4fS=6o~m}9 ztCSDkOI4IpBxXHFUZgcrQgn!uA$Swc4Qc_7mD8R$Z=m+en}iwpWR^0QEA$Wed3w1Y zRg}UngRODQ9~#9mo4Y(XjU-rMeu_6uwu9I!(+A9+tEmOCZxwAGWfaS^O3N~-s1Urd zl^CXMTo49{630K~qQ|YLtZyBf_FznT0LH*#pI-25WVwFjytm$KdYk@{?}~6fes5wO zCLE$faK*#&5pr0K#4{l`h8SNT)H{kOgxc~1?wv;3UVf|_mFMq(7>O%!;n_*qNnc4=(KaGqY%`&n zsHIB%z!PDAO6}zg%_mrsF#H9Z645A70C$9|7-E;goM9doHz||re;^w@VqVSq+_6ZmC@KzTd3MD#WMurp z!ty0H2dxr9ybkwVxj!t}FfJy;URO=6_P8ZwsHvr?w)>Nd(t9DX`-Omw2nZ;&{vGo8 zFVd>;;oNB!-tY@tyuH3@J$zZb%EUq8GuvVbj2e3v=Y%RcAA-~27SMAHdG+-C6Xh7AdaJMF`kUG>jc!_5+yN==9*}M0E?>)@03|a)&nTH6WpL@1oQSp`$nDW6+uw z8T57pQFxs{F*U;%b%DtNJ$}Gtz?MZke4$#=Yn?s2U`F4=*;(=!zG!3cq=Mz!(8IGb zz_tW?7UR^&=kdKqxs1T`u1cXZ!d#M98mlUDErDF4-~6T%v;9vNM;k6?z)iNYI}~A& z?Jx^LVnjVLm@`XY0Trv??6Z7kCVKdum(5L*F@)v)i+IU;^fbpt=-cu`3RF5RV8M<{ z=>`l=@jigw?*m`ES$BX)wNGMx`vW6|+{1a3kibRW0GnF%D9QYVjmvS~96*sMHM1cW z*_9P&Y z6Q_pv-N#k00g9tiBDctsop&)sw|xsSiXH%*+Bbto%YepAf-SmO$0jWgoi!7xHQM+j z8(-9!DTfn}UdJL+-QhAtKWzegwfQ8x(JulSM7BQ^_S|xniXW18yViBqtxrMVX$X3R zFTj<)C4#ZnEG&Fuj%?~36XtolDh3=k7u|cJYnEMEe?AlG01M!qJ76sF)hN!!(b(!OOSxNgWSP5A4(&KNTp?l_-P?W`G{5h&wusTR}M4G)8(|Bad-aEH7 zT=9nKQw9gR%`^y!Nk1^$98DXdWWSRBtx**neRF%!;ut{)eXXYFCFS%52UrhD31gaQkdL6 z+G|~`koVC*4j8u~TzD(=1rLzGE!B~W00L-~d84M#cl8~z`H^t_jPvQQ;l{3XtkaPn zK^%wLz976?7a(-@<1mGH0L$yk&2(GchWJqH(m8mR8SYWL-1u>ys;krMw9`{*4e!#n zI~^1_!Tx*CsG#^UbXLq5MzC`!!Yrk&~7^3e7Y5msVE2Dfr8u^YkbpovL~)xAEBswUpG)NF8pPJmZix!e+gy&38~WR z*g9iOWZT)|4cs|i4#OOx^vpYZs~#Dm)CZ>NmS)2GCahU|)XC0i)0I(v=J>#Qke4 z$27DbvZ(gHj(v^nFjB$Y8a2OPZGFWyROj`T$-)iMwImQ-ruT_F4#s?{g^fw^_+o1H z*Y0a67$NLNy`F^LIiFq|gT1biuVFPY!kFw0D&;2IK5M1me)XYiWc_z&2PbXoQcY|} z3;pbZrGr^_#4h0VPc z&-IBJuL;0MB6I%ZwD9=JV0-6(+|GZT6^&^AFY|M83jYh;%>UVs`ajjWvLb>!CUpgS zn!-A^uQz8xu9+D00en2#tW}m@1^z{1zjU%kOMHL(d8s023qc_M4`^5c5bpvpUw~qa z7KtSWOj-S+0AeUp%Ie$-V7w0BAJ7+%DI%ai2GGszrr-L-`lI5og!Lx2#h%_5mjZ~=orm*l|1q1NtfBC_UZ(nFaiS$4JfHpZj{($2A z0e&<#S@RF*-cA`I2;fyiGrzw7=O+Gn694pxe@4eY^8_$O|8F~ER%hq$*nig>7A+oT zw3tckYg?s4#B?i7ahf-S|A1nFy=$rMuL#i2=GO?&UnH47!~b4m_b+R)|1bOWBSQUL z-64~0*l7JgD*|TFhw@4@%DyJ$7}4G#rSDlMkD2aT=8|05GW&hjbwJmRQPyuKGK`8= zQ}%d#a~Hj}p0pCaVXo@sBfXm)n{w#~i)!s>BB$t3>j2X^1{{uX@qafqi%)!%qVGe> zMK4$vBjhA#JnuT%N#&q1a;7gRGh`3!+vn4a-%EpOP2kBU1YPu=hJ_im}Sm#Q_C~<-g2V!R=*HGOYnp26#+R>H zAKi0*vH2v(HdB?i3F05IbR8k_9UpWLH<*}Ta*`G5tfcztltGtYq{bH7CYKgA7bHpO zd#?c}|Kx?GeL8jALN|aJy5VmJk3=Qd`@P;a4f}$u8QnKnzsrN9PatK4_1o|#FT~vO z@n=H_125?glyTZ0&}qR8WC}f?dGC;rCFMM~z&^ClR4BHOar%TW0tPH=96yP-0u{p$9d`cX6{SBt!+I?*fhazQmmN{uY(b`$BHh6 zRkKsxdP&w;JGp+NEWz|#)~G*^GJYk#@n zVHMqS16DP}25-?Dmaao6FT;!udslT_BrO(H?UohqU;1eD)Nw8R@+wGT$l41hg#m{l z%uDUTRL2&OGd*m@+(r4x)zuL5x_w?|F7>kH;zA?Y$Ci3>9M+(;iCVne2A-iCsP3Ht z3Z1T2@8@`*##V0ZE;)DiRQ6x2_j}AYG?95T$GTG+w*z5qNyT0Xc1&@W zbK9`Xmd1-7o)w>kuGst}d)pc)sr~{Ih&Nn`TnYp3L7&sgOtfk6yQ3|fr{}TSo!N#M z?`}8CC)k3O&~QH<2hXGDZC^DD^pMmZugP|(%^x1RA@__93sZX7&0j_5Zo~-I_4rmdwwmRFKwWQeAcPwLTvG&8FEMuHPCv)%>)^x+I7bq^)d}Ar zxjzrvv#DNa*pNawY9_b3d=uN3$Pq8m`nyXs_RWf!;^doWR(8m-m1>g%s@v=8)d|Pi z$sZU3FTecc_dLM3)bHv=4%)H_J9KXAe7bNkaWG39$`am_%Yx3V{mPPEs>@hWeydGuYPz4)c#xz1NG8Txxx~!rG+^^?(1aan=eY?CaI3On^2vh)x({^7TgeZFNzOvN0-n#GoLv!7` z{G*bc96q7x{D;epAFz#?uEO6({q1O(@QW&!&Ed2fKYW1vw0aTX|2*GdKC7FjWVc&! zzhEF5N#CGhe(UFI>^;D|TJ+Vyf64T)mUZSpWq-Tcd)-{`bS^1{<-l->bP19Pd`vt+ z$BO!ldASIYrXN!TK8&&siryi`kF9$A%cnKu11NIb?53DfMG<}X8v3#D+k1lC2^O@5 z04^g&CW~$B9mT`X_xA;w}Ge6r>Np7}kwy<<9cyk~u`fGQ3q{q*L@S6|N?~HJJm8Jbjb_po-J^TW zWlrA9P|HZ1YdI`Y;|0!<^FoF#(2z8&!qJgVCPk)%fw*IuKPHSCDtz9;Cyeu_$ zU+ECUa`|j_s*Tm7Ne!it)Kyd7hoLrqF!5aU`y7Ma*0t2G*FE&9t~7qS(;e0~bv2h$ z<_Od)U;q#8_{$%gyaV)D|Cysxe+78KVU2f?pHfo;Qq1kMOP~b_O*D{y2E^*sS>*UR zjWNAE=dU$Lz$n*!hbpXsCGBc%?XxRwxbuEk+n}Pc+_Fl{?=IxZT>^WtHu}PH{YbyA zDfFE5@wsulXz9Y_6dCcciAwKsaBrr>fESkvUIw~y;cPr1?x8KpGtsCl7C52~9?W#a zxZH`Wp}Eqg+Zc=4d;VOdavk~I;m#7gr89n^lP3P#W}kDxR{Rz&R}NSF=E0osUL0A^ zcEQ@U!W;Y2)DJVM@xO|<{gg)t>}5LWxgHt({tWMk416mU!d`Q~`H&a=sS&pw~iP1J&xSDlJfVsv46&!8))=(q76bJ30^)8R3cTJag~0g;=Ft4cQI z(eLMnRrgo+?!y{T!c00sWJoHSI`-Mn-3#~-d0q}aL`cS5)Zb`IbaB1SspS2{r^pU| zYN2^+3QBGd?!y~z>HsVba|^aC2oSZU59pwNcTToAmv(#Q&UHBZS}UO9KD`L|l=JE7 zEiNPZ=$b=gmv`j(oxrwPp9RlsfHO+R$!S#R<29s(PCOEI(=>Iu(mX?C^kr0|m`JX? z(0ZZwFkTzGvU2eU6xQgGhZjW2LBK7ERyCPEw!<={a9AGl316E#t?P#*4@GSQ(Ei;e za)RiDA3r{{%Zn(ZFh8r&lHHcuv|j1V7(9=pl_p67;=xf6u!@n2+@2pEY>-w%`xi93Pxpt@l*G3$Y3Sv z+ zTYn@3Q?6BLJEbxP8HyX@=T=(CWRW@W70t1(k}sMC4hGtz-GljYo6Y=i#&TWx> zOZbP?;$iPLaWv(a61j7p^@wpZRhV@s>TNrhTEZdo8(aR*)+`&JoM-a8{Hfql_nM}1 zGgmqkf|jf-SRk+`PGV#eCjHvuFt+a{0F_uITD?F3mc7Uu5Uo|Mp7Ze{uJmB;~AxH;y&;NS@@z zrMo-t?I?0b)EP;y34MM4-#mUpf$&ruQ^p^Vk2hAyqPT>*WoC_6JsS2^|0TVYUXjB_4_M5d_&S%v%+2FK#k5O*R2r| zZX6Yt0ut+&cWU}NVdQIV6|}0pMvSp*bygPQGU#KysHBhGAkyoHL$2V1FS1Yl@oFiC zR+Zl7#IcLKYih(w_{`ew&;o3xn-kj8sB5|8Tw$>&ucpra?b(zr)ZUirE9=*~OVqDmb%wi^Gj^GdBid{97j7I$Fh z;I4Qx(9S`8&ug6Xxf@P)6NBKc7suxY5BCIAxW}_3} zXc|XX3zLl=>|1WnZqLd|c5`+{_a|5Ex{?T4^7h4lzD_H%eX?$K#SchHwijX_K&Xks z#PP#28TJ;H-b1O=^PbYvT|ndFHZYU1Br6s0nO66^HP8#<$o%~L+o@^0b(W$Q<#CXr zZ{`lo3=;=c`n1X=gwyZ2(PERgnBWP5PmcsX_5FMaA$k|zwx=F3)5GAdzU@Xi;);LERD9%gUPju14(c zbYBGPW^)AV){zc>nLU5G^xrvX161s3Lw?_k%)_U25qYOhESv$knV5Q6py)0D`@dOU ze>eey?Y#(b7H!v}LNX}4fBZQhAgi`fk*3@keIZGEy&uU|?D?Z)KI~5H+v`_NH944j zIE?$>owbTbG6V3&6Y7fT&S3f&j#e zul4q|OD>pNn80f$aR+-Z8Pab=3?9HxVXfbzw4RvAl9JEGr*$`8Bp)DboiPhKn4u_} zsYV@)xsD<`^juhGY-NmlY~$RbWd?n8Qy_Ryz;)2ecahx9VUj7|vk8Y>N$J>W7@`7R zU>#}vzxQ*#Y`^OYzL})lpFB2{+f^}ds+V|M3v3-2Q(%9*6YZlio1)tUSU$kb6#1>L zZ8vjRUU;< z%Si7)-5YM5e#*F9$$B&C$A#=+T#bd02bV;a#}ZUL7-|$ zxxPG2axD-Bs(Dh8BeT^kj;APAiCV@jwa3z%`{XA*wxjy9oZ5ROUt! ztiQB=RY>Z589x&Opai@sReMJ@(dSY~Px&tIu6J+pzD5N69oYM?3Sce(6y7d{A8R|v zqc(m#Ci3EwSXs^}QgrPkuhuqUP%t{-fzRlCbxcHJ<_N{{wQuopBH6 z(E7LKW5FRQ7_zjD$CY)i7+(!a^QpLEn%bxR!Dgz-AQ0_T3qvDyF@8gd0 zoM)--Ywzu-G~L(5`p8&zEy*@uBFt_k^e%Uxa17oH8;eQ<$I6IDWYIP1WeVby_p`VX zDa~f6A%m0c@{fpSYU_{s4{ps5?(3_ANC45IE#>*mt*Ls>f=*f*ONX=H%OeD+FO+jK z@9mVFSmbLPRKsUi(@$X_s>Jug-8Zo>@NHiS_V|)Ahu_g<;Rxa49W$@cf|{1hMdh2y zWU}oeo$STl$DzL9KBm3yd3v)Lk0CajqnmJkn>s z*2gD&pmpWQ&p-~_BCezYgnVqpS*)z^eUGqmF)e5gYzj(yal`C~ETx-k;&Y91i>9Qu zq{|vxHz=0a_Z%xuTD`_a5N9h^Tm>;FL&5LLP{P!1U2Jk;2nO;Zxe&sgGZkp)ldQh@ zvVETF1SY6!ZYQ1nol=?`tMSJ}yP?I~@0Qx8_p3$qd{wQ-t65628V{L)ITDk?fJw~v{ns?IS@Bg0Cs2P4!(@@Tq^zl^oLHY}Zfe@|L zNEeczxQ@Uk?H26Oa#n;NXQz$(0f(-ei*L!|PP0IQ&J~y9c}oG%T{VL1tRs$znnolQa>%nv{ODpsTwww z$7nyH-Sfp{F(B>%lAkJZGKUm4<4`}Atv&Ce#hITY%8NfXsjCCEK5)kId5Utu*aF_}xh`WNc){$!`o(=Umd(N((PYQ8DSX~J z2Z0?QnFG%dsYA}z@{1{ zS^p~{;o~oh6u!6Afg0XrLrubl08*b*|1`EG3w1^mo**cr7uXGA4xYSiUD9>-TU!b6 z_qw6lj^SsylI`k}cD~UL0i1Pc(G9fM;7;>q_Vw<5&n3v;T)Qgb?K;p$?|l=9as;O7 zTwufYv|N45>Z{ z@e`Sp&SJ*D@n?8G*WN7DG_F3*#arJHmc{$Kr{H57=NMvgEl0zEf!({TqEQ=rgz*Y* z6#s_zF~JdPo>*?5F^dC^_X2>`dVk59J+7~=SBu?gCr&QuGU)1prGGYYFjK*2h+>ln z!3O|AJK7IQq+ZTGdDHoylaX%vuOw`oogS15Fs*=HrH@$1o#18Jjl#>iq-~z{o%!!8 zd3RFI%re^2yq#!goqkF(9+}pN-D?-S;mMK4-^RBB*rikA-B$c(j9W8R@SQRzC^MQ+ z(f%{*XDwC2*D94tc&anS@1XCB)}C@gb1pIgedd`S-nalB)G^!G%$m}vu=|dXtPq-B zD9y(HS-n7bzIT*A7NlXX!({x1Wh^Z7Q^9_9L5+&bYl|qB$bt(;$C5;Hj#-AA5Vx+Ir)4)bN)V!#^#0G)TQz zz}m*~2rj${K-QFwHa|BsNkl=oaW}KYii#Y>>01Tslb%U%+%{3C=e_;a=`N}KdXs1N zzQMmOJ8k`j{Wv4ht4M_(Ew~5G%Ht=BK1Kd4Jpv2O0XZEmp0aI_9*7ZPRh2(@#0!h@+}3qv%Z^JCZ-!21^u3D287pB5b~POj`(@3H5~;qN}PNhlnm zRJeJA9@MEGo~pv$s0{4=R=^$N?DP2g-@YmaI9PI)ZEkBjCA*L2kJ<$5+XZs;J<{eu zb?w<#ipDdiPUC7@retD6TCQT6YR9UEIH0jK1p>tb<CkMimR%jso{NFYiVaKMfI1 zqDZt%Ul|G>h7Yc^R+s|}u{Ryd(GE*ST7)viA#)$^5)`Xgo}>VP(M{i9u^-fAUC|G0 z7R8ljNTH4-8+%}jgfB`*p^qj?(?h3`7~ZGK%wTogs?EUzPR zk!a%pB@Kki*Mbi`IHp9|Y!8v)%_)P%(~4r(UYmG&jc5zw_1O)f#%1ghnaqMDI0{S! zBk<(mi&}Szw$3V`R@je2UaQc3QPpUedVfOhSaD19;#XeYq05{(TQ294S&W;OO0k%w zusO>qBKO>!>4K%nTqH|ziDS5rT?wyk>NoAH@sX9zPC1OWdU$r6uo=!OL`f9~#oX`{ zsK(k1c@@l=Zrv(ib$+CK?YYI>G>bLH=@0iJw0l+}yzZCJ60GsgIryV<@yEFCu-9V< z2J~@5!-V^B(jHHfBE`ouISC#1Vk5lmtuvE2^ZkflLuhA|vLQOQb-)W%?=DxLpH&7A z^sx%9f0NB03pm?&RVcC`A0b94wfK_|9d)oSj7!9EY&aMM5hCbd<$@8MB?R3FuaQ!& z4Wr~0wHPg#@ARqy6+oI=jxShl z?YOzEe#hL6s*pv2K-roOp{FS9L8J`R(i5>Ky|;oroIL@YaSr}n@XZRT>9mr?Ga*& zp}$W^_)L>=6U^er4m4mIE7~2{QsrOKc-{or89GD|d_xY|OB&<~mJVw+yjTHTti?&K zxmu^63IT%ui~m`GE3;~8#9kzQl&#lDv{yQPGmDFSUDDau>m;_%@wL0oPq%i}CZ>CN zKLA%jdzm1W-p$2rQVK9`p5dsII%_kx5AChNZ>U{OSXP6GX4l#K9aqna^&`NSntAa@ znB;hXvWz<^O@W~|wZ`207H&A)F4( z#IF-zQyxFdHpY`N@TqeFCrV41fvnG1wO4Uq0S2&C@YTdxBXCSrSq(uSpSL2;fsnyj zMI6J#9|yq}vkK?!lTT+oYOcNuE0i_Wyj$SH{yR2I2C8yJ6=&Y6uxkt<8zbCu z@o_FtuGAKtj|62-TD$T6`f_Co%6Cbdzg)1Or>2=X9X$k89D=JL8O7UxvB^|5^(C0+ zI&^DY*I>!Y-8|AO;B!ZX%3W{8>yFb`4y^1qmo7uVo8e!&VNj)T^d5DiqGQ7I-d8Z? ziD$_sL01;0x%-lawqiN38@mITA_{C)u#)adVhNnIHD$zVOTl)UNx0kRzV1gauUAI& z-O%_d5$zU-{Y@cV>eMq!PMmFsKh)~71KUBQb9D<|yO^NMU1hyGZDuy-n>wq;SdYky ze+{uI*3p{B-l3R1S?A)xJ6|;64BLPST%`&Ti&qzw(Xp!VR%{-*$mM2l99XeUSv{Vf zLwc}mjBT4={x9|-PI!N!3Yb`zwJv-OiBJK?Y3v`+^P!^~BvU~C0t86jN&bxdTef?s z`Bu#m$u=x-`VjsYcsZ5ob=+)LKEDTWTn+t~KB?f-&c!jBgz$H<7eH`o-zouGnq=fU=;m=eWOygyko7&_Cb( z=UM&JH~tw5|I8czv*yXkgV4z*@HKR@#9F4Moa(sO3*oZ=NDr#~8U4?&6J00$f&CNh zCKtGRvf4rtzMhCO#QR}po=eSD7uBYrW8}kmRc|f7?Z`awNBMIVy92JE*wc*c`5m^T z%mVuylMbg%>XVC3YIaR~Y zw(@*sl%)s;eC&#kI1`j8TtB);(8e%)otrZ^*OoG!ov}CW@$4Owex;f4Nd6DV2y~b8 zD&MdlFG75ZV1ZdsvmA30wMI5zlS{r7_U?07SSi8S3m#lFk|gHNUe@^8dr$dw;_jg?+yy z5}hD=8C?=BM2jvWS`Z|9iQY0q1S5vgdlv*jLPYPQj}c{v7SUtW87;~%gD}x@?q|K{ zJ@50pXPxs0ob&u(S<5nW-!u2#``XvFzt`srTPCZ|nEtCKGImt2CbyC!QpddB(tRUT z+8ScrS41;;pb4lReR|JkzJ6w2$@M!Ksj~JOhl}~JzBiGldKde$pY)qh`SscvHTT^m zRY|7ZTM+)UqrNNJ=~|gf(`Wi|x1WeFiVoct(;R`{cO{{@ej5KnvA}0dYtQcP^o7#u zIOQ^=5=4h0>u4SWT)hG4y1S9AK(vsHJmX*chmQvVmJ+tX8}RJNgT<&ZAy{(c9M1>x zQfh)+H`WhJQvQ+nWDBd>t@eTBW?m{W*%_~KI~qm(Be|ul0hp5!p}^jTlDqVMYel(7 zpnULF3@K@mUWV|C4BI&7k~A&_axyLw`bs1h@o)+Y3X`b=^?07hot3t?-D*GLzRgG> zq%E?sCmK%lCp4a8xjFz27KU@gp4N-s$)l1wPDJZaLjY&|7*%r`ez-6GgS1CB6)&kl z%*6a7i2>2Q)u8=a;BYfvVa5@m8@h5k;m%K-AN!9eB`5p+hJ6zQt+end^=K$=s)OdsxrpDTgAQxG_a|N10XqV3~ z?MmA|UUAcvy_vw9_I=ZmKTh$k0I4<=-BilaH@j;@%&HeHD+ZTcRZ{1O{O-PV;Pnul8?$Ks$5O9(p4OnOw=nIiP$+$>;`~_Mv5h~)TI>pc#VeG}dNJp?mNi#Nr(T1Zp7o`&H<3QJy zU9h&M1B!*Y{tn{E&gyNB3=5kPMU7x^*}OQsr8P!yxTVtGpR~?TTWg$H@na!dy^hBE z)kr%bj89^qm1G>k2QzqX9*@xQ$Brc0iaxcBhv1x=;ckvU^m@Py0j^Gm`wApYj-3Z| zL{Y3{d?td_iZe4aBcE57A** zT8@$-cluIw3-Za8QBQiejYMvrEAyjdyec)Z({2V~T`7c0Qp2b1BDrQ9HO0zC5_R?c z+n%UC`x_sRK+xPDz_#=1R{^zjAWiQxisPY;*%`GX*4eksfNcwn?($}DvS+#QOZS`% zXi!x@Nbwyb@h=1s`zq|b^15S`jaSbv>CL8&yK#xZFETjiy>aqv1h(inCLdC0PjgYSBwZv-Xgg7jPo(%rYLfOkufl&PcnJppl%J=|3%hJ{q>?_ka#+VUU@0esLJ?N`obxaGyf&_6@M(9KQ?Hn_=XZVf4UKFn}a|9T9h2o*9`a{=DIH9deFp;QA2pHGTlc*=S035~ zr}c2u&Ty^qy$F#V#L+q;*$U6>>6d^Ssy;jY*%cZTi#;VBPsoOmN@K&7OiDJw@k2Bj$k>DDO0q z1p+cA&@~4-eOg|Cd|k1fa+!GbwS8(&-nyne1-p1pZJyA@o%xYDl?{6dQjCGaCN%&0_(_#$;Oj%x{$J^Tqnss$Af%+fIgAFo zL;%lQe9cB-j=zuJ+VzUw*(>*yV9?&L4S6wK3OoC?6w;B3TEE^R4hh^_A2NthuN~Kn zvT3kA$ zS%=ij7N;f7IW&1zb#+MN1TV(YlsYl6UV28Ns4YIUta1^p&l-Cna2c);z3%E&2iKG! z_C5K8YLS-Kd9}8&&qh@Q7jHg|J$?vRqg}R#l-MB=Q^gZB=~`muiXQw&V+HZ?POlA8a3zRj+jlI4Wa#skFVV0&1@uIg#QgECt0U2 zL^!Q>0wQ3uGg$Hg(8`l+H`RZJyDZplE<88TH}IH8a}wBe1(qvI-mQx!d9QO6VaSvG zX$5SE07%pRJTRfUdAIA{GW8fpx81!gV|iJFx22VI<6QY6!{0AyV?W-wU*An-8&|1| z@*k=4;JEv_q5bV!Axld~M+$*7jdhN{eHhR{4eW{m7IOwmx=@1A7R6WN#K3<#^FMr* zH{bO_-+Y-1KT`CpMK5CnvXh6=AVZZ8!hwKJhQ6cJ4VXCliS8##WYA z;^kq57RSuw7x!Qcz*mgV=~MG)J~t0Lkf}=CToK9Px0u%l^M>t>Fz!?BQ>K4hC8)Ds zKS?w7RCWN&kF)Uj>vorMURQ59gYx%)9p|~kf&AW;59irhL047d&nwZLWk9@J4H0;P zFCCo?<>_L{IV-Vwl%|EekxL~IdiX@B{)P7Necg2`0s$|NExX{O#*3|NgYev3!O@@I?VA{UiZujH%JovvoD%-d>iT zDDDWl8t4D@3*~xp`YL@tsg3dFfZ*V8@W)5>a4?N?$t?Q*=z!G&mnfWge> z-FtOtQ9s zOP+Zv9VRL~veLOh;3XAxh>Aac%{u=iekxQ#@MKN93E#XQLtufy^YasTy|P|+$^|*_ zy)?XBi0e(y;0g1A@)GmEUN{hgN}(E))mu|e(@^4ibNj-U^`h12$yEQjF4>ocdNO}D zWjE$+f9y&Vu+K>L!5H-^!EQogf`b!(HFMkTbyX>$Hdc06S$)H78CuD*G@)%Y6l6(E zI9V|Vt=lFf)kZ{J=tF=5ve%8XHflLIyhv45lZTxKxCMkXxdttL-0Hfo8VvC63CO{dMpZUqF4j7@OWp#X{kgKI zWE=lCK<*~3-YkWbz}1NdqSZ+-IXwGbEIJ$UN-X9+_4JDD?BQutbFH|c;x;5)(kD$ z_vNQPKUJ{;c1#&o$cc%3^ALU%LW7IJGln7=ZPlsk-O8z*#~=7SAW1GD`-R}kGbn?c2Z1tEjhieUG^cusDlU;#LIS2l0Nzt*R z6m=x{z`aBQxkdzDk?5wa-_1?*Lg_k{#IH8CG)^j)`1&xikspLUy0uDa^kY*RWi}J_ zPGua&Krk9C=EI8Vy6;qy{+B7AtxL@FmZ5hq zKV97;E_~xCLhXX6+*=B-%{a^y8Bc7sw@*ucZ84*!dF1YRu7aqZ&(d#KAaMgaFdwb7 zkVoO(f@0qx%ZeA#HjpFhwh5sYX;HZx@zRxFBH5U*f$#fPf+E*19gCb;8VE%?1fTvD zn&0(()fSqiMD$T#+L#*@Ft+G`Pw`+^mmb~zJ41m#D7|9!SPmju*H;pQHRpsxxyF7{>BH1F3oCZK}{)EEB5-7+(|5<(e z%mt{<6HwCJ-Z_j0Ti5t0Z~d zb{F(3hhxeX-PrSb?kh@=1ryb%v57NJBY6@3tfo~Y5Cv;CEBRsznKK3=VwVxhj)@p7 zj;d{{wzgS#)Ubrs-n7xCMN|vrzWmOlc)61A+46AMetu+N?ZmQ`$m;_?!2R+soqoFP zyyASQ9S3V}i9LjlU1^V`O7gg|FC3YV7O`Gv1mOXd`5LIVn*y?|!ZJQ!y2E-E`w&sPLbPR~>pTjRwVvZx%7 zXkh+By!XGYW@_wsZm+<9w0_+s?^|J3<*_FU72 z2Mi>vvj{ePBd?!E-h?zBtq%6A#2PRCO4i5hf5wP*{-w$I%lI|IL7xBS;pqdlY-r6+ z7@@7sNu3irj;6=bM?!ru0PM-Ln?^W*U|5b0Ef7@f`qDVg$T(>mQlR0Va3?mG?-d!@ zP2Tj6;yx!GnFgu$gwR}|-SYWNC@0bUS&rc}LYo%VMsYGp;q4kRls{g$%&ri`%FR~J zXULB~=o?4t>k!AVlJRFhiZJ57e>_lGS^XU~7k=9vG;gl8%;ITP(sV9Hq>g+AKafSg z804pR^zIJ92z;5dCTbu&_cTcf-XKv>#wmBMeure_W$ol&J41TrjVp%BB4E9u z+FFfi_`Kvk*IG#O)4s1`QJ%?0(8St?(@AXBL`)DK=b&WvSbd%(Yv+bTL2bji&Tc3N z!63O65Z-#`sYNxbc#Gd$uxf8>(dF61+Vis~$HP>>UvfyDm2rFtq$tm;9~>v`b!sT7 znjKb-*ptAN<|-w7xZdGkmI*N)8>xK}XOGP0nN3XnYzdFRruy>PNQTa?NvJWn z)&N@-gUIjaD1e(|*ZcS&mM?u-N21avS}I8@_32wfX`l+TSKRWcbge3gekk^8J_QTJ`Nj-5T^Z$8v5k^N42o z6T6XTt&2A9)s-F{mssmBq4vdrQ6#prxkFI z5!G?kg?`ay<>bqBw;jr8_ij5%WrQ*i3B|euksdWF9~mhOD8andDylUvDTgwoWW4f8 zgXhgVPVpJ*sx)bvIwzgC=DBjF>E(#|gv(~ICMMuNApKlBAR2%>H`i;tE9(0>%4_~l z?&CMj@Z|Ny(KIdUeUk>jEYS70=yvzK`~Xs%7>@TG@4phA?rxb3Uif`HraP&yF9~le zbB4FIr$}ZzEFk~UMg~G$cWqNz2?sG;u|rND5w^aeCYnFMSEwx^#>bbTmNj{tV3kBo z+2(16r_YhTVy^c&Nn72*e?r-b#)MSNi;q&AxNED#vi^?3E-b_`WrMFdb0! zR&wc~0W(3+%C%;AQS?8OX23eh$A;KiZ2|OqD%LEh)Mg1SmsLEGDG*h*UU=L>Q+XJ& z{bkW|oSjO_40%5kl^rUMuj^O1AjRVRp3VZy{;}g0pvfqGo5{|Gp$M+YWjMfFc}s#V z`o;Bdb&&gz+*hdT9EYQ}YB5;uLDEX1U@%AyPkr(1$iOxIn zc@6c6Bl6})`T4nqx}zCs)J_UqZ%G2`D5sGmi^QS<7>DIS z0mo4>9^97KuF>|wA+A5@RuzUd-A4YA+_)0*3|yAP-}tkI@?o;sLEcFVfqQkB_F+Oi z3Z;*%-6ucrTdc=-Pb;=tUR$GWqdGh4pY4Xbc4BPx^`|7?s!{EjbBv7|d)^nDtMmDE zuOZ=W!n5+Zy+;*hmy;3J8FF)p9`_dikXISYEp);t0Su=4Z2dru|Ghf} zw?94AA>AI!SDRnKioqIBBf8xl`s-7_P26iy74C@e7|i|hC0S@%&R{*p?W3dE_giFc z${(*EkH=r!I$O^Fv~9)G9E=GLp}P99C-K3APo=BuqFs-jEu>H*Y^M#_)Uw}@BmV|t zyt*TB@tT-l2FV``RsQ)_jY9Y9<8-3lcboZn4dEhY6l!cQX88A$&ULkZI_8wgn_kvy ztOJlOV&rOSl%hy7^bu4^G8!|-any^4xqF?R8{%jy`|;`+CTD3IqkF}7zk?*y>mY== z*ltg1$y>y)E0~$NSpRW)NCoR&nWYEVZKCvkoC_!TlW!2^;D^uiHjK|QK5?v81K@kE z3($=6jWzp-Kt=EN8lq9}3O?SG1-&rh!F(B)>z(cH8ZCT(xG6@-l3P!1G2&C+Pwm>P z(a<~jC>Wh^CyZ@m&7wwDw9}#%h}o!$1#Jelrs>b<51nBd+%3X?&Mg6Wf8C!gN~c^WU( z#Rw6z1_D9lW|!89?Ex#ye?rWze}Y({6*4t~IrAI1}7`%LK( zy|5y^l4w6*yCl03k#|*x>2m8u|LKnhP8DgfqaDp%kz<>c`4pd7#(gRN@~6A12UOkS zd{`cz2V(zIlniWOX7Ibl_KXYLNXnUu7j7jQrIwnxb$odP!SS12g@zi8{U!<}qyf-T zeJ;A)lVpNX;^!_R4Bynl0Y`$SRrz+G420+{8P|%xJ=gc}-cRoSz;5QI(<_jr6v%i} zy*3A1)Vt(05ZFy4I3e-b0!oinvbEZ-UAhWLM8Bw5>0feHjf>@8R@}t-E50h@r8N1R9iA@ag21e^uP(1hlN}k)As48)B zcLpf|<)85XdtqDdUdWP&;$cI~2}ajCGO@2_Q|A&G^(thP);y%b^g5Y)czp5HYvvb1 zAXWtFrwch^MK$zsq|H>F1j5{-rth}#{NI38lz7>`_^=up7T_2+nlG(OLay3S6*^{r z3n~74<+guO0=fWTt-#_GZ3`1!Sl&w@ul{kf*pm}!+;hqmw4!J2bS3{a(4G^TCSatsgMO5q3Uix z9}{LjfARo?t*?;u_%Fhn;5TbVX_{3A&8&M9)_Me;bnQ;Rr;DoBqF0-tm-x)JuO%4z z@GE)3N|JwY9fx};Xzt+cIJGC^y~|Gf7PAyiou31hO+!Lc%9QuY=~#W)$YxgtBxAdq zjes>KhKB?H5`l-an!nM5+TLb;7oFVor*Y^*HO|#Sq<}A1pWh8EqVZp1iBZ)q;B(7jWSm;?HqWvvb zTzF)rfAN=wg}zNsj_9$IGUn@3imLa@Y3PBOax^Q;X zbD@Uc%+8pHIX+FZ2`MM{ia*G+upfP)YNbgfMW30*78Zb{Bo?iWCn!_m>+A@jMP&mW zEm>k$Lcx)OtlLsaoWiZ)tRpw=yA)fNa(ly`h(;3$_`0>zepq}SPib)Ty9MeRV?#460gMy2G@<|N!T|0>29T}N2TmZQ)-GMod+j6$KN>DmtK8F9#IMqe z7iG{YyEYf;>{eK|O+Tu89k26k>eu}hNe?AcYEX$&NrAvFsmlGTursL1t8lH ze6-bx8zLF_;i%5G9>EQDHgc@g8a*dO@;_M4wDhQj16HWsjO+(~f@-wbGgPtmR|Hsg z!K%u--|;@5k;tnt!C21s>*0R}qmV$QTJj0LCH6C9qdbwLSBH1@7}T8|%h zLkc(_d>t)ceD20J;D_z#1{P&~tTK@B*`sW=``)-O-=5i%wQN3T^1ZAHMLQQdTZ>x> z#>`sAU31g5ea`Z$P053=aluQC^B9Ki>s2&LLg;%#qe^YO1Kk1y44?8jv2@G* zM-n97pD4Gj$PdaQWf#F&5!6^Nd8Q#1ex53wa^Up=-px&oS2ey`W1Rlctl1`6D0tH6s(t$!pTb;ODP{O`|-tI*_k zVdwgWfXKGI`=I$B35OaSe(?7!@#@^3PE z$#WcRAQdOBbQN0sj|4e);Te59SbakhsOWmE9Q|RvJn3H3KFJ2a5o*S;6|XDVM+qvQ z-sAuY+W&RAlK*lapS5^?KGgqzyZ2OguF`)b|LKzM|LIoBjkV!)CIDyce_c`r_P=iB z`JIyg{^b8Zc;>Qy=3)>UpUProzJ%z_TY`DX*wb!3c3MWt{TFTt0O_UV$pZFdtg*nv z5%Ky*0?WJrbc#`lK*jNx378k%Lszn>36lo^f%?DvQ3lql1(dvJT82?C(!> zQ&)_V0J!(3;oQ|f5)tCW|MtdSiN#?`MnoM1(4nHCWPg59w5EL_Xa~?4KNZwnUKtT; zB@#$*Y8ZYXv$`b1!43{#$sl|>1a=h>(0%$>ZJ@2=&h`Jkk^lZi{(Fr4_pJHvb>u%S z+5g{OS!G@4UQ9`TL^%b~_LUD=tef+`TV+DMe$fd8AmRn=Y!mk36CLvZBp%WKoBkiW z#<2h0W8weK_2ugVRJ-ExnoVg}VFv8xB$c&X_glZ6?3-<1^ zM#)lpo#PbkrQ{_VDi$=o1i<9){@R&-r~CPUuSa=?TM|=*5|yMO`eA9WGMTrM_Yj)? zwL4apZSm0c_IR9f+vcLp%Yr$BC8YYn4xLsY+70j}q1d6V(e(4E@Gw^tE)cE}WIZvlHjqsU zT2j78Tx@6S=^+bKucIf}_tnMQ-&!SIdfs344VoJ=3q)EYyMt zGM2WKzX_;SOr!q-HN=Aw5Sjp7&3s}nbi&JbwaZOM`$)iGuGQr6PgKmy(3fSK0`B`- zEb4DVS%|pRaTYH{!(=yb#eb9mHJC=eE-qW@`hbHs%0BGUB=<2tZGEXeoFP?^4@(SX zXvf7BJ&rDZogVfd07yuI^?Azew|2$mvv(Xw1hd!PCM?x-;(C^lp(eHmt zFe#+K+#q8SpiqsU4^SU^|Jfc3V$O*Oy>CTi>Mf$|#@rg^VViqZD?FpR1YS3C1BYzI z^QA&^0(z-ME7M2^EW)+Yc*ZM2Cy=C*`^^@$;{MvrKYhN`PuDp%Wa#TZbb1{%c5nU0 z5H8~;B?&{p0Ta{{9~+~r>5FL>|3hoIW)!28{K!D{hkn~zq6=sGzTN$RYoCxL;;owl z59?$RMDQAWC==8gKl;vHu_!^-z;(+gaV#!~u!K9b4!Er9`|eJ?w~PJxh)+RrC<-?o=D-(u&cQbWJI7ao=rLH9hpM<~taEVJiIY63Ww)Td|v zUAy*l+VR2TDTx6JO0xQ)LCxj1A}S~tADiUw#VxPjJY5`V&RC#&f*rP~K6uRA6eOK~ z`P0IOx{dAu>NTwCay;SqAPPn&a7~e+@a{Y*T7zI_dmXjHC_s36WV4-j<0Q+g%-Y&IXey_xF+uaT2jf9{ zp}C;92+wCN6GH6Zh?0E_Qj!3QIbM3i9oU!lK706ZY^`w8{($M`-wQBT(Vd{gn?Lqbdu^(v0qwUI1M> zBC)Dc15h=8$16zynRZ+ijCYVR8Regi)h+V!_E(Usze#GbnkN?Zt}GTQt$`V* zakqlS&)E`0;{PZGN%Ejo?rIUk?Y*9!ob1GN@gXgSHU?A)V{dbcKnT(sMX{2Ae@qL2 zj>Knc`AHNoCx6Kty9>Y9^u&qH<*j%O4T)_{TatM;1gI^E^EF#1 z_rdTQ2Y6Lg5N@f!rY-MAw(zos?Un~;gVogyqGcT<7^vjY`)mC)*K3-@`EMcP>^axK zzU4#L2RJ#SnrkC#k2mW;EK$@>u{5@&6r@8Nr6+TaGNYgB|$ zOAI{RvF_~G!@QwLj`}lM3w1=^@5ngRx4*4hvMra5uUZ`Lc5O*_NRXkjsYsvCO&x9* z`*p2`sHsQTiU5kJzF^Emc`5@0wf{V%mJQk^jirVxA60q(nrPi`3pnP!Xji=j5T&X@r!qi$wz>iUs+l`T8`yTH@bxkLJOF`at*E@9PVK?4(6yf;rM>>e<&)tb8`YJ4! zz+kz28W2NRP6oh5A+R+R02;A7?Xo+413#pI>pfUzbG| zao4vyo%fO^4Sb7u^XsqtHOkOteBZzvj7n9A=(+Ngz5){4pQ9N4-tMiykCu#mN)`ju zNv$y(CeB+bY1R0X=er-_gsPsi#j~!OQI42I)nu2;QZcKnk1dlbsRB5L&o>IBzr3WK zU-O2P^no~(?_zDW;1%Ms;*6{F$j=nhd+TskmUITBMWWkl+hHArmlO&pU!eR5U~yQkSlAhBABa;~1Y6)b{y1jbbjoFh zKRZNK|C}A{h)lUbgS)Pn9nAAD04$$u7(US~#Y4x6&41NbU&hZZ%{GL3o)I z1zk0himRY~_n38S2la_AYx?#PE7S-fi4ROK@S6?AODlbwedix)BoIR@kdWl&pk79! zmj*14Iu-4yFYI#+fBwN~qXu@(+EYVipAZRJdlPwITfyu^pLL_)4l`XgmTS5Sblr=574tX7%ih9Ld2Y-|G$(RyAb;*F zt2tbjlxtU5byY2Bw55KK(-kD?^Ip|p6C{`e4?Vwk`N3=n{zC3uc6x}v_gp}H#r%-R z$%sF%Od2cZ+ae{G956zlb>+X8?-4iFjN&>}dZw!Dd=MgT9*n zr0K)i;Htxcz6x^HT>Q~DBtMVTvcjF@3?<+?-wDJtt4kMZ)fkzR%3`ddN{)=elqc z+$hP8YHmwMT&R%o%%-DV{+5gq7`J)fkWH1{Ievg!XN@;PgL$+27TlhhIBtNi?W%#Q zwPl*NrUYcYt#J|eH@Fuh7rwqo9s_I%RDHWs=%x}@5!(2YHGn`zjUk(MIk%5Pj9dyl z50c>b`26#a>+dtD3lH1~B|U(=DF&fZ^SkxI9s!14o0@l^ z<(K{P1~Jah6vls;|8QJ^d-uacr~(nD_Y*2wWGzlnQfxG~J+AGPpt#%u#3z>BGK5FTX`n-B7Q~%$Yn`axj$? zXl$!vwhC!H-Zf{mL|a&+J9ngQdMym-=ZP;Tz)DBa+2}u3 z?!q^^B2JcYr7^mF%ulqpUjOnNP`stXJ2z?-8w1tC@0v^?Jl>!~3hO+ub&UEhP8GJL zaMqJ|ec7B6&vnxE8<^7+$PcbP=$Qr1;5ZE?lzj=BgD{3a;tzCNkbpflbipfCbxb5> z*67&39bIf`Omaj)a`QV0$sON4(IOkKv$59LwEP&qBc!~*x!X%)XbBWCX~-fu5en$wI}JT4Jwq^yBt02xsf!)Oyky{ zGDR#Nr*nCcagO1FG?PvE+cbVfdIT8x1uel#J^`wP1-GD|bN+{@!8R zU!-x{O)8yr*EZY0liW#RVo)t)?i-q?qWo*Vmso?1fdl`qTqnm;9@~+$*ql2$#)WSv zYDF=vz1>U;o=Apzf_!8%bpXC|P_XqhR3LBvn?9H!S+Eh#q$XL&yr!ik6trzSkcb~d zb>l|@?P(-xOs4vofL=E&YdZVmCSB1_649BPix0=yx4M*gr%wUUPc-5Mrjt8p&s%1* z(_nzQJW0BO3pm7n^wGsI$b|N7^ALn(%en(!_~D9wQt;=W+i}|BUoucie5vc>xzy6P zzIn4f>{-8_ZVT4@&qUY%z|)YJPg&7lguxHaPJpado%u*SMD`Ql@kI?3=lSucJ5uAvhCKi{QOUA_a&~3PZ$`CzK?Q*%C!si!yk4N0z`ZWE zV?XDs=l#;lFX)^&);@YSf6t$`XwPI${q@EE!F5j7fK{Cywu4y6QBNWN?3FxVaT-(L zDl!R`$@`5gf_klUE?8hADMqC9z}yFMqQvGa{#WwxBTa#krFy!BD${2Ork_+9W2D7%53Qc~zLc z@hIW9`o}3ekP7&ZYJ!l0vdUrnD0Vp1&(al7}xBHw!v8Y*emAjhsVL9wx;?q zoaY3_^CFrN%$`Ui)UTH5W%8GKz@)a1&v>im-GBTz2Rr0kNO z!1z2%$WWRR(Aod|-*xt{VYiU{?)J2REpCV>^~hXTd&=bOlo7B!+~35i>kV=lN9guu z=<=y^73LDg?AHpTv32y@@>krm{*sioeb_Nc&FCK8x_M%9_cI?iFKwqwH{P_(j3vxF02? z(NGk|CtQu?lVbABT#UEOF@hM00l&NB{U`2^Qr48rfe4P#z+dLap)Q+gyFOEznfUM9 z`MMORxZso0384w7zewK!-Okxn>1uiN^MbLY__CV)9E1%08P^ViQuZu71(-tYtJ z9#C7r$^Mjwli*qe$oJ?jbbiVv7cH=uu@L5Uzf@1;(38c>q>5w{wNqT^v|swAbYcc0 zuI@r4G2N+q$JW*=YAD5=xC@Pn?MIhUvyFp+fD~S4#Vw@A`H{3Vx8Lx(Bf@Y6vy!9? zCTM?KzBh42r;V>O|LHH&H7EOhe=o}$oW*A3!ZH6aIQ_K_=Y}zy?%l+v)RMK!FRE;+ z$*Tq4))lLiwHOCvAS5y+LJba@4x6dH#^Y=I}lJYOWmJwUt~pcL3Dus?ApMph9svB{m*+C0f_%zsWJ zoR1sdD@POU*Jql=)eN(Djq3AV|B(m> zNBuQZr&zrCh*3Uki$|~MCX!!rF9!^m?r_mF33s&Ccx?fF;T1fB=Rq*}(L+ExzpyoM z0X#{0X!H7&GC>&d=g}nYu7c=1?by2~nyb2;OgcKdr>}-!U-c^&4%%9RZ^q|sEGV?x z!PvivcvsH&sQph-jH<+rRoMWoeBY^94aS}u;<{po-%Dl9x}VM1?bYg+*`8#obqCZd zPqqHG4y`AuVv0IDi0hy0;3S44lrSMmPvKrQ+RuuN5-$~XW*(-~EPQinHg;M+d=QQu z2cj24GiZJbV;>*N`ioMPk4QJD+g{(2J9(Wqdk8EkUhvgfi}rSOoZ~}vMS{&rO1En_ zgS#CaQCaz}Qy`Wd$)y!USL#ycSy!@RCs#prS-H7Pa86{Tj+SQjxUJZ9SNTws<`keu zc}hmN6jH#rR|+0YqUX#KA0wKwhqb1~4mJc{nJ$FjUu*o;bY^H3Nmw^O`8X{RDgn{| zmVHGr2bMWt__Z|7*EJ;+b9k4(Ero5x0V+nt2CweBN5vzp71UCMS3*s>zYDE!SRE;# zJ3AV*StxVoky8F%KuzFg<)RT%tgpL0e1ld;e^&;H+m{im?4BRUDxd#D~Z&~K5)&V zdwTEii)K>ZaAA*xcAYP#8_C56{Sh(|o2jWVcYB;#?35RXe)&{mPNt60-O8-gtzw=+ zQat18aLHCM#>J(-UN_5soc!Lm6jems=n`mE9@Q?U9QXiYwQIxy^zkV3mCswR>>BUn zz?>&H<7@MRE6M`glAkx2^ux%szL8z!nq-9o7;+OJdfxeuX0YpH$h*rI8cq0K`k3AU zZoS|SC55NPybB&DZsIBngUAPWleLG#V|2+1KFviym52=ly)ARXL3E7-Q07S(!{qDV zJ-#%0`h4nh8=_OJ&gntCaG0{joTcS&$q9X*xmA=_i5WP$=h@oS#ct`QY zZuOH>Lx1PIJA!F(8FtT8<=0(k%GjB)-DJwwu_0>M);J`eNJI(I2@|6Ew6(#2Qn0ey|I~+-FdAxt=+bVxSmzUg)(@ z4%eQ3+2S?wzE!%K;t{-->9v;lH!%nq{=pbD97CV7!gLHMY?1<3IK;%n*6=LpWq?bW zciw>JHCxTVyDb?T-Lu1w@09jNEgP=nF#idReB0%xg+~^YLUNuf&X$31Ssu_xE#z7Y zrY@llC2q2&w2i;}J7`A~$4at8`qpZ`mffnKxZ?e-_$0r~E=T)jaB%SV4JVF<*Ze_F zB5!KAjOZ=K2?D)HfoWOOZgV^e!H)(@3qRTu+mA}|c4<+t zG!FkxJG>@)OdbbyArv08Ey>6_pAdV$NeN@6 zA8k}^i3@yFV^KEu!slQHD)2j!);F`9NIQLReaVg(NOESUqjpOl(B;)0FQRKM zN?u_DGGk6GOX3<>t#_^?yKriU-T5{%nqonB@a%)HyhoqZoQPnABG68wbdMu;@H%!G2{^^K+$ zp3eE-`7|k872VbAfG8pNs)C?JrKb^=ok5oEcI)%B&AoLee8uiraQ(exjSYL zu9SZ}9@ukxs4_Yf4_0|z z2_fK?*|rBK5P`-fX`Hl;PDZ=!c%!Vsj|bK~3b9xppcj8_NnV$z@36*6&w*lVo)&Y4 z!ymXVs{h;td8FG$IdeDizI5WfL7R63#0OX^pwP_6boUoLPzNzcD$8TY@UovR~diK!fVt5f1O{xtY`j|TCRE%Qq_-?!gIZwi5GfI;0~;m!udr;V5#Q|&y!$f?&r@dnGE(+` zu=n0kO?7XdHv-Zj^xnHv=?D=-r5OyXSr9`D5O--rrhgxq!nthkf?mXYcF!UY~EDqCR);uD}k) za_^|$?B{^@0y?oI_;tjee)o!^q3Khwq=chf-RxFW;XiD`;Z8 zt^3V*H6-uBV7`kk3+Gd8)scB95@CUN>~)Og*2`bo_QHB*`7U3PqtkFp1jN>`+?oc% zqYl5q_Zy5~X?T?lIF+J1Fx}?2@BWsL&0z^uguCI^BK?A875j|vdgYavUzOfU$4AZa zb~c7Sd$;4GA|w+YYiG#~Qj^F#%q|xvqb#w0_~OV=fjShwIAQp6vupX##N#JVu?E?} zS!s;YtIFPb0Uw=cnbN{^ZxMpqFQf>nAMu~<`B^?8JC$(pMz>7fqP$t_iD#n#ot6rG|2{EsDVf1|8Agk{o{>U05L2t5-Yz%b6)*B^|bO-M1UVRvc5y1IcO?tF8FsM*UZ%@7%~d=et%# zIdYSWFC-;;WsOa~1EiT|D2#ddcZKMr%D~I~7N{iF{Av$1K%I%7eME}x7_tcFklc1{`CKwr4mn6PH^0K`kF{Ze8dXv|dT+ZyK z+uV^XXUh1CcUXZ-cI(^?FC88bNPlc#g6-EWywQttQ$Z`yYS}+Q15yz}T+5;34@8Jc zm5AF!Uu-4$k6#Ur6lDmePdY)au0mg?n77L(YJKih840V-SoI^uAO>n*Bq-iQ`z_WW zyGP>C;AvEF0m7oTe%wph@L&d3mV1+XjQ-AqeF4k0)4krLk~Okb`XniNj2X_W*El9% z>Le%ZV1Ne+nlUOoZ!>E}sv2fqoXcDcz&snf1ieKDgk~eybe^VAc4$XwbDVJzGH_~f zXTCU#$%7A1*DvlCjg5h}IiknZ&v0K9;94WKJzt_gN5@Ok$WueOQa4(QXdH;mp|(D6 zG0iX;U9&><;|2qo6H)uB>(-V(lT@F&NpNzBk{&erDWXMzBsy^dp<9|m18oirLrLVi zulrM}7#TzI+&I8S#sTurfF?&dHi4zr5(BH%08Hy(ekn^Ub%x@$(Ah}x zP>YYeWv)$D1SJx*gBl%4W0^K5B49eEr;U?n>X^Vy+10A8I{~ZRq&3oqKxT?%UA>g6 zQPCUpv8&%6u=BuscjzG>i~By9;kE#2JX#DPG@){>Of1+D)WKd^5Rz=IRYEVN5AW_}fM$moI{EOnQw) z5%oDNXmz4(ZzxR_!SQ7+pPL22qMSad5KFJ|gQr}4%HJ(Gs>yIcpS^^wFi3=|{j9*% z@NkhFp-s$M%AqGhx-1+dYt#F>(A-9HaIsp&KCx$t5r-eF`_5B(sc)I-odp<@=Ae)jp5!oIVJ)~;y>u!k{t_z!m6 zo#l&wV6+bi&rBXU`vGSd88y936A(K5B~! z(4NWkIy<=qu%M^UB>#Fdf=zKWA8(j*l7&%v*tGuj&Xc!>)Gk7V0NE|{f;CuSpY=)U z;y*h}4a1{Ew%}7G0JxT8fZPRixKRRsJr>}qo8(m@AG^2huY=A{9r5}9a4f)8k5e|^ z{p$t)dMrRy*Ngj`d%Lgkbx(t@7ya zZ(Qqt+o--6bbS8ip?nc^iqXTu$ElV>L3C7A!IY-!QEp}KTr{qU7ZYrV z6^`|trV5ihEB*M-|E6n~2i>TaDTQum-wFAfZhQUh@&DaN_IG3I&mr-@>8<}Er{j1H zFoXH`-dXtttdj3ej~x=NOCZlOxTYX~w(sbX!9)6(o(+(6J^Hn{kM#n^QW`LpdPdvU zIu|d_C5R6auq731ryP2~AfQE_9^?Q+T`mzA>g<1z9Ib12hN*ibA&bsy$=R~!xj@MNL9#oBj1u_Ktae7U1?G?f zU=AVq*MQX+UsMqE zFBLr+;X=UiE84XF#xIc!b<(oVr*eCpQ;uqIgV6CHI%aA!h5i@%Lay^kH^2#T0Jkdv zNry1@Dz$&2C0vh6z7<~KMZ-=Z$;z&{6lMb^+m{qPRN2!{24Ba4Vy_|$2-%5P*qv#^ zX^t*c1)I4UL;9)QLz~w=#?N2|`=t6Tu(@xFamF{GlNYi?H``WXc$uR+xA{;vhXAN< z;F2h#X8s(wiWAl)DcHZLaQn3HJwt8#??rmVW1{^sy*CiJ%3-!y+%WAO>!Wk65tbYX zatW(=5VBP8W=+s(YQ>Qe#ibVG2gKfVKX+sFN|h}R8#rO91K{kt9_qt3#;;xcE2)sw z8|n3nBvT^>*-E)zx#zdSJ8JaC3KF}22((b#mGJ)nHe)Q0~QqzEr`&qV< zH~^CJhef%-TFh3L*o6#-se3$##kjRDNAgXJ1wLTE2J@TOTer9!ZE@|vV@#zG$;*js zvTG*0j_Moy)KdjSSwi1EdzzoW>Ah^GcXyv;d)?Zs$5Vh;LFFy(eqrk_$BIk zKn4c37xZfQ9cD9CQSs8ob?GGhQLjL>mdtc=G*}a-X?qSiiV{vt-bLj6&Nh3+Y~Oh8 zhb;-q7%KQMfItSQuPH)#;C86u>t?ajK>?-q$)aTlkSMOh4%LNrby--F- zw-F5S<^Y)V=hW??r-BU6#w)GT?*_pHcf#Y{5*`B(q;|(z%gDM&@#N*}kGt+pJ6fQ} zvGqn9G#&n;?58vFQeTC0)gSXDODTjq1mP?8*18sG@i`rpj0n{U`QNR~C(4bfP_TOn z*i1Rb6f$iQn*@5>V*J+X4HGY5U4^{RI$ZB>kGRY#`{oM+Fh0-^Te~bg%Xs@Db;(SEKUOwIz@F2f$ z&<+%5XtRZWcOV(N;DRnPA=Ex~loq9?>ZQp27T+I`XI1=kpRgG`xi&TgJAlG4P$WyQ#$c2l@eBk%JnWrsIOo(rA6p9Y{=Qbp=|~RW>~ds5=@a5l`SwRY7@PDVNYWojE{4rKk1)ozozT-LiGvR+;33af(XAbAwAJcE2F3jJMVb2sv+}2 zGXcV0Ovn5!`Dc}uxBH!*z0Z+}=R*M;Ieejv>nF4D0Du5wVQ~^+*P7KM!nU`=6!$vi zB_`&XAiE$l19cB#f7zH0o?}J;b<=M0ai3VJ2(S`0HfEyAx`rYaq>VLnFZ)=?mWgV& z>(EEg>)^CWH#3lzFXY#u*wc5Fbvf+w_BteW0qGkoK~GJs#y!_r9;I^WaFR!cJt5l= zpgprCLYGx0a=ILZ>QMH=+8SHC3NJr)Xt^t1K3}+`*=xzSj%FgZ;>Y^}EmgHx2$$6Fh zsiwqiRl#hObK6%p?Bu9(k@JERK5db93*Yc^+n?vx=*r&mBeeoIIzl>$!RP0%Wu&5` z2(wsFY+bM{@?m&x^ z+cv3=A#J+xcg7Fa#m48^sn{h&U{US&t>3BN;H;|1(!uMsxZ=r{7LuCTCNz@X;OJ>K zHWs(WWTrYi2kanHu(>d4g#afK^0hdU!(@gtk1-+q(MyCbf#GmmuWk&b|LIrymRhpP z)3qmgmka}ftSx02eIss$N!KLf`nrM;d}`g~1p4ALk;6gf^er@#C>?9>H$AMl0T0!D z|8k0> zJ-gA_SJkAkBaD~@9%<*GXJo8}gkFu$JP;1s}ghuSTZ4f8npZJq%RH|mp$rRAf2IH5Fo2M=WiNQS_-o)MwM_Tuz_(WSs%{Z(){mF~R zlZetM+}K!>yk2Y8l4~S;Rv<3|vJXTJSHkIq+d#6)A9MFPV8Wv#6LrhSl!= zjYrq9zlHsV+LmyCUD)Y`U_HN_7jYiddfbJjuSl-o&_NmPTH-PH2-=;CEE4gnX_iwv z73_z4LNRrx$2bkr-@4xL%n3@p5}z z`W0F-)-txRSIbmX{#^>R!%D9_rXm6R+2lC6I%#dMvCd|>)WcB0hi{7)biW|&& zpv&VvYrK2XYe+_-v|Ubzb}j3bJiC#g^0}{vLWgedUKW~ECDd zGJ-WrY~fit6SB%*AAP9;IrTaff`rIs=|1%G@?dxO%x?w^cebAuy!s4b-r^*ss{EcwBAFL=<3H*m|NGZ$hZ51+ zD>h@u%`wn9dmUjuBo>=|%wC&wauE3kiEg>>%{-qRMgWGSLbQ4cj0Q&AC;AUqcubyI zaiND<`&ph`d)WFcKV6iB8dvhfkvEhDK;MsnA-TlNsUxFlf1&)D0WPlv4;ikgD$|dt z_9geGX>P3i=GxDR%VwYOuf(V=CAILUsQ_L{$;^h{K3|ky*%+$4H>8CKUUrrXB%jl} z=iOiV4s5&B#`7dph~Sm5g}U3%u|Ive8 zQpX>C&+EHwZ)qPUR=y#5E!TsT9>lz(e7r^X_ybT7$kTZtiQi2`XglL7sW%umn)Qk~ z(=i+KU42sgic|-J5uiW+P5nlUpNbHJt?>;Vw0t1@aZly=w<`7_>6lfHh}+TjHai9e ziJh+!d8CDUX6K%=|cPrO5DY!EF-z#jvd_3;Ud_zx6@wbc6Qc_8>2Se z5=|NR%wKj3KM->clo9n8d~W&U&G95*qz}{3EfD8(rbsA`Kmgk@nwBmfU6o(s;Oofe zXi?NnJ|V>}CY^`0Bz6>oL*Yky9(gZD0tl2n0*RqlCjGWGmttN##q`3sXRWgy5Jc=S zGfgP+9#^kJ_*`qdNwev_A01p@Rn`T_(2`w*e5_^6h4`&v^qcb<)@X>Ox5OJ|W)$=1 zMiPynl=|NGxkh!a9z@5f9>RhUT7N>>2@DIOWiPi|6iWXg+fGy2y3OU^Lv!p(^4zfI z9+n_AA5i*Z1Sf|e>B^;@Yz9JocoE#W{el6n7WVsG$zVzlRaG4qq6p=zEjjp=)KL;N z>soJJza8~W;nn-2ud=!Pu6VCy%Ra|=2vD}n*zs_w#fZ#C+HYzRGaX}Xr2g%}H`%DF zt){Q|q(RMkb*o)XWC~42TwC6@#esURg#vGo4lM;B8DWoY63#qu&*pDyb!2uB zVtDoL;mh1FQDJgqj~}<6=Bu2L<#{k}U}U39yf8j9>n?g}gD?MZc}lioMz2*RK;k0tuJCE^bj;{}+{ zj7o}=w*5eH>^81L5pZPJVRQ1VY)`~&Yt-KS%pAmB8P*u&!#Pd|af-A^WpP{JS3l3w zqCQh=HPY-w%him_71Tu^3qRTer`KdK4MwQI`VkG98G@J6!XpiHLr0NhD0AE_ux<3} z%?v0kUlc!uJ7qi{23K9NDkS?BNEOIxAwwx@If5h)WnCoRT4sA6*vy`r>5zr*yFL1` zabnr<1E0KHrtK9uVMn2#iQp<>J@2sMouzEDb|eGBy6|EINN*mdB9ubhKM|2VsvH`x zDTW_p_p&!kfR8=RoSwXDsW^N_VeR7sR6K@Q0Ig)aMc{Ao!hS*o7K`V5`y4(}4Y(x7 zZ1JK``ppar7t@>D2`yD``a8;j#i2kgz^9GMoNR~*6@=Fh=uTARv&b;la2UL+j}`k- zCY;c4X$4adIYQB@+w~Uo*lu$*hj2m9zbB%pRU$6 z2ggG)!n;bh8NPB^${Ac!_K92jMHzRYb;gU=YjVX_vJgLy4VX{l)n`x5%8RD;1nj&* z-Bf{XZ1eauk^@t1@n!G@(1x z$B~Z7%YpF#=~$~KAF#qPA4mBYFFO2sj*Q;k248y!WBANzuOGtKl)0vW9gNjv$q2l} zntAR1PDs2={;s(l>+CipOPX);+vNvcurQAD=|g4r35zH3s_N z!}8@y>L)fu8F?EguLGt5u(Bn)x9aw|u1maZm)s@JL{66X{y6kyLPjLQtF+_v{+UcI zeyRwQk!Vr}LH}^w_wx#B7HE`M8@!?`}d@qiGc)5>{`zUsSr2pLW@^926M;h zeNU51^nImjt2vpM1}hXMDgBke17Jo-5mp?l)!Lub8i|;HX~Cz z!<)zH($XdKu1(SIx1%KzQUiHCNn5)PUGOV4#Qi>P=ATFodq{alv5jE}?t|92p<3^E z1M`nH5epc{xza7g0nRxp8UxcCV`SUFl#~nDO56ub8vgx5nT0STSSFw(u(Ls^OISct z-%_%ma&`9RV+sGExz82O5n|*mmqp)8ay@jA#U>}CHO5s&c!?soz^F$kf5jsh@^_rv~IYN>XjO|4t;gB6`gFo&_3u{uZIco=EWOImzi9q1y!b$+ji#Q@sZM zzvN_Bo2I8M7FDVpS^R&}BBV2fAo%>a!Y|3N^K30SVhwI51-F^ZP4tnc*~sQ3Tpmt1 zRG5pej^GIn>U$>@R<=BXnbopOk{&>rpWZsNP2a^)#-cgl#&@T{Z20C4sI1AZyjaZr z35lx2$8EN(1{jr7jrzH7_YTSB3vG$^pP-90ABjCUKIcxG)x74)Zb+=G?AB8U+xU3f zmS8IO9Z{d~?a4^?6vO*W%}dj1^P5@2#f_gV*yJs`mx%fn@8OXyvA&@hfTh+ z<(cba&3(*z>4X8%GzBLsI&wSad)^LdcoAG{U#|e!0PgxNh`osEcpdGw_z6`Jhbkbz zA78!t?QlN~{ZvNJVEjj+2e`z%wDx#Cs66SAA?18IQG* zftr$H4_ZV`lGIw9LdK*!e<#OcV!DwDi?fcbs7uRm1m6<$d_iCgcBWJ^!uI<>g3KJ1 zBK*UgA#{jQRQ{xQCEQxOmZL)+qoqxM=0(s;SJq~gPZE0K948%o{JZ3!6q!EOJEqhD%h)Ea-vBfWj}5m2Bys+A)i@ z7d7RM2=iuQ29^uKQi#wPmr;2gjWf@2TM>N5W2*4K)-{H^e5)QrLBVF9| z(HTKtFTV1g!GMMhb1^|_@<1K&v4Ci^^KDO?ZoD`*JV&{1XYn&s^2u_=y=wisaAf?6iC8x{K`;*Na%u5`C8x!@ck5z0| z?zwU78<~l(hl{_3Wvm3Y|1YaARv6CDo2&Ml#} zLc)BY8>L8UcqBGC9L2Zf7-`Hr-sJcrZ8n7Yl6*{kD0B2c7mJk**V(O4^VUYbz9AkNwn7H7tMqj)b_tRF%bAnJ z3_?vG5RBxAL30otx^4kDyWU-ciZ)tbZkI53PGOX60q ztW?ZjRfNVMM1k!o${A}h0S?OT$=#TmR-FA(*JL{&-9JvD*`@Rd=&bC?_!iN1PhGq#?jcT&BiZ#mOEtM7=WI4wJQzMr0& zq|5!Y0MoL^LHj%xtqP|#n9{o9fhPM>Lk!8=xY^wi7_4k_KHt=2YZ7?5Eo~)iYc?-7 zC%A$^M?~9W`sZ|?V?6@G!2-b-_i$VG8S!sB;m?1*4z$#`X$X5}RE}Ht&>YmDP`{2? zVQ6v@5#%oUsI=Y&ECM;=!B+^8zzz%Kw|Lo3JLWgcWA=8-w7i`Tt=+s$E2i-EEm?Jg z@5iaU!fpMEGkrmXHe5wHe%k1}@(8uNH#Ag+kW|c`Qeb0u$Ejie1HZLIV~Y9PTjU1A zrw^*I`gTc5%TwXF5U3?lLZu={HDB({vZi#o&bCmHb3^P?d1k``DNqJu0GY27X*)}* zbTo0KjF{ZbJsqVb{e+)x+ST}lmV8sh#V|z1ZgG7abG_^c<9BCpT0#DOIsbRAPBcl6 z=A{PrQv+f~N3&aZtJ*SuxECn)f_vS)9Q9mzM7F57_+F~A&+NNNmxQvETAvv5D?H(X zcr@V4M6dwy!)ZI)SkE-`W@pZf8k%zqvu;g@p;9NyVdyE-b=SD78>58P&O4Oty;L5A z5vG|lJ7P@<;t_EKpL2Uc**2>Za077bivKjrL))@Xw>R%__m#a=E#i)Ic!5b%(-IBt zf)x(xovw$`{uQ;1Wl4SqY)TXvS13AJ0(7Qea` z<-0QyccDPc&-V!B$7ghU(RGz>H1FHhHYephPBs{KsxyByt#~78)ev$yS|OraLG-f9 zFhY{JE*}f#S~h~>qRK0ycQ1H7ESn!UiJ^v6bDDcaKkNvmym1_OvSL>ETI2mEu3p6Q zvi$aC0)u=MnzxPUz9i3P1$9sj`sgnH-BVr;p$(%w_L-Zvv5hr;uQLy0xoSDQD)ivf z{!gMAk|3~DpNL;vs*75UEc|scXL)kqaBbY5mJ6jLoKxFkmu{Z!pScVj)VqXXWXy+2 zKhexyJXHhU^76gsw|WCEgzzJ0BGkFYPKPGos2#EDiSmnSm{{k5Ave38i-2DhWr|7Y zcK}Mn5w8U@a;J<2vw2t8>WgP$(u<$r7L99TOj>OvM+MTemzusNQZQZJdUgZswsiz- z-jg-lfa;a_%)ag{KB?Kela2;%jnxgUanhf#RA&oV2bcp4R6sOv#wbun2bp@$c4LV>Yaf5xFG}K@+NQs{e;wE9SFN2Vt zkM(-N&osVL^Svw&)jSElB|B7JxKgt#4Sn>zw_~17NhV?Tes#okKP}GC?;85ul^}%1 zRCz7E=?q%v$J~7HSI=>bRb6JJl)YUAn_B&C(ePF0Tu~YV2osOvh_7>G#UNN}@YC^v zc-JjbB;#f#dweibY^dcAl0nsBTlkDg5wGzTZDH33>=FVxaA~vYou_Bi0)Vr1U+$T< zEu=Jf*4~EpgPM62#GJ`m|ML5nSIhbXgWhVYR1Xdw5q0p6`1*mW$pOVz#uPN^$9b0-dcM%)9ORkir7;WuT~ zx-_CH-mx}ceDZhm4f0oc(>fQ=`pkBa$1R&9GRG6zn@jwFBq%L|fwgM+5{L&w4DM8$ zMabtDNBeU$KtJvwp^FB>KU}zk$-i1z%BQXg=kx&iX__{82~_fCds z=IXa8>w^iXPpPQzQut}_<8urf-N4rlL4gl>#3MpO*GW!K%#8jK_Y2m zN8P@Bdjb0tRhWK5HQ%tRobe{fa8V$-GmHDOXcU_WygFi_jrzN}r zL#JcygE7a`6F?1OZs+Yhr+w zuM$Yinf&!w0LiQ&CH-L!xn9tA^c%nieg@~9xXYiPBQK-?o^r9SgClM?Qh@Hvzm(6n z`$Tx&5p|PV%&@4z{gEA3Y|ozjvr>CVSDpWY@wsxw#YNj6By9Tsa4`TFJv9#O+r)bp z?~ZLR*8-`=N70A!2Qj`iP7pMOq!Ik`_6#y3X2R63hzqB;}U(Qs^V3wEHwD{M1{f83)80^1! zC;-R$UR=`%8&SW@L4C9kNG!%35P!>RSy(E3O^Lbn1}M>Z^ZLM5FxkfO@2=oN1Pbya zLir1S;FI3gn>tq4bq|TAjVS0uEh-Q0T{StpgA8~)z_~y2!Jofi` zmyOWLL-j3*xHr@yJ_a;j`a4$mCzJo$jsDW*fU_FW^B9byvT}BboP2W8}8*#>4vYH%fBIn%vmj~NJ zIxgn#7J3&1>$3e^u*=#1?f(Z!xDt}!7vzH+nVh~L9|8vR1l@0LAjHt5y|w|2ebUE3 zH~zH;Jei!*$}?&}+8>__GRZ@=45VFzRbM&BBMV^N9t)1 zgmrJSCgL>QKL34!e_@XG=(g@mGdhCF>GKiKPxC$VV>5B%eg<_3uF7z*HDEbxO9+h9 zVtleGY6y^*A-w?%O;m18(P~w(-s^#~9eu{>pNIUNaYHPI#K~n^H6$fGzo+QPEXiOn zr6|Yk>68V%PZ)1|ntp2}Y1`HQ(w}T2HDE7-k|Dk!T!OHWys|m;lZ8!8U(iF zD8JbdVaySBs{V1@m{{~R?gfJ>Ro5GrTp8L~h8Z+WzHw-^R1*YZerxuP*UsOY+SyTN zR#v!|ES&F5XKpCSo51_1*xpSas_xH*4#U_f7MM|64g>^XK9|2|whgWzMduP#faM}KA;XGr z?+wmKk#=6&`FN_F;#0bNw3aUTvU7(F$!gO5rlYg$EBJ0p?;nuTnZk*&T51+MJyG`u z*nApWvG9%j6bt#fZ4jiT{>wxi4-}HsRZms)P9)WAoT^O6`G5k=IECQA+Ah9m;{zK9 z70!;7-KyC1Vq5Ml_ol9Qg`=9N=E7}7VIrv^(1HmOd|`4oh;t6%fFGKE=?T_%FaCq% z;kRZ)0}kq6M{_te!mZx$+PEo8?3&ZhX6|b@*JjxR>z_x}2G+Cm`FcryGi0IEa7$F%HZVo-WA` zru}A%YNORx+}^+-;92_zpQ}HSdx}UD8NqWprIYEOWGmw{q7j0AIqb?v&m^Ayvd3Sh z{mQz3=9rSa_Ln`a-zR16W~43zo+A+U(jadx{bk}2F!D)HajI02Yr^@OLS&=&$Q>M* zngF>?c#~f@KD%z&dvWqj+EC7<=&iLKnE%lgs&1{pC9O+WPeBRnptrkB%CmL*A z2wr4?)tN&Z^P+mcWM&QyiFNPGO<#4|32;+*OT5-A&41L%&lPhRLg-6mx!b^fz%Pn! ziaYBqTD>ATc_@@*=XIZ?_YzrJpLmX>-Kjo+DF*V;`MnMTs3;jBJYp*4tG{TMOR2v{ zhUQDDysKGqB3FO)#HP}ZoSD&rB)IA{w$2k9Q~+?zZS?L&jVod7}rm5HDiKX zyzBS@Xqz-3cfz^x1@pbh{cqi6#M5t-lp+#b6}?9e-)n!2!Sqe)>yivG=I)Pg({nHT zMs&OCVbi|5JfH3Wz)^lf@?AO12yNB+6ZR-+OvP?lh$BMLqm( z@=Y@*=*QxZjewNJ^#-Q-*}ICVoj8Lzzi-v4R43e6ggk+{&jHD<`jPdyd0UjfwQ^sk zb?;a5XI=7{kjvCLYMx`p_fShf2&2;o*_p$zOeo3-5gb=uNBDaeu7B@isdZPGW8dYK zO!>t0Dd3ShRnvxa_hZ|6X`Fl<;vtS0U5uZF5MJOeCT}707Fz3aqc5-&x4-M|CC2Qq zw9$u0zLpEI*Y1HFtF*B^d9IbnHkZvap$Pru7rtKek`n z4yL~Sx;0DPaWdWSpgyiF5aY-Mz7M{Fed|)=MKU6aaZL?*CzoIuYBeWBvZB|`E}%LW z_T2If(YRZ~HK+T!wh8|8ujYySL|f}ccWgZMmT=bBq${SQYeAy!h5elEmN(l|GDv~B z??mV!j*sj?b7S?mil%K#{5|nFu2lV(Nm~A=eisKMU$RPd?&ZRSR^LMkKKI`-5 z=_f2}PMXI`CoMf{yB^BD$t}CqV;oJ7ZSo^Gl&w==1K_5|<%!kx*?Y9XE$>`>mBXhs zt#7$P_)*(%NXG@|D4GhvLC||wWIGF0g2&u)mp4`6kUz}I=lwhu-umX%Hs;<{l0MQT z6T4HMPDiGLT_$&%!>Z3RHYycSrHkvvqWhKUix1y+t!6ld2c~=|R$ouE&x1Z!&U8Fn z2|U~`zAS&3BE_l7KpXUKHheRa$YMHmFt23LSUXkbS6|&s49DqdBsK8Xtk*O5$V72l zJ#t~#b14h{A$d(@kocTh#gS^`2=qh+;8r?ZkKTV!J1*lAu>d7==h_#k5}PmYE=zrT z;WV(n%zCWvdk}nY`ass%>+oJ7EEuhQ4H27qx395+Q2V}ON$b?-v*mk-51rq?oF=8xC4F(FJcmWmR+3jw7|q|?bjYPos0)CEd| zIf?{jXDT&~Dw4FcsWU-GYCD7h{-PCY%=l}BKdo@Se7-(B&(8YsXi7l&St9tOz3Bb^ z3bk~)Yr{8x(&pGhbC}OWu#23(E+{9xO!ofgwLnw}q z^aRrXv`!Nmt9%y!CdtM35+9-+^ff`CHIC@q%g=;g$j2sQSh}BQXLCAffINwrO{wVlRz^KzprPAP-iDqmh zIwF_a0Ats#7RhB|3=CN?;=M9k?WoUrsj{dk?TND|fnCH~s^84>2Isj{p_tQMTx~Lk z^1jCaj&s}ZWmz4yO!@nT9yN@Kh&9<4&b{{$GqKg}^nH=Ghc<^%pWJs#-7$%ElL@vB zBdq-dxnWzmf|B;l5jK2walU)DAb~tntHv`Uz*3vNfVoR>E>&`jt^Ny`O}&FMLLF?8 zk${q4I=ybWtQP(F7166}TOe9)ZpQi;ucP1|4-qSKlD^$5x*tsZU8B%ndF@i23@+YZ!`W|K01eG`(S{cACBV|3W0VJAYR3bYMAd-7!H|u@U0Am znrB_=OMnn8)>DgPy*Z=jTnRV%+Lzd7+MI19J<{OrSzM)BBBL9>Ez0_={n!QeFkOU~ zH?QeXEk;Y|$@9u^g!b|WAKc>2NQavG<_ArUx(}dlr*t?|4k<*`hbf-pS{hf9tfs8q zYN*qd6$?fJlXh>LVn5^xn#UvbYU}91IjJ$}`u88kIC>2uePa$yoyJ5q*I1DcX%FXS zWFkq?1LxVwbz@pA>#FxXK%Zt}8iNfTrfk8ssNvDix=P>(6-uUb=hqm0&O8%tw|YQZ zNcLB4A&+T7OE;hAxy1W)D{iT! zyvmK}`?7g|7M|4=Sg-p0Lw33Q_3V7XxTz;=iFw(#-izL2;7b}0E64^FB67WNFU#=gAto!Qc7#gm+@CwOA)ID$t{BY(O_O#JP(|*Bi25DY(By`!HGg_~zQ2#kA^l{iuWWx3mt8 zm?VQAiW*n@T_3*E;f;`bejI-Yj+I4|uB$R8ezEq@_*r{pMkvO-+2DJyR2az?h4eb+ z-#EgS0}e{(g`Q`cCGb2X2+m0zv&gsrjuJO)f)9nJ#yOoWhBh}z@ z9E(;I!L6^MZrZPs2BC;=olaKy0J&r24TuGK)XNWM?)@lCg{GFGANMC+RpVt22+uso ziAFLh1C^mP<7eL3l}sKeRv5)W`*Y5Agtpe~mZHlxe8s|oOC%vehXQRL*|!MNVnQG$ z!S1+_JBtQ*$0U?=K04-8;}w^I;-J~`{AX9W^3ty{X~%je!7J~cr`4X{l#jNFiIp!G z{Q?Z>i<%0I?O^mZk(vp)1xQGLko_pO{rcPcQ(+-$x~vZp{@f)0uRh1nUSw_gVvIkC z)fl@=y5S%SM8rfDpeJ>xLwtEnANP<(2YH!j<5j(mg4tSzu7dx(3%PH!W`iD?XMt~ zm+*ZZ3+&&~455Qs%45oyFjOQh_RYJ*bja_{7UDr1U2KJD!CEc;>+grAdG=Za8GKu5?4tF55-N5J50$O z%Zm`Owq|L;S+WB3=0&d&_R_@1qkoEglUVMHjkN zsh#(u&Kg%Zy<{UwsmXl$t{Zm`h*blyQs(L9q#Epy)UwssL1Wot0(-3Y=hmLQ8L7L? z531}sMf{o+*$pQ8U4)|H9sSZdrJ2QXDEU0&Lm1Sm=F8h-H`ktSZ&k5JRroE?@VA>M zuWqc|M@!+mHeb1iX@_PGwO9rCTLmGYY2S&~|_`~8N8@O-Rb09}4 zacE}e&fe@gfflXx{YQlh(W^Ikq`yNwkF2X467T`yXbm+vD_8%nPi-++4_i zMnupuedom=L@$3DqD{{+62x2`&Mp@53E-PRiBF^7Dr?Rw}jJau}XJ+Y8d>c!8K zgv=eVApwa!>M@Q-(ju`WlWA5Eda@pRUs0)9p`cn~K=;-SQj^;BP+=pkTLw zP#jq?whFNi=ya)$l5=xuj(u^}KuxroaZ&0jmxVgHpGwc!4I;quCP&ClX~|d(2)wNQ zAgmyCF_L}lc2kP8kPCT{j%QHiaJd`yA|*Gp-;q*f5wva-np0#y|10C-N=ue8w7;1X}zX|q0(lvdV$z2y-dM-GWDBq@A)*=w6N7R`4nyE zxi2OV-L`X|D5Ozm6)eHkPmig3b(!YQ^&D9|MhKJ_Oe*8K8ir$0#5Rs9u7p*j^w(2< z@KBZ(Qd?7L&sn7xp0?~3U0cZnhjg8gPh|^wF>g}pzuWc_@%qwOpS|oC7ruw|v3w{M zGz{+`-n#F_O3ds%x?n_t?1;`*D59Ljh@oD}TD^dm>)s>z)6Tn2FT;S&#}Xc=V$O1fs9@QxJYC^$Ol)N*9AK~<2#l>TO$AKyFT#djc{ zGBe=}pfs3a zdvuZdykC$4$seHpZROe0sp96DRNJ;yD$$S38#mAGsz+b(u!R!`{)h)Op`$-Zc~G&E z0*W?hk*LdCY|#S@oyG6iO*P6JKECVcxFt-CaAk0Ny?A6k+^57GV70z*>42t95Ft*= z3jy#>u{R%UU+e*h6y7Y?hX*HeSZ}N$Y(^QRu`JY>`|EW{t3(J7kuR4|s`s8BT&0)M zEtk!%&!;ddHI(Cx3m$wGeY7udV)M2Mdl%5=_SYgM`*uYN5GQtYGlO&HGNs-l;dv`_ z_}_;-W9632+j)-Y)~W2?Fs=DmboWif=BN*P5VBG@`)A3zHqRT;&^8;nRCzfEIc(is zOLw;CqVqFkOJIF=euQME)B#cw73l-iH)P`KuIP*d9D6dQ{TH^fc|MnQ%}_sp_&;`G zWPacoGdqX<51~bof52qCRxLE#?o;Pi1Cd&G&yS@QDv z>j}@!s+M2*N=`~dR})vvO|z##go!Rc&r3v&O%6_S7=2HKd2E&h^=f6r4oBqXM-@eW z6^4*p+G5^71(2)spPiIQ5!B^F$BVkR%@RYO!adR9t>>;ao1l}c_Ir00+G=gjQYO2^ zO}?!%09=pVH&sLJE1ZvaOdU$ws|niuBe8FE2tCXuoZvYsJ%7RDf}%`+Wu&CDJ+r_6 zy+SH?2dA@`EH~Dooztb`GtBy~)S4AZs52lZ^PnA#sNb3Y$&$L3t7W0Ab?}hVUD(w< zEs$pGPVSF*n)mdtT5(=Qym(_&A0T{UGK=6SUBl@4wc%L5#3x9L ztpZ!TI9JDxj<->7%8j1Vo~2U~H!S9{6T~)!zr>5MDq5kP1ro=G&C<49i+oH^kr`Ae-RL!Lt zizE*z7@h2`%_5R-VLZ@y1}M^fb<~*>0dMYL7Z*H2?Nfo$VEM}X9Y)yMZHwu<@@^_Q zKaG@8h;AGJ2O!haMS|L2;ZK@6oEqO%&Ezdko^S}N(Rm_v9+Lu3sDw}~dA(=ArnST@ zZr~$=kfb*-l@a={BR&4C&{yR5AG72_y6J=phO{63N zaw@3c?HGA4BqMjXtfY)IIyi2Ht;25@_UJj_?L_jsqz-sS6W%G8UoV1#kPvr}!pj2zeLdm&(Q0V(a&qzwqUk)0Q|uN@$fZ zzyq0?h$>xt)ID?5#H>s#zI)6YXZ(5Dyk}l?kaHsJ7@^f|<9<8P<_=yHrUtFtzwRDw z8_NCGt7bQ6Y+u$Pt8~GYa>a5YEj22ol0DK8;xA|LH2pIxH|EMA7`&D+FB&7vh3<@h z7#7j2Qnunw&+Ij=I6Djl1KKEBM-&)Vq<^mWk9JakNx^(4k0ZtGBTb*?{$`8w8*$-p zYk{`Q?C$y25Fml9-hQW^s@8Jv(Q27k6`#+Y`fjmht^SBb62ZR{Sn%XIzhHu7X26S3+yO^_)*u!g5wMC zb5)-skvLZBZ4^sUKE|wng{2K`6(|2_p7q)E!Plf^XqBo_o^vfX`<VISW~fh z@c`-N@j!OA6)9M@<_abgRd*>CqzJ1vv68p1u64_>gO0o$LhsxArRq$T$@^H!pnc9d zF`?_EQF*Lrj|e{j@aP1yeDhDLmQkUa!q-+KdAoZbRa4noY%>`lZGHLMp2t}Etn^TF z7Gexu)qS))`1$q%!Y`EiyT8OPt~FPCqMcXsZLcQwh5Y?6*X_F{K+Hw|k_=eZ@-Ka1 z14Z|yYQJB2kWSq68uh;s+(20V@twIEEby`MS^ME;6}CZ_I3PSrgHfn3`35nyziO#4 zXH?IV}wWJJIGz$*3{*oaZ1sP7&9IgvwpECA#;{=8LKY+wHN ziA_oigl33Omwd=Xfk$mqCLZqa3&z}d-ke*Rng^!h)4e|mV{?nEdo|fRivZRlqXBh{ z#~OV0yMp>avusjXg$lUJO5V$2;afc%X5Gpz7F&t;XnCuH# zupKN0V)XMY`&n_ItNSVU+#y$xT(0l%5r^FyX>?ztT>WzUHjuf9q1^~+bGuO6o6k{- zIt?ZH7W$=u>tHrAa(N}&?F`7vJ*%M)To-8P{nGyMBUKtNkQLAYy$gtRF3e@En*pGD zjL%~<^kl(gr~MTv@#?|>B=A#1*gd{?DPI=)aWt0%GElk)tn0e0mj;~}=k)^sowk40 z?lxY)QYUYz`u0;}Tik+u%W8D$V@)ZT26u981UvCixX!0vYjF40?sXkgLsiM$Kw!odi8R_0sV=28zyg!;yOfDB%q~tFsuQH@rBzmoSU)>B{X7djOHfO_Kc7m%lz z0%R7)DgCu@m(Dyqg+JUFD}ClCvO3frc76e*E8Yg$Ky|I)E!u_?s2+Dkv9rQ6S1a{i z{(ga$jl{5>11eV&7N$U zI??>kH^`s}qPu()2Tvc|Gj68F)%-9){Vgu8Z2`38?X~oC7Pj9AWFY#4ZYo`|C;#Q= zB)|;U$iw-hE37w(du$cZhN1s#7=R%*p1Mh|ngCKQ@eO^1-|WJb%ZM8c_&@W=ub~IQ zzwhw(&LL!z-(h$Q0qj@;J9E#W&t2+Ms>eK-l*zL>KKC{!1GHtwVwN5N4q*?~aTVYK z!^q7lopX1tUI+rj+)JzHqyQa2(X~QGR>)^i-#yct|IEg1%6x)+kQ{+>rLh<&_0tdfs^9dxp`8(+_rLe^0y{N$SNBXIzk+}^=M$Va{Pm z(6W%Z4wTPHmko;wn^F>A$Ay zJ*F}0>CnGCIvJzJ2F&KsFMl$_Q;Qu|8F zJ+2vs{@;uWbj%96RQeRc^vxsRiQ7bI<@oHvuV3IjSNGdPE>LAHZBR-P6VVM_(}QgC zku1GI9W`~8U4ioJ*2;R#(DF$fa;Q~kK)CK@ZG(Q-|5o%jBm8LXcxNlsK#wd;SX18X z!!Gz=p6pnMn-h~-t}(-(6h?$)alN(ID+NrUIRjUB(AuWs45apNFaGPy3A>x)QpoMk zYcb4&O|RHA^Vh%qeO8Io%be$`UE1E+X2~x5-#tS=PIJWF(Nm|mssD+$=Q7ey=~_4c ztwmTLXGA-4SE@CXd>3u7dDoZt)Rp2^4m}RyZvsK zG&jH9NOMY4e|e}pa_h-%@pq+%zMuNNC{?@|Mj25CVfIRT;Hux(``J_8|EIjN=pcQ{ zLp@r(d#>JCyI8elzwJ*je?x{fLCZ~+XsO?todA*O;=sGb!a)Wi{&Ir+Agl+T{p9|! z)I`yO0AJdvV$#vEzj;k>pT)(cvJ4-Jk!(D0`LondYd`;Gso@`UZck4RjWiS^;0(r=9A--mw7=|45s7N`<%dj@O2YI#hQOIbG_U7HBWn;dvo?cLG)ZJ)ihFZ(-Pi-5)aAEUP~q51yi$`Yk4 z9&hNP)CZJkYhImj9=Ru$?ldP8!?{)M#fH87m~uxlCTdK0mH^4X(hML~yZ;HM$~4K@ zP3UV=WG0Sby5I7E|4!J)BCJiEww*V2sOP(v^O$IG>CSWYL8a&`?3vQ6KEtxUslt-c zrBYbA`EIIUeJ@92#!_4Z^^$VSJz1k-On-l-649j#+~ zz|#Z&zTAfrS3=3r($RiFzsiu-LQN(P9eK}!m=^5P zkq=V8C~A^X5r8Zs!USGVA?zKx8_W8Iq_yosn;p3*E>kwAcyZsnGE9#kes{T>tQNmQiM|- z5G;1IqI8~w5~GBY8mrZDs{pp#4>uaDLeoLJBxrW*E(0u>!g8jnx{BU$%5#+W^@ zd|SFTrq*~+je;KRVf{CWzW_@1gTAxptH>kLLa6d5MMl*!(-{M;?A7-lHCl#fna%K~+2i1~bVN30tqI8oSaz+PX zl*H+9Iy5Bw$q&1-_s<$|0)}sTB|rR%Fj$YVN8t_X#`2Z#k_9BIpd*)_yUiFi4xdTV zrrb6zGCD@}sV!&Rea{T)wN_I9Ru|KRfhwWYhb@JhrnqJf;J4%{0WnbG&ZS0ksTm{7 zd#$nC&eD$zP*(X$Vmsz|k$&Gom@glM^}6N==gLGjxhY>QYR{L<3p;8()wF3%A5)DL zz7w3#5n&(Y|01m?BuU@u%_@l>cG0B~##2(u3H};t;8>yQTZN==iXZ)^ny$;#Qr{q` zfmFRIo<9DSiG;G3g|-Rse0_BhFv@BVkK|ZCArG~t_Kvh;$VrVzEfIwo+;vMyWRwj= zBw&H~@rGuS)4aw^on!^#s{wZqJ<((F1BVeBR=zu&emB~8FHeGX z9|d#&=$)T?;_?K1rG%DA2mdPdjpyqclS~?gcvp_=*-dS;|@oMF+HL(OzWsG{s62=42c0T;BHOBF9 z%8yz0;FK^xUfru&vqCMkYS`a42Y;4D=4eSSEU%(pvQkstE|RW)#|>k;f&ZQ^pkJY(dxIeu2S zTyxo?d4qyJH}Ybc#A|p0LW*1^Q$f}-np^I)8^!w`gly3@nH`avFHDr>2E9#LNW5zu zkRTYw`v@i~#{_3_oCV^#|5|&JvgT*t)i6 z4Nxy53%h0Hg=k;;dSG3$q36Rd?|TJpyfxFC+&^qA*OG7X(uz59b9hl~Pp+Jey~!xe zH?E_*!^{RO$6yl=l9fv-^!6Yt876#d8tI>0T5;gqQBFER`Kw>ooSuO`nG5=na_9|*F17@wslXZC)m~tX+bhS zARR)L_l>(I^ya+i{iX&~kAa=F7u-#_=;n5wfPWVbYCa0V6N+YBOR7h5;U4}0>YW_t z%0xa8Uolg+dp10dE&4%DedUVHB|3bw0accGDO0_d1x9L09r%OItSForX415Me~=1~ zf`7?cy?PMw^%;wyLVf~^rnTY@s;g^-dvarT^-<>(;RnG`$L<&v&#tG5EXWtH-GVz( zwlmm%RLb%sO4fu9tZ<@A-uQ5>Mt*IM+bK36@381;x%D+=AcZ{4HFCRDT?$nfwcUi~ zPDhpXSt`zyM)$Qh9mQ?75&Li9G{pE%x;4?5-f*E;aQrYtLJr$Ec%$ zw_weBfVmLP2XA$BV;DArnTRgm@@!$7prFe%{y-#gLQOp3QO5_;SEc$tzKlNS(ft8R z1#A8Rs?$*VZaP2gcbyKInjp4K6>59&Ai#2B*ovhKA0uPh-jS?2qaJH|wlcR1wrb*} zNs%4q=y`u3W1T|E!5_eBG`0%d#zp#7km4=6w9w>Hp&&zv{0oZ*GTs`Q{c9l=*T;jm3O4`14a)Q^VI;UP zf!O8^b>7%JgQAE+;Rnfm=Z>@6sIK~T!W!_C`gEgiv0a9YxBJrdOG~UuL5ic%oVWzW ze2Xc6CIuEr8hvD~AG<0EzEJv>>eAA#U>FHVv z?EHKLT-O6}T52UYYhM|r!x<$zVNBa4)Ta4mtwk+Uz{i6=VPN*krPkFxc>3v5BPRJwlftO){A_KomK2>wJ1h_D5KVWI#Uj2FhY3b^R#Hl6+>p z4K4K1!vjoxaN5*^un-;*eaiYhjI$$K9q%h%pet&wi)p3~NbgZfmbSc&DTAOsa^@IT zxVq*|Etqw9yc6_V(h^$orgw8ouqSeQis!h}&tAbP04uU2LqnqFgVqxzhAhduw6)Y` z*UjS1VvcDvRtx$;BkIbjo)hFBL<^CFObIO(n-T zzc{3EQJZlX7ET}gSQzLWLpKGeG5`_;ka&a`a@oh?*Xs}li=7Y_*ub+=baVY{p^bWY=Y6J%@kzxZ2*tknW+C)Y zjh58~1JRxGg^}*$SiWev5FVeSbCZ@nJ?6Mqf8Y)w&bQvqaa9}qQz-JUq?%$#7=3_a zucz}5OP($>{3f#t3-K@67KqW&dGh;YpBZtM)+oc|QUuECVnW@O2r%jvsIvZpCxi9a zZnhiRxVIbvH^c8#A0BaN)$AvQ-G~$4&e$!{l{(%YaN|E2gwZK-VJuH2=cW+Ur8{~7 zZl@x&D^465(_`W9Th72l5<>m--j)#W`}d-g;DrLPcalb1v=Lpbq(A^%a+!34rcov) zlQr@u^~Z_QU0P8SOG_wn^qwb4+8>twR0_kL6$8ZM5EV9&8S_DiF zUa>V!O9frjC~x@$+53T?6HyFvMv2GVPT;1pu01qPvjXsE;KFXI0yxI$4-iO^7y1O) zr+w`nm>>vn!ZZYJH{#09Nnl0YeW z29uLmn4^&^@28LObMik2*3S!e194jvj=`gmX}^qYsjm z0BVCWD00q{JxX>H;+XYgrJnGrwN(BY+tS+P#&TzX+>EYYB+crF*TJ1}aqq~9`7yj9 zOM0|)S*_Dg>f_~=0QVi&Cj7qk*TM{(uWt*2IEQw>JXTlmNwrW2xwqFvfPzFqJ7-xGcM^+voKqwMUs8yL^>|sS05a8)`FnqW=&s0&1B?d9 zR?oW_(FU873+Ay{aoUQc?3y>k*5jJ{miuiB3I>FOk4$n_nu;+V>YZrZP6%^UZfQT) z;Ml-NiaQ0dT5^pnAiY~sm|wx`vGTMR{Ff|fmS`^@q4|NTu^aP)v_GR-APy}wkIlve z4bxUg<@Wu@xZlSQ(w%!2Uk=P%aiQwseUBpk05M(i6?eV9VuZJRlA=Y;CDxf^BENrI zM`$(yF~pnNR1m{@iPsGwd)6Q)APn5*>#>Npu2U6MLfF9^^kCBOJZ)8b?o@$gYeEx| zX&dtrBF;;|b%o7#Z>8x}6{>Jy0DT0IT6TXivugY#1!jg^Q8Ln1`x0v1r7)>Is%6G0 zyif3Ua3dW|2I7Nn(^V^x28=KEhTQp8urD8kq^quVm8>CH51IC+{K~+lY5!7=C@R~S zqSI_C&X9ScsTN_so-0DLbA{91Wqi8)crIuW{J7RsE67kaqaO>7BhK5)_69{NH9fys z*0+j>VKl^~Ur1o$hp>aMs@7QHv4CwpRw*u- zk*t(fbqU;P_1Ht#YQ(erfF$Qy5xb5w58>&jQ#|K4#gbD8OpKfP7tr~Yd2?IZyjZB_b7UYU)wfMThvNi6|R|AA!YM(BjY$(wM(CS33 zOOf6jv&Y#Rsbu%>YSL=`$SEfQO2T$G=(i(E0-uofw@53B&U7(eDq%{h&CLQRP0=#R z#qDaHnNy5=7ee>12Ep>*zV%X@tlaq#wx4gkMzDT*V5o%aw8{iuefj*dC4@eAsEcY! zxQTX_%c^nbaW9jM=v!S%Eh4d2nl8xr2ihO?;Q)B}AcGo3yWd*?R-^z3a1{u(TPG!x ziI$u)e~7FfNDO3IvV()x6=x8JZQ&d@bjj`Qi6G(vK8r^SnbnW~^zBSM16HvtR2UP0 zWLP~xm@{g&G;WS=ElMjsJ~w;ORqsC0!wJp?8G_1NyE-85);ip)Pu;|qR-`e+ALlJ; zJ5a@%UX=^rvj%4fP3BaE({`%+jby&?H+ML_#_x`uH?Vt`_!M^$DJnyB4Fvt=2r8O~|D zOk>Un=%hMRNva*eAU&6wA)6h;I>O`PDO3nW8{4Ey;!;s*slL7enrk9tmJODQj7`hr(3mKS^o2+8bW+zeiHlT)tGebOEzdE0ce7!(=J3EIDB+qb#Z zArc)*4&_4Y_RU?1q72p%)3q!bNthLSn;aTGK?zkhBLD(LHqt}>y|->Gm{J#A1XaP9 ztc^(w&H)+;P+MRV!tmH?cOIo4C|yGJ+OS^BtV~0rIY)wHG@Z?1NP2U=J7lO!0@a+0 zIs;CmKWpo4x!*y}YIgdc(DKs`{0tXu%e$Jj=<;C;&1PS!)Ng*rQm-xTDiCTd= zVYX3wYqXI_w8ucub`!kqjafevx#Y^0i( z!|%zY0AebsjCdl}-8Lx5xXz=d76-ZGuOFWO2dEf}YiZ<;60h8-V^6os+n|B=rBHf= zHX6Gpg!A}&jrd;w6-}!d=%>NkRDrWj)opPig^hBo4DolBwK`j+g|teRU9;{&ic5za z5st4d#y?dgY^Hz0nnQrs=K-$vEM#*Ta!jdbVQQBQBYJNka%)ISAwAX~@uZnXV>p%D z!W&^nakqA{Pd2eG35sJISpMyYX>fCulWk7_lb5QV9M-mWIcy(s*MW7a*r+!|9(B3_ z?rtiJf>JuLy$ayyKRe58E+@N5BO7-vrWS_vYNWFK`;9ae$Ehx|UsXUN*M!yNcQBn) zJfSYd2+FCwT(R1$YDKHJSLPxB_UO+`cH&`hG;>L+Z zv|GV^#Yy@S!Sy7>mFhIxK?&&1a*@2*^V4-!^4%+vDa9L@qW)0I)uCg|wEmRjXdUtU zW^ZE^XZbssT7&^MCN(-ac!J}@Uk6@N+`kCwW$@@{CI7tFVu}}`RG$=&6jF1%aCj}a zU|~TuC|v}yKfj#yrdjjuNzE&adLh9K>|{M~$JRxNF^l(s4NELSwHQZ*< z#m;5X>&^?t5u4|@n_(INeoTqAi{td z5tRgUS;XjTHDz7F1j0cr!rE&bBQma>t{n0pdiAl8jXI!;b=@-d=$x+c65-cF`puvZ_Up8reU*%W8{0M8G!hqQIvy< zp^cKDPrOiLP!H4yz3Jib`{vd*MAY{&FK1_=g@^CX;{}aKo3zfIDoMnhk0U3h>Y}HWw`#@Xqx&w@W7YtlCA)wRo{bb(}M4J?^_5asi`+4v9(|MqCgEiUsMfDHIR zmn!5_C;@^4&7p$Ptk}C@MZI^zNp7-O^ym}3(ozX79{%pgfD1e2<6Tff4&b>jwNbD> z58}{1edKwGF=rm5mXlCHP<&NKN7N#5CqgXzSnKvhC7z8Ah!eVv{sW{nd2nffGO~O( z%`tuCx5~QHB2&%&G|$B7AtLe7KCa0mwVvG@5C~au^~q8gRSXERT#*F4**$~gv7ovW zxu=_A*;_T)-_4XxZ^wOcf0)J|@YN45gdmfCNRTh9SNi?hPc}1V15AP;+IN1NkGc66|M76n}DkRpXy=oP9CF{ za6_`O@(CZ$5fT*5%=O@0V_Qa-W>^&Nn1LWV?M&??Gd3d%06lXl$q;iF*QXHDta z&aPJeg;C3^2VQ~?Z31~f9|cqWY0#FGGYd{vPB3I-0YBwI%hPO^6o2;CtOOb+0o$1gw?R`{ch1D{wAwoy zH$7GFqm9lYFa{`xqlZ(MKGpbA3NFTlO${#?f-)&J*iB3&Zx)-qw-Q6Oo!=K4gmzx= zU~+*QoyZ-uPu~hF%CK=gSdcqRD8H-0EMVWJerml%gL@#kL6den0lJt%!^t8rrR2s; zs2aaESA*2px3)0d&_@kcXW2-dtgHxm&rWf0%TO8mTetNH>OL1Lw^Er3;S8Bn#KVA* zxTumLSam?WaSNRGM`k^gs zFnJ%nYWvKVjhB&;@CLRaEcVaE1TVm2$aMZiKI$m;@_soca4qQ^Zb5MLIGkRXO)F-y zq(x0{=q}aT$yfkOeO&x{KNF{{zGfTj|MFQ6_*Tkd#nEt1@ zGwC31sytx@`~e<|4k1zU>4(qcmwTH^@Li>oDUpjafGpA6-o}(in^HCTDa<^nZ`n`a zb>=Pi2O2zSoSfAj+%n=fGzoNY!Ae(TNBF=VRt`uNiaujbJ|nY$efb0A5WkljYE#Z11JzSjr) zR(v~wF5uS+S;o27ahj|V5a=fuT^~M!FZ{S}biLG!Nz zCe|Twr>kV4ZUETl576g#uhXPjVA?I+W)An^nhsOcPD}}oM|^BdM&-ZCfV8Z<)X>%D zKS@>p_WJS25uX)OjN3YJybWQQl-%QN^&m29YRZ`H7Bi!fxp3!1o9!)U!#{FQHG5Ba3i%E?(!r_)3$Ute%_H z5YC~##i-q;`&$1gj!;@YNGz_#7TEpvJa>RWik(*&r`=rr@E@;i;a9I~lts$mBt0d- zEH%}Pe@(yhn@vPd+l&PQjEBr~ZI^8+-Oyrtv z(vks2{_gsobG5J#yu-L;sxUxsnN1b!R7}8fM}t1mnE$I0Xw5Od^!H;G@hV&wKS0M~ z$b#m>kCocuFU<*C=O9Aw5K9NFdlbQIpzS!{D@s(;Y-UmE(zm%MreF&Cx`Lp91-S}i zUJ-HXfdpkXMEsq1q)JUX%N2Z>Z zC}#?ZH^1&bzb0+zn=JF!{hW-Iw{8kc&0`KH^1M@0jc>gq#QJRvZfSE}@A@3V3UK}zqCjO{X5cVg-;lB*V-wffAHsg9(e;vs(*V&78 zHxcc&#FeL+3nTazmJh@l1=w#8a!_-!JWc(S^I4YwV<{v(td^|asIBxUjju*15@Ev_ zc-w6fp?|QDkT{Y7s@QyTH4S7CPH1!=Edq>}!Pn;1pDt>xrmIpYxtDvTJ+EKGZ5o@tFDBnhHsx31@4f!?(E_|8* zf|ofG3@`0H)+Io3hzRGc z*$~9tVa|>h`=HQqmP6XViWOjLR&gn@-YKJ*hM{f+YiU@ES8P_j(r*o75it6Vk(WYO zZlYAj-=5)8-7_&*g`)TadE+$%rqVhtd<7!boW|LD*08)_qe zoHb>tJkS5+FqQA|TAl5iq}ht!wTPAX3shvi;^0(H6P~NdHut*e((R_b@aJ-6$H!u~ z`3p2%QJ`=9Wij#`Mc_FZGEMMJBM*C@XE^g|i9!e&zNR_pH9*?sDH+LRUU==lO(uZy zjSxzQvWk>sBvg>?n#L^t>pH2K>O9&Av_>#e* z=`7mTizI5YIWNuZ!KlNewVE}K7r9N1Or0Wy7*3+rks@T0x3pT8vbVa&~6u?{M|(Oy8tfq{xw6WtAHw0?|sO3E>Bx@GjqE4kwoVP=})jepSd$j z1f;+z>t{(+TX5Opl8da~E0d)U6!K?%euN_Fw{g-x>R}&n0HoA&(gpvuFf}FBm%8LJ zO}p=3s_>6hs7SpLVcxH-tm5ixNEL{DfCb~k(f@21dbSNf0&41hVM4Fu)c;!06Yqkd zAnV*2#Vbw>j^Vyq+7kNVa^Nas_@Z4D54QC^4Th9KxAvlo!L8{7}^skk@R`Ywzr__Mv zg?m86)pG#ky$Zk%HsGPxi&(3M+A9$Sy9_{ohL)L`@0uFrp2=0*R?l0U&`UcP7ZqZC zosYMmwkGN=kae|$#=ATPK*ikvPG#8XZ$<2^+%xZAd9}ipXB+FGktMAeACozpG6nbd zy^ZVR0YsI8kA+NP2<4?HCSB<5@Br(r9ZSy4ekI3X<#EQ$7|JxN_tna2cbQ4ObpH0_ z>qX}!L5S#moNDrgMYVVR1TP=lqc(VI%@2U>JV6!U7uWy!Z3A?MXmGxi(K-8F7T>w+_(m(lHDeQmY5hZB6Xs8l$jD4g3if>xTFHv|O zCui=Msx@2>pH1Ey*RU6qkkmB12LpJEG1-#EfKH^Ow!Sa#<5gW+Q$Kj$6kQYrugVY< zu7^8o&fC*d6?(lHe`^)U_Y)@QUd!z20GV^<CVeL;_A;Mm7L_mTwmkR6JzQF~)C!=uOD9+NYAU5zbkiL~ z8TY-L=28G-yRo;YtvjqpF$Gl#v=;M_F0jR!NhR9GpDLoJ-^NZjnM=RsXRdplGSB+y z{`FtDi$Vpm+#I*Js7V@^ zp7Dzh98%fWoAS4oeZn=2K*_8PM_y_Gpvcu1zS<(@He+XtZi*{>{YoqRyI~csZ&Jg9 z=7wVxX`zznq{ag7V_>%I97$dH!2$oM{~5VVZ2%y?6J3&DC$bu=5h3 zj1_Sgvz5uW=fOvj@QMSd_+(c)Ri>Wg^Pv8gQLq8;11{;>X_h5x+x*G}k3LxBPnkGJ zG@x!o%sx5*;aaeGNJtoP#Ps3q*RrP+6kp_$_VdoK+sXA(i6?6%R2bu2gL$rtzsEv3 z<>7nRo#kNPq5j2z>6Vc*i!Hfwo~@jO#D7o#SsIm7;0> zCdwFy|B9*7MM-8;-*{g^hhOjlOKozP+UPf0XB6cQ!}RhSaRvWkFOd^r z^0xw=gP%dxJEg&HI;VaDt$E`#Nyu|)>b8uB>su_QHfp2Ls)-v7m-kUw>yUVPP5TXi z6i@5nY}2xw$Csnp>Vljtp=fWn)R}h+ftDdp-uG50wa5cNLj||cBjD-}z3{i9FitYV zr4%L*&AzZQfh3LcU8PWQ8eG>_n-VCXloo$7X1Jh(>y_}M%CHLyCG<*Gs0jvKJFufR zBzByci}{<0b`28W5yogfzR#BsuIlhF-p!oE(&NkM+4I)6pvC0KE1FECk+^(fp>bnl z{Rf*N!8BdY)3N87O@&TfoKv-M5prX*-5j@Fy6vX+Se|e@1w)NZ924Dwn??e^?aAp@ zW@KgI%X`#Mt+9GQh8DFE1)q247k*{Fb@Qm$jVG{JRwv;6am$@fx#2y?kv!V^MeJhh z2EEkiztldg4p{zP0y#W$9k@m;G5EV_tOmjQX6g=zu3R zhyZ~kc`|VLZ|AHN_Crz-hGwB(Z3@7ymJZsle=!Uezu6dRQ=g&2-`Rk(NC^4}EFCx0 zeJt$%WgP|?q9MJ&tvrj;?EWc`NfV%?Ie#huq9M%P<8|jse0oU0qRQjMnIellE@8$Z zjtO3~iphECWGRpO6&>S?S<*=p`8q{A@YUu!yUG92!fR%#a_a~R2l&hW zAo*O#sAsPKD2JPQWB4$39*+LM46u4%9u`XJg(Sy%D9$$;S(cQ+FExS(8{st0xXjt) z_iXyZbu^j5lzpLiK%NVse%qWp`C2FAy{?YOX%o+$;lOJXyq_YR%A6I9N=$$Ms#f|1 zSg%L~!%9Y}lj%elobZ>7Dn(6X%}|!JAWgg+?yLKZLx!UJSZ-l}HUf7$D?U^S#oE88 zxi60m+-LL~&g*dH2#Jp~-q}ncE_nOwDbeQ*;1=nhk4HLde$vlrqRU)NqZ-S0=FZxq zswbNc2J{NGy+7qANl3z>oNurS!3UC;?^V~vBJi=R&*0UH*8#$-( zxj3wL)6eipLKGB1Ax}%|dB@5r$4PHOPDS36v&4bKec#%DUU)Us`i3EVq*LwWs zeT8e@=&Nz7#u+1lAlg3Qwu7thPO2RPSB&S{rl^O&-HE$SaIK^}XSW!zk2Q6o0_?b!W!u$kR9HR>acWT1_+M-RoWEi>w( zsRl=PrO#;47WL_JAF}lnt$4h~&AMDduWg>MZ>3Y1RmUR=R$R_3*$~q;WwVkqIdW!g zx7Au(v=X~q25&Q_+I{ZmOJQ^x_Y^Swk2mH2mwkQHcMJ6=i1vWGJeqqilZB;K-Xwip zObX`3E-Q@V1p!~!f6V+7_sG#8>p8if(uM6GprW)c<@_6Q7>U7daz)M^tqKg|gPE-? z3sh!HrOOY&Vc%&dLou#>y_U)m;i8w%4>%T@>Y$+j8y&b$0dK&F!SKa~E)_A$)ZvYe z{Dx`Qc&yh3>bAfUwg;-A|kUIEIUK43S*~cKC^CPA<sc_&1l1tYBLLr$kREFpmUBiFnEvqsmX1-2cN4s$9qKa zZ$AH1JEps^O9{1F?q5Vs0{zrT79o5tp?u&wefHdj7NL6Xlm8v351jIsz zh;#!4qy-3xfbQ$y)21<(qSUzaq#}9R9Gj*%dYOR!U%%hw{-wZoPXhqMuK@->=oZTOMIIIMsw{ z;Q3J9eP)wN|o|J#Y_4}oI4>JJ^m#GQ!K>@l6B6wML)@s_Uda^qNP9Gx0$q z5#-RK-pslL*_SmM;+977CG0~Bq&&XV5%n*sa*i<>1CIn{`A+d|?5%jMo@_RlVz$|I zg_JDF0MFh!l~d#%32xY~eELG&{qak57;hkG4$P?vBOqLWc{(GSzh_cZ!(8ChisMY{x!n` zvd8|FT5{YwX}U#N4_3X&nh#8-jr)+=S+Uq}8i|YGPF5qKZ((IGAHbiI_fo5y4`$aS z#VJ(+G&Rg|3{fS&t+9BvucAeHH0(LNx6yyD{|>i5T|Wgy>Qyz5dI1rFSQ${O)RBmN zXVyLm;k^%M(S3LCeF~dHbRMXw>d?l%%{^0!Q}Y9^J?5tgx2sv37;d$*X}nV9YaG$p>3ANkimHWu=gK@RJeagrs4XMWR*hnF!kNC>>%mZKH??S zg@x7GB(H~b)|_v88%7r9A3128|AVFS+!RDhFz?e;{@&t){!J!>V1346ce!R_n#_5<)bbb28cJhVNT1y zMxL(S&!iV;EAB?>&sV)>ew}De&0YDXw&4=t{ckdgT?~FHdUqwM`&VL$;&`cBQ^KdZ zqiOekZ=Sr%Em7wG1oFFu1qFzSU}r*1$i`uH-AmXANu4&)j)=748m7T zrDim5Pn`+#WO)GJh-b=d<{^#}$|y6S~UMylZWj`d1I>F{Nv*b=xIo{WEiswC5UcuP?&6@KI6fPK zy#mmJTZV)~QAVKCrC2D*&t}_87xP-vg13w?+|^djOHdGHk_1&dJg-Zqh@ zFXJAlOGWyZCSqQD=DN-n%CClsb(q$>a;deN+tv~ymXo+)zAg2H@Nxotq*HjzNMPI~ z{W3Pzf9z6|gA8FIKfX;X;Pt}{iBuY!9G@Lu;DJQ(@rz`JgIiNa%$pF8DnfOpd4%Xv zy^Fl=K#ejcJ|&EH1AG8NCPSl^&HH3&K}%2oDbRJy1AGagd)(&1zg*Ycm=127FV+7+ zvU#?j{^SMge*Df?^~sT5*|?_H84+zPdZl2V!6a-Yt82YgU9>xTN=Z_dsZ{;k4Np5s z-&7NobXR72`6q(ECCu;RMvz%SiJ^n$XT=?( zNyo%(D+5L2(Z=htq^b)m{+umtet2P~I2I!7s2_w6%%c}kqYvrnO$aU$FAMN1wz;<$ zve2{DA;1-O;@!uyv&_&zR&lfOxO_UEc*6<1#dOYQK^`3R;nQ(CF7N?6RQj!%t~`}o z#n=||8Zcqm>cIWn5S(pFP&gC)rrmWJ)M7Ro!m(+d&Wmxa^!yovwKj0Hc7YQ6;u$)5 zFAR4$d_2^8nZ<)oXnTh{#oCqN{f{3s;O||Wf2PcnX5DXJth7`A{?g}|JGu*4&xiKz zrODrJUFW;KKHkRoM;}3864U@nSj#CqskCq1+L(FyfK}ARF}C6cb=b#gL(pr)c@LNe z?ij7oDI22-Nl-FC)cSo8x!W+tpG*2i8lLb(&E{F$34R&Nb^TSN$H~qMt;biCp4Z%D4yeW=@d>Jk{c+)yx;S=oNxL?K=uIp zyQuK?XC^Oy)koFL>_CV*-q^B*+v^!wYc5~n6ZYk#9xJ@Ow{xrOh2-`xcK|SyPyXL= zx5#y1eW!Of1!Q6X6-WVQJk@mM3SKaFr)I0UOe4#!CUtmcgta14oW5@1vO zkY+!5SV=S`6rcgRha+yXE)Pg_KiYW5YeR0d==rz+WCt4)U)@@0M zC)XdTF+nFq8{kpX7(T$&8+P}XaxJF6c5e28wQtj@Q^AD)oJYY$L|zvO>chiWeBR55 zFUMzhQ*ys)0K3$)$N2-`+QS76L$C}wdwF=i%-2g%i{GZrC(g$eYrJlXsaWJ%CMOGw zW!dZFn%A2}RD0&(W%fEzbszAmlAV6}o72j5ESvIH2E|525eaA&=WhnW2jjsaPr^F2 znbR_X9`Sv`R7bxn_A;y_3Imt;bOB;wTEBE z++n6?S-(Qt6Kxs!?>^*{-@4c*GDm-f>MK(DI~SVe{LM;$euxQPVuSe;63~@N{>sm{ zNLO%2A5=1O2d-{ZyVf1E=v2+t*#xdVHM>oehm}Q8KGfHnfivOv!Zn3v7oTa;OnLYX z7-?=AURkS2HsrCnQ#vAWh)(7=p^Rr9dY*Nau!BQ%3S5rr%#y-8CBFSMvI^{*hQu!f z_A#G)BDVmrS4?lrHOjuVJC(~Ces>|X=KVPOJY!)=*!;yM6v}ZuH^JTgcCPRXHpUm^ zcGeiM7UQNMZM$0u(JrOs02BnY!4c;K2O zE!%{kvEz46z3n~I+V{j}k7IQ|HdB3_zuB>8YZ3)B_y}#*slf|I9C9hy%ABZl`gjNr zu*JQ1vNYfWbO3Kr3V;tXx-tY=53>b`3e5x;j%?W^-=)aRCsV6@x{{Uw(4$@P?))kteE;0@Kna^BKhawx@FTUKB}_21_BXUL^~W^(W%v@wQP3#1QmI zB#Ij^k)#wC!gP;CR5N5?TU%uP8RKdqZO1n44+dDuayBi&x;NuKe4w4CrU5H< z*D~qmFW2NWDPE>9-XcQ_ELT;A`L#2+Eir(O{wR?nrByWqmhRy^qos3dY}+hO1#)dW z1?6C#Pgl~V`$Z27gxpV6kb;73^w5zW26Q_e6lpS1o9svnVQW0Nr?o{XbJO51I&HQ3 z32@l{vlVPT3hW6Mz->aUQAXpR9OOs5y@h+&^jj@IM3aSp4+fAs%b;^m2->+7yGPLT z+oJ`mCMMVyZgA9{=l1I47vano-KPEnHZYZ$ZbHhGCr|x~|Fpbgy zXUAT25TvVeCcG<4ee&XPlXGk#ukY!xiu~Qt&U56cR+g6cy_b|~F?&&PY8(?1M5}`z zxWC_GP(jEXw`^>%T#MQfxZL6+YU+GWMHo?f2ePmo4^SmPHg0JuoaK;cVd~R`0H(gh z!9H*E`&ISy@kW-bUz?MY$u}8wg7k=DngGq@T;@^dzspl;X;o+=? zi1(?X8c4=wk<)G_8wYh4CBW-a7S|W5NLR^5=zAEhKj!{Iet!PC{ob7DC+!m zR#8weQ(Z8)c}mr+t8)E~=2Ubku@B1UGoIOtGwIWMDad>nSR+UQ{erZdu-Cp6MtjRY z!I-X>QN5cwtUp-;ysVX|(#H0kBm{$n7Kj`>!n|_DBcT~ zp_!XtXLm9DOsK|G#tBk*!Zoa2y)9>@7vJUyGG?&o728;Rp=@O-9@J%fqB-)C7#^7+bah?Uca4fTW-AQBe6(IU1(om^IXe=Sh<8E&tIAIWHh!TkL;if$CY-O%{EZVJP@D*Y=yNdYJaS z3g9nRR8*kWL(;;TzYwZ!#240r^Tcyh0WGA$`;l(%F%Oi~tJXhX--K4iv^#1bFft$T zAZ$P=oZD8xt=U&^9Pm*~D2N<4j^$oWDM--tQDEu84R?R0^syb;Nk9wI7@;LcVmNu+ zeV~cFl}r}e0phmkdY+-?*U@s4+)vkoFViE*W5~#!Z#`+;%f1L#CSD8uytSI>}<8dSuXG6C!!7XcePzSMWq$U zatPE?x7b?90jOwEmk%xqvmSjI34|iF=o!Z^mA#?_&zKnN`}P;IrC;CrMoWIRVeadY zB>9i;un>Zh&vFvM3>*G<2gJN;pZ}daSAJ0JMjjvt(6t+Ik!8w&#kkD38^sO4T(3_?sykE)p zLjE&A&5``}Q!AU%c8hKy&`v+1<~eJdK4MDLQ!hiLNbNOON_a|Vg={BD zf?$i+vc{<-)GDVUd)vfuInA}>34`d04^WXn(j!S;_UB~JDU#(?z1OsG34lFZC5j-Q%V^{u1aF@vKS%Y?o;_hEKZ(=VGZ*n7cykg&05`$RsRXw!-Rq~NVF z)Q|q-!L=y`@$KV<_ov^#iw8NenLfByzVrv=Yxy_Xv;wim-fZs=EEzDfZoMTveWCj6 zze?^+{!fnekFpbdt(PtmoDcNIQA!OB`$umL9-WAq-MGs2le*ptXF_;Dk_XU5M}o({ zKqQ5mg6c(~>Y`bfC)Ax4CrGoC9vr=!=xSE!FMQG8SgQr{FH4ZI+++uic`gKC>WM;i z?F_oH;mA7KHS4*P)ZIv<9f`MmbHqun!9Hlne9C+CiELZ}5rExjY-{cPY$rM={rP^z zXjq9o&Ky(m4g-u6dg~3UPqmilrWSb{6VYQV>arM&NDyq&k^|L!ftK&Rh|RX z5>(?+byW^Wab*dr%LV}lj`wD5U%TaYeWvg>Js@vz2$TE)`SX4Bh*uEYh>>8X(=%D1 zc$JHW9g(PUEpeEYV5!OoOKYREmFxyd+IqpB1JX_`@w7GQz($xfl1(%H(8$tPUMv>x zN0Y}HaGS<6{g%o*wsWAX@-~j!_i4V0xCxyhfM|evK?jRS62S5MP1frQ)PZG}Wl#3( zf0H2!fCf{vVR|o0@J9Fh@&_a3-DbyOMjAuCd7K|)_g0(x-p|$V8qNHOxVcRQJ?_p9 zCtbqJ#^y{bevm-&baNcxwV(Nw1-6Ykj=FF`!|9?AB57r=`GogZwrF7t2?M0y8N|{C zf-ofb=SaWoaab9lfC=D6UMfP)qvlH{{7k4&LmqBHJ z_)Qi+>OFH3&qs;J{nPW@ZwDqi=J{tMu6nt+!?5xDMIIiqGz1iRQ&{{PB z`J0T|jk8HNr(h!mW%LncXz#GfaVHY>G~&CE8Y4FLc}JG&zRypSzrU%MCfFnrPLsGQ zGj7yfwYgVO7C7!*+|M0T*h_pKuRGGXH3S&F;q)b^iwt?^Wq5+;`O3lL>Z%(<=COO`7FDX|pt51)&|GB&TWnk)boK)%% zoX|;({kevD8|^L-Mjbz@(1SvRxY{(+^^^JC#czxtQ3N9uE5M=G=?yJ`KJ`%Xe_IBs zEAnIdaP2^55#t&EE_~N!FsH7Bu2-%;_SeII4z3Wmy?VV%mSt&EHL*75?VMRkVN*1K z7=eiHOhLqNn8{t)#b~>{Ur93qj2i_pptxEiZS(1$551K?HMHn7HAeDA(pK5&KTUCP zV~20;%+pZvz0jLl`ukU%u>jgY7YBISG2A^hACn@+wce$(21gx;df+W3Md>1+p6~JJ zCS%QhlO=fpiBUl=$j=#7*Nl#D|I&%*&G6tIkSeY-ce*z!-~8F@RM&9jh???$A-;dO z`N09mndLa~R<4`8Uh#(Jlj=adV_}cY414{YN9jo8h+rU#DWKX=Pg8Ke;6pl5FNaVIU z{8Rf<^#7=|_#fs*L{|nrgLwQIKsS#87^zIM{UFJyzn$7%sAt6<{-wkIWrqLA^#AV% z`l}6e%kiIsKV3Kj4d^vfR-OVmeMVIGrtF^Db zm8^BWK+!Kvb&UdkSM!mqVoP+WFbK7XsDA_$O$3LTnCb0XzFjWeI>z}Kb9ETYD`3W zAWx)}%u79Af?w^vDHU9T6S4f;OR@l3{JV}ch6iM+wVr>(<7>SkqiVwVR^{VOR;!)AD^Lz6Uc)7zs!p+-QW z4(}O?>4b#XOQoT7r&Z)pR))#}t+n~7wJlQ50~3|{+|8ST8X=JGVcS|bc@RnWuW?Ze zUCYp-Ch^-647=^98pc$9z@?g+TPJpiCw(Xx+57`fL1|`)mfJz zAkkDmeq^{TosB9QdUaMbP;dPh-6qm~mGd|_1b(bEhxogDa#D00R?fzu)7#Hpi;+Tw zZCtG$SH#$RQ=RR!_S9uNwf9Ge#rk=jKceqW_wrhO!8P4>mJMfGBCr|~JS3uo<8F?M zvCi~RR^L*pL8qe7r^XpZQk^NWLu!q_0-}k|^r`fQ*v4$AS=n)5a9_Fx(xn!bfI^Hobmn1Yut& zSQv`<8aiynkvV4ZAy48%{N0sQGSSS#r0qY}j31&%=Yd7t*NHzxA{XN@tO$7tVjybx z^835|w2lv$fs|pJ;j90ty$xpY;}F zsfWuG^Qbd7|KX!9|FvWP_0h?4|E^(xLd1XYUDjYa=siYg#gP>zpq z6nNNe8{1;dE0O9Yw~6@kJox8X$QTV}$lh+p-5y5TjHe;@u0k`1)fkKoD)*eT_G$9v zKc>69|7sz3dRo6j?o)d^$pS$QeGcN*#*=R!RL;#97V7z|8T7m#%O4TUXdbwcs^rlV zK>vh~MY8a3TTX^LX0yGzzVplhX%!G5QzM*irLi!17>5k-W0)-O*{)PV_W1FYsRkx) zTWas9(?20oYo~xx08;I~?6~PL-_>znyvX1;{?$P7SF5Y(mX<52x*E2#Z_qv+6@C9O zRW?wu{b@}7wc&;Ssn`9#-c|mi55TC^r%8@Obbp{CDlCV{Ydkb|V{y`xN&<8j@>2}B zEk#m-Rj=8njfj7S3&8jZA-E>n+k0I+K0P6&uTE`OCkKzGeWH7r?XYVGqL=e_D|YfK z6w>TI{CTqY%V@D-lAO?_CCW|uwz}xFkHY3;FFxC-SkgO+3tfL^`X+WLMMyG9Tq{N2 zBx85qCw!S{7KuJiQa93yQ#Z<0Jg#5Vn|q*E_C~srG?XNu|a6K6A6}u(+f>!)p zE|G>{gqcIoqkd!wBo1rQ*o*`hx~D&R+SsVg_oW#!IJ~JZeZ^;~n0+hz?`{n2W-4T7 zInmOss|#m^aR?8kv%!qCH+~CtVyW1NjVn%up9|g`Vq1|tBxc-k5=@~4s@9X9?Kyhr zYDHJ9-WNBR&{RQ~wS|{^yUoDDU6CpdJQ0>Q{G}=`Ys2SNNB$5mC9-3G3j&lo7^KyeFbL|-yk%XS(}YUX<7yil`CHzQhVGrF+fJ8lUbAG7p_hf2 zH&Q#PI&P_hg>)I4UtD)uxi)fJjVI$D#ytfR=68A>CQOLK(j~D%-x<7F+GN(F!q>Dm zeK90F<+{_HxM(rvl^5E=TE9B{oeJ>Dw09`5nA-?)YVl9^_s+kDWOp*_{0G zO)~NhcE~^6vVpHJyoDFjP6qX#>w>dXpPqm1Jg#{EB-OBHMAGQelBC*)F*N}b?v9R* zimbm3m_N-?{|o5_$FF?^BJh`j51^69$l$EaZIFqp%x;Ep*7n6k-#WU>wE16Za-dPjfF_oH2W*1T;k(s?B1Tl)j`-RQ^&#BX3s)X8-)w!+KT`2eOUYFM z4uS}7pi7f)3MN_PI^gGkl^-hQJ#lK!B!1b}a?M=LPrb?cg3ey=rX)pYb~UJTgnX;6 zP4v5>rS<)BEL1ALrLDP*!&JZ7RyuvtjpL&E>Nhg)UhkgZ`hQ&09qbmJO2Zf?i5i&f z3yV;=eIZnBhuR>K|1N2v+-Q97Rm`OZy{AJ9v(YP8$WrX$(1MY0CW3>5Um-!2?u}4O zvY}98dvp6!j6d$!=iaw#s#hr56m0kY@xFs@0t~pt2rPeakJaPslv3#1os|jk15t=Z zx&6i4{?%+oRmGrWnV|PsuK1{Mb*1H<3nmQEx{;k60LFdn1w^#h{M zW`-HBJ*QW1fZ61@(ryP4$yXyvq9`H!O}&^LKK5 zhRIg;p9fYN;_Aa;YUm*rBoal{T#)rf)EZG^qBQ?i?D^j5X;i9Nzr@~+Wr~kv01xW# zCPd)eb-IxBb&1>;oEDJ(qy8vWsk6zRqq!_saeMCVEY;eSRNxJ2jjiBb1kkp7g3#El z9s!ZjRq&93gY{uuYg0|wnLSo6fRZKOXNS*eB31KYGFhen-!C<~dR<3<=GPit_HYQ} z`lvh_oiVVF7Lv{2zQ<-m{vzLjf}aX17>RHk3Dsn5s%pj!Cxkpfxo`QL6R$7hGJAx$ z#!6@VW`=Y1^{dEruNz7~1OBN=)j2_DZ{c*%?Cxp1D^rRb>C5*U44da1n;R3m_Oa^h z>1#t`9QzGdX=S!9T59$k>|!+yH@y__c<3^GWtk<9o6hbIK4j zwQHbcA)OnabK%K{)u*%k+B#em{QK}%8b;T>=f_KO+6XkVk&l^L2AMVHZk70m#}r#{(zpQxvbq@>KP2aLD)n9crR6cmZ)74-yj zyptX@2vDu=qxEA^En0NCw-P!Z+CydoFb@9s8;|0R<1`yf%bRHd!s}X2%wxtFG@xyI3Bbd0RgsGkv6F^za@a?~?$s zYL4vc8sf3sl@YbbiB;lu4tUP~h%x{Dg{V8zz9y`0%BbTgCAtvvd#xt&bY%t)pkl`F zQuX`I1kjmelevv&Dm_{3Q+{f#ocdGlm9@O|XN4%b=+#U*Z8Iq*2)Uq z#^#U!L=`Ny9R1PK@sXRO1~bJ)%ho7Ga`=CRIRW+%KbW^h31&ujXZ=dbQ9DxizRFn5 za*5m%9zNiD&Q^Dgw%5n{zlre!?fPGZ`Tsxly+LDu$JejieJ~y?G5~@1mn-G%Bd2JJ zBF&#}!UBPF|BnyN#ylrLi>8WUAe$mF|K+d-_#95{8Z(QeE5vH#Zq3aS{yAGSL#$q_DFwFoog7|9nyZp8hXYwKw>>l)y@YirshL9i1BBbhv91#XE5wY$+a-B2LLX=ora& zR5`>%OV9&&N;FYO2qV#*psvo1__!J5S00t9uAoG~3&?FlG z1B0!+NH8R^G&~KrsU58p4qJL?;jsU>!1B`TRBa9smzd<&TO*3QVSOW2q$y3!Hr#Md zg*B3rGfN)l8S@GHV4{~M%DlaaG>~=9WJq3Y{%D^*sbMiH)~auZc^N47!EUr9IDAn6 z%+%|nm6F)3=dJX-DKmTW42pU@NA)j6lg7dRJKS7O_lpwSEy18_1d<^XpDMzHuzKycKu5|p&a5O{Zk9q@Bn(~Z|Sgb zVO~%Edd;AtUV4@#+zYUXes02N>ML!+F{W;gU&1nVTPHJ3F!yB$;u!x`Daj@??~-Y{2rzPPl(;~c-C&^t;OB002g zdAqlKO(k=#|0&~n_P~*WC1$|ei!SK`)TM)m8VRD`6u44{L(DB3Zb?I3?L=y=XX*2` zeZy$PA_nC9dD(<0CDR&4eHEqvl>`BzE?~@^%DZa-1d=_iQxMF zEdTv`sr_kHC(otnBnOVwOhc!}lhqnxJ$D-}bZ zK02-lTzfac^cDf$QRuC26D0r##WeMR8sz5(%e`iG-*QJ~l;>MN%!zf*4>Smu$-aV{ zEi}9@E}hQ0c9x?lOn8S?B`V{c-(t#DFt7Rxbr`1J)B{!(7g2hCYV*x^BNxxhUzK=8 z+bNxO4W2}($0cbKI&(LI?8-}?@hA;P0orbE$G>b!7#7M`o0>$U$3%;DXZi1bxhC{; z!_E!Zayua5SEv0Ff=%#<7ckM$y3ZsZaG)g+e) z^&8LXtjv-;*b6?} zW78SEvzs}6UOFYG(WJ&R!25+ZMRuO15_B0WL6m}=BXSX(O01#5rg(R&*N2OhKJKy% z_WTdn_H9h3(h=qAbGITK}CuBBEaaWn?df1j!8 zUsW&te2X^Sf&0qZJ=+)Ru=!~Ryg7O}9B*dJ^tfSCNsw`i#GP(gGh}V>_EJmT>21ol z8rd zjhMc1?;JT9dOCTIGyp%Bgqfw5GPODc`jz#o!$=S&4Df5$zLB%KL!4|6kje?N%Tra@A!SK^$-W?&cbL{2dl&<0-{9ac5I?nO!^nh+)dYQ+vM-eP z&Nfd0)r9SWBA6&?xCYnPjo>uQcRObALOBf$XB}n8ig2dwkV`!o_L-)F!qUjzZ)2=#C^@GvSl1jHhP{LSynAKgLQfKk7dyX1-9`;f z!&byl1{L&vfRI;fH)2x3{dx7F?*>jXukDvzdzgOz=8YSbDT)vj-Li2cN%TwW#Gbmn zqxzLk&#InfXtK29ez{pyeDHKm6hD3B#)rGBP_3o8iu7CG8{q}SXxlHsSWAfNc%9UF zSF7@>+DuAKJCSA2u7=BBZ#s(XCev)#Ddm#}f7!MsRykl1{LrGFxY8fTL3}Qc+hcFJ zxk0mR$}i|pde`)LpU$T0qJ84S(zIO?pe|VI9fm>>QFwyrgl$zbts>OBAB|aAZ-B|i zJPXDJel5B>9axOi6wW^GFk5%Me^WpOx7U-+)5c92Zt9Bxw0X)x9zL3Y1^v=_A5eGs zbB|bWvsig|;_c!bc0e{StXa-qFf|sht~<0RggRPBki4!Is?^D{E5$8 zhJc|$Y9M2Nck8S52TF$;GHJxS)4LU%w&Nj==tc~xjCAx)IES&boU%u3V zG8uT!+(BSHavLQwm270l6Q+9vgvNcjoa)CFTMbRkzEsCr3|i39o2vJSh>0kuzoV9n zBrk5F6X3;3E+cwOn2Fk>>Kb^%9uAapYbo8x4in905PR=y#q(m$NvBs2f4W&}flipu@?LB^s?-8hb`Tf=d|$k}%h6H|qRZk)b|o-abL2?U`@G7o+VOJ0 zf{pKbIT>sfJHopGqVvnX2!^xbem>p0jk9~_1Cd^ye-|0LyQo{3=&jU0?n@Ak;TLX7zAUXCKlm}BWIqf1iI9f7 z4!b>}ZiG0iJz^s0*QkZ?LS|c*B)&F+v(+@8dM-434$H;wYDm@sIJ%dx$5_bc0Stt> zB6_Or=?KKzIpZk6`zzU;qSWa79Bwmne&bu) zXz1hQzrogH(NE3f{v)R=`z?s<=TJ!UyCd1R2?XxGd&fPnA?ocd%`}WOwLWesCLyx+ z|H+xNd5+}$u|A6ZSu&r3y(yh9#sqKw@#W{>Ji+&W)Rb;y*NnafuQ5^T0tRd_q>q_WX$cq>SH>{D$VH z@zIY_A#a&{2hGf0UA(RA@A5(+t90uqln{{vr9+Zr@kWsmMMldG&P})b{S>{d^Q#oA zZ9sRI&s}TgW>K#n3Nxtuq^@0nfVlExMXj9(qLjz21K$O>A0!vD)qm`fj)4^NIj*()E*u6^a4Hc9es8hW?aH zMedlAIIgguP`o6tVECBhfm=`&@no2cI_nj=wk4qwZ}^}c9|CYZ%9-vpeTOlr*JXepS&mJu4Rr}l4TcEmyW$<4 zw_su3yVMS7tvJ2DELq#DU6T%D6@c6o*sIxPm;ts=!Fy1@N~6lyxYoFUKu5)jPZ0mm z{4!4vM51Kl^WAFI6rN_th^Nc3?OWfor8l}t#`ySiApeW{0G#3D(Zdf476f}NgrI9R zpc{TW{l=%U6UW-xr{9})p+|$wNrM3t3qdOs!@KPOUXcaSC5>Ltx86`yT)gt`k%1D! z13=J8y1jZ=fGz%uQ%nEtFgn%H@ad1g96B-=E3vW{+e+p7V9eQ)lC(E*+?KY|wP$01d2R&gDzXvM;*s z2|T~i@pCC6n@L)e6DmL51#lu@mr~uboONQX3}bloW?5iWADz|idr|vLz0hE3)Z*cD ztrGl&7)Si9BRH_ZMu8{6()>7ww|sXmUhu zbx?@DPdi%sO%}8-Er_W>P&F2?d zjiHAci$7<$ND>G_#vEeiUymSKDpm24h>s{hw^fc`1sI+7l{$d8#TC+bEk;ejXBjF8 zHn=J@eHBKIX}z5L13%*Jbk45kWi2Lk$}u%?;jT8l1^R5T+I;f$etjR)3(!Tl6!aJ= zAU&!c0?u`Vvk8P$qy*gUW>YeD^@<8*S7XyV0=|T+*Chf+Y%Xm5aQGJ=Kll^)fFNrN7z7`swpE3E=q{9 zt{bf^^7knOD>p@SQ<$X8zt;)iyMrl z##uIJJjOfcPG$O)mwzDkzja(Qa7uY2s-GV+W^h4rszu|}@tOJJxjI5lsaud=UnESW zavqB4ISw*HE+Jjd^?DqtabJX!hT^N?{Y*f9)X+- zx!td1T`uie?nh^I*GK*H{A|k!S4aWB^H-rPpHS2yX_#j{_9p{V7lIY`Y*Av=e$r{O zc0i#>$BnTRO#yz!aYY&FLQ(7=@v{61g-_^YT|^01kh$$DX|zpfw2hI_jyyskBBTL^ zDPg&`sxf_2Ek??@t}*qS^c|hoSETfv-g3T1NPE9_0OgvsyPYb6+-Hvx;?@EKXWIpp z=MUn4j$E%y+0$|sWC3yku*QB-Qz@vf0w=?t18efOH;nfTSfM&ndms*eyvCYiRXPf~ zq72MeSyy?D;SIHh(K|VbySEfKV@3UJV|S?sJD>Z91@;O&I=&(JCmt0+%Q@P(h z*6sozG~EK^XXpET@*>hXT^F>U)y2*@3B{KP{R-0w(XrFPFJXWZ1UHF&ON(Kr);q_j zR4K1uqyF@MM7d0E6D4v@r`FJ4k5kr{r&x+-4#5qk_B7pFWfsy0Qu_1-kxrDIJ^4IR_noY3pHnbwC zSK{9FcSje{Kx1EEK3fD7VFYKZdr^>X8n`fEAWmtXF@>T!3WYGHHh2d}k#ieb7))NiSukgcVo3;fuaXi$&^4v5hYE`0ZC zwcc~?b?>#H=^L1&*^KTetauup6C0HvO*BG$+&FFusj0J4w0b-=2I{_)YINH-{h?PU zIjXcOw|@A%vE(HD9Pt{US1=Og4`Qs%W|4>~R|L(wRnuJpd`d2V)S=IDO1`@l{`UM} zn5Ip9$_(@N&;M>g%JlpDzf~gsML*~d4Wa)uSA|xe1kZ(kOEue)3KnDKwXGUIw+emr zT;~-Xsm)`$qrd5kT>Zu*lz4-UJfSV&5JY08%l^fs#B{m_EWuKK04vPf>!e7!-(;t2 zKrJxk$GP5LDw7Dp9T~)rClgwG@3csIz-joO{)a-aJK>$=t>0v(|C%W7Rmj1(k`!4$ z&!#h~I;DJQ@=sNBpl?)JSGy9BWZqfx(=Iou*i8R#UxL3!|MRx)|AwAS@q6t5#P2^4 zM~Z)SZ23#-_+dxz2E(xpZ#;DqWJw^XmkH3R{tEUe=kN9{|%?`pJ!F@ Mzi|vx{GR>40B??w;{X5v literal 0 HcmV?d00001 diff --git a/panasonic_ac/assets/webif_tab2.jpg b/panasonic_ac/assets/webif_tab2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..89790fb9283fec43e7658534c205c6362c01bb0f GIT binary patch literal 92131 zcmeFZ2Urx_vMAg`&N-(c=O9Q<11MRt1W5yubCMh-N){AQP>>`_8p%Nf$p|7DBr_nQ zC@_G)AOrJ8&(Xclx%Zy?-hKCd-~In@FI{v^t?F7;tJmtTs#V?S_vkf%LPtYe0|0>l zKnwE^K*ND+>LG5<0HCJ_2m$~=0APXm0c;Ee0H z|AMjoe>9?Z09ivF5;TfEfrw4EasR00%D&+=!_N z2VehN0DxVF!E*)Q^8Eo1VPG16jDQ$;=?CocHyH5)zWEdWISw@b*Nv|L01g9Ys7dVY z{apZngd0P{9^~MH;X`s217GrXbo0i*nBRc1mzR$t25!W_{1_Tv4E%$??9D&%arqN$ zZ-4VQPkVcp-|*jb!JLWd_^Mlgucv+J&maGtf4n>cG2`{K{Fp5yuKrram?K74(0dOb z-QTeHO?`u3csGo8zhKuuEz@7HL%V7TCvVk1 zcm`Pfmd(-Ws`@W_CvVf=`ug5d``wqX=g+nAbF7>^HGks+f=qs&dkZrmfAR6XW%zpx z-R*UL%=@3`0@nadfE_ReJb_z)KVT1N0B(Rc;B?C=1ZxIs4eJZm5jKELj7^8lfh~e9i+u�NWDV8QUK_3OfZm7rO+z7P}pL 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%0DG#mbUj)hw6DW_bRI@Gmkm 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 Wzvq9G$NTSZ?Z3zQ-|zX~=l=r#hq~ne literal 0 HcmV?d00001 diff --git a/panasonic_ac/assets/webif_tab3.jpg b/panasonic_ac/assets/webif_tab3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90cbbd700cf31eb8a2d78603c9be63a6fe9db2d4 GIT binary patch literal 92225 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 hLk%e)zA;;h}%{uVXm-zW{uq+<^c9 literal 0 HcmV?d00001 diff --git a/panasonic_ac/plugin.yaml b/panasonic_ac/plugin.yaml index 976d46d2c..fc4df1257 100755 --- a/panasonic_ac/plugin.yaml +++ b/panasonic_ac/plugin.yaml @@ -12,7 +12,7 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1976353-support-thread-für-das-panasonic_ac-plugin - version: 0.3.0 # Plugin version (must match the version specified in __init__.py) + version: 0.3.1 # Plugin version (must match the version specified in __init__.py) sh_minversion: '1.10.0' # 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 diff --git a/panasonic_ac/user_doc.rst b/panasonic_ac/user_doc.rst index a2b4e2769..8acfc6785 100755 --- a/panasonic_ac/user_doc.rst +++ b/panasonic_ac/user_doc.rst @@ -1,11 +1,11 @@ -.. index:: Plugins; pcomfcloud -.. index:: pcomfcloud +.. index:: Plugins; panasonic_ac +.. index:: panasonic_ac -========== -pcomfcloud -========== +============ +panasonic_ac +============ -.. image:: webif/static/img/plugin_logo.png +.. image:: webif/static/img/plugin_logo.jpg :alt: plugin logo :width: 300px :height: 300px @@ -13,32 +13,78 @@ pcomfcloud :align: left - +Mit diesem Plugin können Klimaanlagen der Firma Panasonic vom Typ Etherea gesteuert werden. Die Ansteuerung erfolgt +durch das WLAN Interface der Klimaanlagen und einen von Panasonic zur Verfügung gestellen Cloud Service +(Panasonic Comfort Cloud). | +Anforderungen +============= + +Das Plugin benötigt das Python Package **pcomfortcloud**. Allerdings ist die aktuell releaste Version 0.0.27 dieses +Packages nicht kompatibel mit dem aktuelllen (neuen) Anmeldeverfahren der Panasonic Comfort Cloud. Es wird eine +neuere Version des Packages benötigt, die mit dem Plugin im Unterververzeichnis **packages** des Plugins installiert +wird. + +Notwendige Software +------------------- + +Es wird eine aktuelle Develop Version des Packages **pcomfortcloud** benötigt. Diese Version muß Stand 18. August 2024 +oder neuer sein. + +| Plugin Instanz hinzufügen ========================= Da das Plugin ohne vorherige Konfiguration weiterer Parameter lauffähig ist, wird die Instanz beim Hinzufügen in -der Admin GUI auch gleich aktiviert und beim Neustart von SmartHomeNG geladen. Die Konfiguration erfolgt anschließend -im Web Interface. - -Das Plugin unterstützt je Instanz nur eine Bridge. Dafür ist es Multi-Instance fähig, so dass bei Einsatz mehrerer -Bridges einfach mehrere Instanzen des Plugins konfiguriert werden können. +der Admin GUI auch gleich aktiviert und beim Neustart von SmartHomeNG geladen. +| Konfiguration ============= -Die grundlegende Konfiguration des Plugins selbst, erfolgt durch das Web Interface des Plugins. Mit dem Web Interface -kann die Verbindung zu einer Bridge hergestellt werden kann. Optionale weitere Einstellungen -(z.B. default_transitionTime) können über die Admin GUI vorgenommen werden. Diese Parameter und die Informationen -zur Item-spezifischen Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/hue3` beschrieben. +Optionale Einstellungen können über die Admin GUI vorgenommen werden. Diese Parameter und die Informationen +zur Item-spezifischen Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/panasonic_ac` beschrieben. | +Verwendung von structs +---------------------- + +Mit der Hilfe von Struktur Templates wird die Einrichtung von Items stark vereinfacht. Hierzu wird ein struct Template +vom Plugin mitgeliefert: + +- **panasonic_ac.air_condition** - Standard Definition für Panasonic Klimaanlagen + +Ein Item für eine Panasonic Klimaanlage kann einfach folgendermaßen konfiguriert werden, indem nur der Index der zu +steuernden Anlage als ``pcc_index`` angegeben wird: + +.. code-block:: yaml + + klimaanlage: + pcc_index: 1 + struct: panasonic_ac.air_condition + +Der Index der jeweiligen Klimaanlage kann dem 3. Tab des Webinterfaces entnommen werden. Die Numerierung beginnt bei 1. +Falls also nur eine Klimaanlage mit der Panasonic Comfort Cloud verbunden ist, ist ``pcc_index: 1`` + +Die struct richtet folgende Unteritems ein: ``temp_inside``, ``temp_outside``, ``temp``, ``power``, +``mode``, ``fanspeed``, ``swing_hor``, ``swing_vert``, ``eco`` und ``nanoe``. + +Das Item ``Klimaanlage`` enthält als Wert den Namen der Klimaanlage. + +Die Werte für ``Klimaanlage``, ``temp_inside`` und ``temp_outside`` können nur von der Comfort Cloud gelesen +werden. Eine Veränderung des Item-Wertes hal also keine Auswirkung auf die Klimaanlage. + + +Item Attribute +-------------- + +Die vollständige Übersicht über die unterstützen Attribute und deren zulässige Werte kann auf der +Seite :doc:`/plugins_doc/config/panasonic_ac` nachgelesen werden. | @@ -46,7 +92,7 @@ zur Item-spezifischen Konfiguration des Plugins sind unter :doc:`/plugins_doc/co Web Interface ============= -Das pcomfcloud Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzen +Das panasonic_ac Plugin verfügt über ein Webinterface, mit dessen Hilfe die Items die das Plugin nutzen übersichtlich dargestellt werden. Außerdem können Informationen zu den Devices angezeigt werden, die durch die Panasonic Comfort Cloud verwaltet werden. @@ -57,8 +103,7 @@ Aufruf des Webinterfaces Das Plugin kann aus der Admin GUI (von der Seite Plugins/Plugin Liste aus) aufgerufen werden. Dazu auf der Seite in der entsprechenden Zeile das Icon in der Spalte **Web Interface** anklicken. -Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/plugin/pcomfcloud`` bzw. -``http://smarthome.local:8383/plugin/pcomfcloud_`` aufgerufen werden. +Außerdem kann das Webinterface direkt über ``http://smarthome.local:8383/plugin/panasonic_ac`` aufgerufen werden. | @@ -79,7 +124,7 @@ Im ersten Tab werden die Items angezeigt, die das Plugin nutzen: | | -Im zweiten Tab werden Informationen zu den Leuchten angezeigt, die in der Hue Bridge bekannt sind: +Im zweiten Tab werden Informationen zur aktuellen Parametrierung des einzelnen Klimaanlagen angezeigt: .. image:: assets/webif_tab2.jpg :class: screenshot @@ -87,7 +132,7 @@ Im zweiten Tab werden Informationen zu den Leuchten angezeigt, die in der Hue Br | | -Im dritten Tab werden die Szenen angezeigt, die in der Hue Bridge definiert sind: +Im dritten Tab werden die Grunddaten der einzelnen Klimaanlagen angezeigt: .. image:: assets/webif_tab3.jpg :class: screenshot @@ -96,37 +141,3 @@ Im dritten Tab werden die Szenen angezeigt, die in der Hue Bridge definiert sind | | -Im vierten Tab werden die Gruppen angezeigt, die in der Hue Bridge definiert sind: - -.. image:: assets/webif_tab4.jpg - :class: screenshot - - -| -| - -Im fünften Tab werden die Sensoren angezeigt, die in der Hue Bridge bekannt sind: - -.. image:: assets/webif_tab5.jpg - :class: screenshot - -| -| - -Im sechsten Tab werden die Devices angezeigt, die in der Hue Bridge bekannt sind: - -.. image:: assets/webif_tab6.jpg - :class: screenshot - -| -| - -Auf dem siebten Reiter werden Informationen zur Hue Bridge angezeigt. Wenn weitere Anwendungen die Bridge nutzen, -wird zusätzlich eine Liste der in der Bridge konfigurierten Benutzer/Apps angezeigt. - -.. image:: assets/webif_tab7.jpg - :class: screenshot - -| -| - diff --git a/panasonic_ac/webif/static/img/plugin_logo.jpg b/panasonic_ac/webif/static/img/plugin_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e4ef871056ef5b6f52c5cb829870f3c894585c13 GIT binary patch literal 106075 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 YFX8Zi*YE%T%ugoj{qNz^h<*?MKfK?Vf&c&j literal 0 HcmV?d00001 diff --git a/panasonic_ac/webif/static/img/plugin_logo.jpg.off b/panasonic_ac/webif/static/img/plugin_logo.jpg.off new file mode 100644 index 0000000000000000000000000000000000000000..00ac1fe5f04df00e84982025db4fe23382a86bb8 GIT binary patch literal 29636 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%0D7`Qniv#0I{8^R_oIj3zsTk2ne7*) zvtP~c_a6bT^XI;vw|B?rm3Y&dO_gV;Fx)I21Ei>HV7jDjjT4j)Y@eqKTQ4UMERHg?dnR8K&(!s7bfx&;xxkZ9-L~S zYBJaxdZ`RE{3TsT;A)3g_RH#{nNrC?0#%ANO#(DsE1cTknxPn1o79BqEE@l%->Tn|$>S3Gn$K zGioaG>rCcUj0kv2IYhMIHk8#TD00eT@%uur<7?7-Vsg_OwY4;(8w!#mcp!o}?UL^# zits@wcgOI)HYcrCr*VoYS6L+8jdRPew<*Y0rK$7}s=V0S6lF^|StgXb<}r=z`yt=& zbH3kxm15=7YN&5hIM1~Bc|@w;wo3~#@*BD&CfqX9O8E9f?Ia;Dyng@dXfHpj1U`;}WbMdJihr7-m`qoau1!X3xn(0a^uSfA2Vazk{v?>5m$_;27#i7(c z81-UcaOb z6#}0R^I!8pMEg4w|7$*U(&j{b_{6S31@(P@%m(rIY<^tmM^w$@9vJuYx3U;s+_-Sd z7Y$L$+H*>m$PBEZ?iq1f6v!0~Qz8>vHdrjk$OU@X2H`R?VdY;`{R9n7pUZW2n>BW& zF2-(dH`bHeg^!IaF`RuxDIa=~BhcIwscoVrB zWcc4_OdNK^hWTQx$p6Xu-KAMX^p*<$RrOEfpQY!Oiv6?SnBlk`?W3n;R+zxq51xk0 zHp_AxIFEpGyG+fRQ9(ZNr&yB!AIYR|rU#frFq;`Fi?H$wd)>4?toFk7=t(peL6xv+ z{v7`lU3`bGQo`!4v0_8A1YvqP?LEk#u=y8N#-c}r~}sV1Zv zn5WtLZw{SXLsVO|_fiu4!a-pz?Jp%cP)B>`56x0i(;@vIH}hOuCR6K6*dty++Q<04 znoQ42gn7e=qx0=XfxKZn*48P4!HrM~7G{mK(^X8F6bx`mWRz$9X=k>NlhYHsky1!w z=iFQNZh;+`;kyz&!klsS(vZpLxx8jZ>Gy(Ki`OJeuEPyEiCe9zJ4;zVOhMFHZmS*3 z#%zPDM@!z0!^9W9Y~fBhdbtYUv!$DWw3|uV=50Q!;TI-coVaQ+?>_n$efAaoY~Mx&T0pwV=Gi_T+m4~016<0_@``pn0m9I6u{_i&{Txqjw_Gug4;1BEwO zQG`pNXnQV0av3&GvCU)BXis~(3Yk7>wZidDBM-lmiw^4zsAelyahXpvziy+B0B*U; z#@q#1sZmSr%O≠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 TZcb7vlU>eE@b5P+{hI$Dt!%>F literal 0 HcmV?d00001 diff --git a/panasonic_ac/webif/static/img/plugin_logo.png b/panasonic_ac/webif/static/img/plugin_logo.png deleted file mode 100644 index c3ad05a451d9cc80812a081b8b9f829093484829..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27830 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
  • QliB@i$EYTjh}@GUPcXf*C~LSIpeIk6 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 - Prompt 1 - {% if 1 == 2 %}{{ _('Ja') }}{% else %}{{ _('Nein') }}{% endif %} +   + {{ _(' ') }} - Prompt 4 - {{ _('Wert 4') }} + + {{ _('') }} - Prompt 2 - {{ _('Wert 2') }} +   + {{ _('') }} - Prompt 5 - - + + - Prompt 3 - - +   + - Prompt 6 - - + + From 22cb4a7f9b5692bc6bae5cd9efae0c3486bd87c1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 1 Sep 2024 19:52:31 +0200 Subject: [PATCH 058/121] stateengine plugin: improve condition check of actions (much faster) --- stateengine/StateEngineAction.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index e8f2392da..260aa6793 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -439,15 +439,26 @@ def _update_repeat_webif(value: bool): return conditions_met = 0 condition_necessary = 0 - current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') - conditions_met += cur_conditions_met - condition_necessary += min(1, cur_condition_necessary) - previous_condition_met, prev_conditions_met, prev_condition_necessary = _check_condition('previousconditionset') - conditions_met += prev_conditions_met - condition_necessary += min(1, prev_condition_necessary) - previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') - conditions_met += prevst_conditions_met - condition_necessary += min(1, prevst_condition_necessary) + current_condition_met = None + previous_condition_met = None + previousstate_condition_met = None + next_condition_met = None + if not self.conditionset.is_empty(): + current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') + conditions_met += cur_conditions_met + condition_necessary += min(1, cur_condition_necessary) + if not self.previousconditionset.is_empty(): + previous_condition_met, prev_conditions_met, prev_condition_necessary = _check_condition('previousconditionset') + conditions_met += prev_conditions_met + condition_necessary += min(1, prev_condition_necessary) + if not self.previousstate_conditionset.is_empty(): + previousstate_condition_met, prevst_conditions_met, prevst_condition_necessary = _check_condition('previousstate_conditionset') + conditions_met += prevst_conditions_met + condition_necessary += min(1, prevst_condition_necessary) + if not self.nextconditionset.is_empty(): + next_condition_met, next_conditions_met, next_conditionset_necessary = _check_condition('nextconditionset') + conditions_met += next_conditions_met + condition_necessary += min(1, next_conditionset_necessary) self._log_develop("Action '{0}': conditions met: {1}, necessary {2}.", self._name, conditions_met, condition_necessary) if conditions_met < condition_necessary: self._log_info("Action '{0}': Skipping because not all conditions are met.", self._name) From 2431e443eebb832bb3ec5ac9eb79c381586418f0 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 1 Sep 2024 19:54:33 +0200 Subject: [PATCH 059/121] stateengine plugin: fix previousstate_conditions set valuation --- stateengine/StateEngineActions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 89bef33bd..1203f89b6 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -308,7 +308,7 @@ def __ensure_action_exists(self, func, name): del self.__unassigned_previousconditionsets[name] if name in self.__unassigned_previousstate_conditionsets: - _issue = action.update_previousconditionset(self.__unassigned_previousstate_conditionsets[name]) + _issue = action.update_previousstate_conditionset(self.__unassigned_previousstate_conditionsets[name]) if _issue: _issue_list.append(_issue) del self.__unassigned_previousstate_conditionsets[name] From b36c73ed2b2c6bb83e5f4efac7de7b5df50f4caa Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 1 Sep 2024 19:57:31 +0200 Subject: [PATCH 060/121] stateengine plugin: introduce next_conditionset to check conditionset of upcoming state(condition) --- stateengine/StateEngineAction.py | 111 ++++++++++++++---------- stateengine/StateEngineActions.py | 21 ++++- stateengine/StateEngineConditionSets.py | 6 +- stateengine/StateEngineItem.py | 80 ++++++++++++++--- stateengine/StateEngineState.py | 6 +- stateengine/plugin.yaml | 23 +++++ 6 files changed, 183 insertions(+), 64 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 260aa6793..69ee3dc2c 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -71,6 +71,7 @@ def __init__(self, abitem, name: str): self.__delay = StateEngineValue.SeValue(self._abitem, "delay") self.__repeat = None self.__instanteval = None + self.nextconditionset = StateEngineValue.SeValue(self._abitem, "nextconditionset", True, "str") self.conditionset = StateEngineValue.SeValue(self._abitem, "conditionset", True, "str") self.previousconditionset = StateEngineValue.SeValue(self._abitem, "previousconditionset", True, "str") self.previousstate_conditionset = StateEngineValue.SeValue(self._abitem, "previousstate_conditionset", True, "str") @@ -121,6 +122,12 @@ def update_order(self, value): 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue + def update_nextconditionset(self, value): + _, _, _issue, _ = self.nextconditionset.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'nextconditionset', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue + def update_conditionset(self, value): _, _, _issue, _ = self.conditionset.set(value) _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', @@ -197,6 +204,8 @@ def write_to_logger(self): self.__repeat.write_to_logger() if self.__instanteval is not None: self.__instanteval.write_to_logger() + if self.nextconditionset is not None: + self.nextconditionset.write_to_logger() if self.conditionset is not None: self.conditionset.write_to_logger() if self.previousconditionset is not None: @@ -366,24 +375,28 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ # state: state item that triggered the action def execute(self, is_repeat: bool, allow_item_repeat: bool, state): # check if any conditiontype is met or not - # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset' + # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' def _check_condition(condition: str): _conditions_met_count = 0 _conditions_necessary_count = 0 _condition_to_meet = None - _updated__current_condition = None + _updated_current_condition = None if condition == 'conditionset': _condition_to_meet = None if self.conditionset.is_empty() else self.conditionset.get() _current_condition = self._abitem.get_lastconditionset_id() - _updated__current_condition = self._abitem.get_variable("current.state_id") if _current_condition == '' else _current_condition + _updated_current_condition = self._abitem.get_variable("current.state_id") if _current_condition == '' else _current_condition elif condition == 'previousconditionset': _condition_to_meet = None if self.previousconditionset.is_empty() else self.previousconditionset.get() _current_condition = self._abitem.get_previousconditionset_id() - _updated__current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + _updated_current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition elif condition == 'previousstate_conditionset': _condition_to_meet = None if self.previousstate_conditionset.is_empty() else self.previousstate_conditionset.get() _current_condition = self._abitem.get_previousstate_conditionset_id() - _updated__current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + _updated_current_condition = self._abitem.get_previousstate_id() if _current_condition == '' else _current_condition + elif condition == 'nextconditionset': + _condition_to_meet = None if self.nextconditionset.is_empty() else self.nextconditionset.get() + _current_condition = self._abitem.get_nextconditionset_id() + _updated_current_condition = self._abitem.get_variable("next.conditionset_id") if _current_condition == '' else _current_condition _condition_to_meet = _condition_to_meet if isinstance(_condition_to_meet, list) else [_condition_to_meet] _condition_met = [] for cond in _condition_to_meet: @@ -392,13 +405,13 @@ def _check_condition(condition: str): _orig_cond = cond try: cond = re.compile(cond) - _matching = cond.fullmatch(_updated__current_condition) + _matching = cond.fullmatch(_updated_current_condition) if _matching: - self._log_debug("Given {} {} matches current one: {}", condition, _orig_cond, _updated__current_condition) - _condition_met.append(_updated__current_condition) + self._log_debug("Given {} {} matches current one: {}", condition, _orig_cond, _updated_current_condition) + _condition_met.append(_updated_current_condition) _conditions_met_count += 1 else: - self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated__current_condition) + self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated_current_condition) except Exception as ex: if cond is not None: self._log_warning("Given {} {} is not a valid regex: {}", condition, _orig_cond, ex) @@ -513,7 +526,7 @@ def _update_repeat_webif(value: bool): self._log_decrease_indent() _delay_info = -1 else: - self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met) + self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met, next_condition_met) _update_delay_webif('actions_stay', str(_delay_info)) _update_delay_webif('actions_enter', str(_delay_info)) @@ -543,19 +556,21 @@ def _can_execute(self, state): def get(self): return True - def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None): + def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None, next_condition: list[str] = None): if current_condition is None: current_condition = [] if previous_condition is None: previous_condition = [] if previousstate_condition is None: previousstate_condition = [] + if next_condition is None: + next_condition = [] self._log_decrease_indent(50) self._log_increase_indent() if delay == 0: self._log_info("Action '{}': Running.", namevar) - self.real_execute(state, actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition) + self.real_execute(state, actionname, namevar, repeat_text, None, False, current_condition, previous_condition, previousstate_condition, next_condition) else: instanteval = None if self.__instanteval is None else self.__instanteval.get() self._log_info("Action '{0}': Add {1} second timer '{2}' " @@ -565,7 +580,7 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text if instanteval is True: self._log_increase_indent() self._log_debug("Evaluating value for delayed action '{}'.", namevar) - value = self.real_execute(state, actionname, namevar, repeat_text, None, True, current_condition, previous_condition, previousstate_condition) + value = self.real_execute(state, actionname, namevar, repeat_text, None, True, current_condition, previous_condition, previousstate_condition, next_condition) self._log_debug("Value for delayed action is going to be '{}'.", value) self._log_decrease_indent() else: @@ -578,21 +593,21 @@ def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text 'current_condition': current_condition, 'previous_condition': previous_condition, 'previousstate_condition': previousstate_condition, - 'state': state}, next=next_run) + 'next_condition': next_condition, 'state': state}, next=next_run) - def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, state=None, caller=None): + def _delayed_execute(self, actionname: str, namevar: str = "", repeat_text: str = "", value=None, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None, state=None, caller=None): if state: self._log_debug("Putting delayed action '{}' from state '{}' into queue. Caller: {}", namevar, state, caller) - self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, state]) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, next_condition, state]) else: self._log_debug("Putting delayed action '{}' into queue. Caller: {}", namevar, caller) - self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition]) + self.__queue.put(["delayedaction", self, actionname, namevar, repeat_text, value, current_condition, previous_condition, previousstate_condition, next_condition]) if not self._abitem.update_lock.locked(): self._log_debug("Running queue") self._abitem.run_queue() # Really execute the action (needs to be implemented in derived classes) - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): raise NotImplementedError("Class {} doesn't implement real_execute()".format(self.__class__.__name__)) def _getitem_fromeval(self): @@ -680,7 +695,7 @@ def _can_execute(self, state): return True # Really execute the action (needs to be implemented in derived classes) - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: @@ -713,9 +728,9 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'False') return - self._execute_set_add_remove(state, actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition) + self._execute_set_add_remove(state, actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition, next_condition) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition, next_condition): self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'{3}", actionname, item.property.path, value, repeat_text) source = self.set_source(current_condition, previous_condition, previousstate_condition) @@ -751,12 +766,12 @@ def get(self): mindelta = self.__mindelta.get() if mindelta is None: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} else: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, 'delta': str(self.__delta), 'mindelta': str(mindelta)} @@ -799,7 +814,7 @@ def write_to_logger(self): self._log_debug("set by attribute: {0}", self.__byattr) # Really execute the action - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return value @@ -811,7 +826,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s item(item.conf[self.__byattr], caller=self._caller, source=source) def get(self): - result = {'function': str(self._function), 'byattr': str(self.__byattr), + result = {'function': str(self._function), 'byattr': str(self.__byattr), 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -859,7 +874,7 @@ def write_to_logger(self): self._log_debug("value: {0}", self.__value) # Really execute the action - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if value is None: try: @@ -884,7 +899,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'logic': str(self.__logic), - 'value': value, + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -932,7 +947,7 @@ def write_to_logger(self): self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) # Really execute the action - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if isinstance(self.__eval, str): @@ -952,6 +967,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) + if next_condition: + self._log_debug("Running eval {0} based on next_condition {1}", self.__eval, next_condition) eval(self.__eval) self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() @@ -971,6 +988,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) if previousstate_condition: self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) + if next_condition: + self._log_debug("Running eval {0} based on next conditionset {1}", self.__eval, next_condition) self.__eval() self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() @@ -981,7 +1000,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) def get(self): - result = {'function': str(self._function), 'eval': str(self.__eval), + result = {'function': str(self._function), 'eval': str(self.__eval), 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1072,7 +1091,7 @@ def _getitem_fromeval(self): # Really execute the action (needs to be implemented in derived classes) # noinspection PyProtectedMember - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: @@ -1158,12 +1177,12 @@ def get(self): mindelta = self.__mindelta.get() if mindelta is None: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} else: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, 'delta': str(self.__delta), 'mindelta': str(mindelta)} @@ -1219,7 +1238,7 @@ def write_to_logger(self): self._log_debug("Retrigger item: {0}", self.__value.property.path) # Really execute the action - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None): + def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): self._abitem.set_variable('current.action_name', namevar) if returnvalue: return None @@ -1227,11 +1246,11 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s _log_value = self.__value.property.path except Exception: _log_value = self.__value - self._log_info("{0}: Executing special action '{1}' using item '{2}' based on '{3}/{4}/{5}'.{6}", - actionname, self.__special, _log_value, current_condition, previous_condition, previousstate_condition, repeat_text) + self._log_info("{0}: Executing special action '{1}' using item '{2}' based on current condition {3} / previous condition {4} / previousstate condition {5} / next_condition {6}.{7}", + actionname, self.__special, _log_value, current_condition, previous_condition, previousstate_condition, next_condition, repeat_text) self._log_increase_indent() if self.__special == "suspend": - self.suspend_execute(state, current_condition, previous_condition, previousstate_condition) + self.suspend_execute(state, current_condition, previous_condition, previousstate_condition, next_condition) if self._suspend_issue in ["", [], None, [None]]: self.update_webif_actionstatus(state, self._name, 'True') else: @@ -1312,7 +1331,7 @@ def retrigger_get_value(self, value): raise ValueError("Action {0}: {1}".format(self._name, text)) return se_item - def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None): + def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): suspend_item, _issue = self._abitem.return_item(self.__value[0]) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': state.id, 'action': 'suspend'}]}} source = "SuspendAction, {}".format(self.set_source(current_condition, previous_condition, previousstate_condition)) @@ -1346,7 +1365,7 @@ def get(self): except Exception: pass result = {'function': str(self._function), 'special': str(self.__special), - 'value': str(value_result), 'conditionset': self.conditionset.get(), + 'value': str(value_result), 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1368,7 +1387,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): value = value if isinstance(value, list) else [value] self._log_debug("{0}: Add '{1}' to '{2}'.{3}", actionname, value, item.property.path, repeat_text) value = item.property.value + value @@ -1394,7 +1413,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1416,7 +1435,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1448,7 +1467,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1470,7 +1489,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1504,7 +1523,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result @@ -1526,7 +1545,7 @@ def write_to_logger(self): SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1558,7 +1577,7 @@ def get(self): except Exception: value = None result = {'function': str(self._function), 'item': item, - 'value': value, 'conditionset': self.conditionset.get(), + 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 1203f89b6..0cb1b2c9b 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -37,6 +37,7 @@ def __init__(self, abitem): self.__unassigned_repeats = {} self.__unassigned_instantevals = {} self.__unassigned_orders = {} + self.__unassigned_nextconditionsets = {} self.__unassigned_conditionsets = {} self.__unassigned_previousconditionsets = {} self.__unassigned_previousstate_conditionsets = {} @@ -108,6 +109,14 @@ def update(self, attribute, value): else: _issue = self.__actions[name].update_repeat(value) return _count, _issue + elif func == "se_nextconditionset": + # set nextconditionset + if name not in self.__actions: + # If we do not have the action yet (conditionset-attribute before action-attribute), ... + self.__unassigned_nextconditionsets[name] = value + else: + _issue = self.__actions[name].update_nextconditionset(value) + return _count, _issue elif func == "se_conditionset": # set conditionset if name not in self.__actions: @@ -295,6 +304,12 @@ def __ensure_action_exists(self, func, name): _issue_list.append(_issue) del self.__unassigned_orders[name] + if name in self.__unassigned_nextconditionsets: + _issue = action.update_nextconditionset(self.__unassigned_nextconditionsets[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_nextconditionsets[name] + if name in self.__unassigned_conditionsets: _issue = action.update_conditionset(self.__unassigned_conditionsets[name]) if _issue: @@ -324,7 +339,7 @@ def remove_action(ex): _issue_list.append(_issue) self._log_warning("Ignoring action {0} because: {1}", name, ex) - parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'conditionset': None, + parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'nextconditionset': None, 'conditionset': None, 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} _issue = None _issue_list = [] @@ -477,6 +492,10 @@ def remove_action(ex): _issue = self.__actions[name].update_order(parameter['order']) if _issue: _issue_list.append(_issue) + if parameter['nextconditionset'] is not None: + _issue = self.__actions[name].update_nextconditionset(parameter['nextconditionset']) + if _issue: + _issue_list.append(_issue) if parameter['conditionset'] is not None: _issue = self.__actions[name].update_conditionset(parameter['conditionset']) if _issue: diff --git a/stateengine/StateEngineConditionSets.py b/stateengine/StateEngineConditionSets.py index c8dbf3612..0cf4212ff 100755 --- a/stateengine/StateEngineConditionSets.py +++ b/stateengine/StateEngineConditionSets.py @@ -91,9 +91,9 @@ def write_to_logger(self): def one_conditionset_matching(self, state): if self.count() == 0: self._log_debug("No condition sets defined -> matching") - return True + return True, '' for name in self.__condition_sets: if self.__condition_sets[name].all_conditions_matching(state): - return True + return True, name - return False + return False, '' diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 42202d905..f64adf7b2 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -111,6 +111,10 @@ def default_instant_leaveaction(self): def default_instant_leaveaction(self, value): self.__default_instant_leaveaction.set(value) + @property + def nextconditionset(self): + return self.__nextconditionset_id + @property def laststate(self): _returnvalue = None if self.__laststate_item_id is None else self.__laststate_item_id.property.value @@ -266,6 +270,8 @@ def __init__(self, smarthome, item, se_plugin): self.__config_issues.update(_issue) # Init lastconditionset items/values + self.__nextconditionset_id = "" + self.__nextconditionset_name = "" self.__lastconditionset_item_id, _issue = self.return_item_by_attribute("se_lastconditionset_item_id") self.__lastconditionset_internal_id = "" if self.__lastconditionset_item_id is None else \ self.__lastconditionset_item_id.property.value @@ -338,6 +344,8 @@ def __init__(self, smarthome, item, se_plugin): "release.has_released": "", "release.was_released_by": "", "release.will_release": "", + "next.conditionset_id": "", + "next.conditionset_name": "", "current.state_id": "", "current.state_name": "", "current.conditionset_id": "", @@ -532,11 +540,11 @@ def run_queue(self): elif job[0] == "delayedaction": self.__logger.debug("Job {}", job) (_, action, actionname, namevar, repeat_text, value, current_condition, previous_condition, - previousstate_condition, state) = job + previousstate_condition, next_condition, state) = job self.__logger.info( - "Running delayed action: {0} based on current condition {1} or previous condition {2}", - actionname, current_condition, previous_condition) - action.real_execute(state, actionname, namevar, repeat_text, value, False, current_condition) + "Running delayed action: {0} based on current_condition {1} / previous_condition {2} / previousstate_condition {3} or next condition {4}", + actionname, current_condition, previous_condition, previousstate_condition, next_condition) + action.real_execute(state, actionname, namevar, repeat_text, value, False, current_condition, previous_condition, previousstate_condition, next_condition) else: (_, item, caller, source, dest) = job item_id = item.property.path if item is not None else "(no item)" @@ -627,7 +635,13 @@ def run_queue(self): _key_name = ['{}'.format(state.id), 'name'] self.update_webif(_key_name, state.name) - result = self.__update_check_can_enter(state, _instant_leaveaction) + result, self.__nextconditionset_name = self.__update_check_can_enter(state, _instant_leaveaction) + if self.__nextconditionset_name: + self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}" + else: + self.__nextconditionset_id = "" + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) _previousstate_conditionset_id = _last_conditionset_id _previousstate_conditionset_name = _last_conditionset_name _last_conditionset_id = self.__lastconditionset_internal_id @@ -649,6 +663,10 @@ def run_queue(self): # no new state -> stay if new_state is None: if last_state is None: + self.__nextconditionset_id = '' + self.__nextconditionset_name = '' + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) self.__logger.info("No matching state found, no previous state available. Doing nothing.") else: if last_state.conditions.count() == 0: @@ -657,6 +675,11 @@ def run_queue(self): _last_conditionset_name = '' else: self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: text = "No matching state found, staying at {0} ('{1}')" self.__logger.info(text, last_state.id, last_state.name) @@ -681,9 +704,18 @@ def run_queue(self): "State is a copy and therefore just releasing {}. Skipping state actions, running leave actions " "of last state, then retriggering.", new_state.is_copy_for.id) if last_state is not None and self.__ab_alive: - self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", - last_state.id, last_state.name, _original_conditionset_id) - self.__update_check_can_enter(last_state, _instant_leaveaction, False) + _, self.__nextconditionset_name = self.__update_check_can_enter(last_state, _instant_leaveaction, False) + if self.__nextconditionset_name: + self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}" + else: + self.__nextconditionset_id = "" + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) + self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2} ({3}), will be {4} ({5}).", + last_state.id, last_state.name, _original_conditionset_id, + _original_conditionset_name, self.__nextconditionset_id, + self.__nextconditionset_name) last_state.run_leave(self.__repeat_actions.get()) _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] @@ -710,6 +742,11 @@ def run_queue(self): # endblock # get data for new state if last_state is not None and new_state.id == last_state.id: + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: self.__logger.info("Staying at {0} ('{1}')", new_state.id, new_state.name) else: @@ -731,18 +768,25 @@ def run_queue(self): "Maybe some actions were performed directly after leave - see log above.") elif last_state is not None: self.lastconditionset_set(_original_conditionset_id, _original_conditionset_name) - self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2}.", - last_state.id, last_state.name, _original_conditionset_id) + self.__logger.develop("Current variables: {}", self.__variables) + self.__logger.info("Leaving {0} ('{1}'). Condition set was: {2} ({3}), will be {4} ({5}).", + last_state.id, last_state.name, _original_conditionset_id, + _original_conditionset_name, self.__nextconditionset_id, self.__nextconditionset_name) last_state.run_leave(self.__repeat_actions.get()) _leaveactions_run = True if new_state.conditions.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' + self.__nextconditionset_id = _last_conditionset_id + self.__nextconditionset_name = _last_conditionset_name else: self.lastconditionset_set(_last_conditionset_id, _last_conditionset_name) self.previousstate_conditionset_set(_previousstate_conditionset_id, _previousstate_conditionset_name) + self.set_variable('next.conditionset_id', self.__nextconditionset_id) + self.set_variable('next.conditionset_name', self.__nextconditionset_name) + self.__logger.develop("Current variables: {}", self.__variables) if _last_conditionset_id in ['', None]: self.__logger.info("Entering {0} ('{1}')", new_state.id, new_state.name) else: @@ -918,7 +962,7 @@ def update_can_release_list(): current_log_level = self.__log_level.get() if current_log_level < 3: self.__logger.log_level_as_num = 0 - can_enter = self.__update_check_can_enter(relevant_state, instant_leaveaction) + can_enter, _ = self.__update_check_can_enter(relevant_state, instant_leaveaction) self.__logger.log_level_as_num = current_log_level if relevant_state == last_state: self.__logger.debug("Possible release state {} = last state {}, " @@ -1333,6 +1377,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__variables["previous.state_id"] = self.__previousstate_internal_id self.__variables["previous.state_name"] = self.__previousstate_internal_name self.__variables["item.instant_leaveaction"] = instant_leaveaction + self.__variables["next.conditionset_id"] = self.__nextconditionset_id + self.__variables["next.conditionset_name"] = self.__nextconditionset_name self.__variables["current.state_id"] = state.id self.__variables["current.state_name"] = state.name self.__variables["current.conditionset_id"] = self.__lastconditionset_internal_id @@ -1354,6 +1400,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__variables["release.was_released_by"] = "" self.__variables["release.will_release"] = "" self.__variables["item.instant_leaveaction"] = "" + self.__variables["next.conditionset_id"] = "" + self.__variables["next.conditionset_name"] = "" self.__variables["current.state_id"] = "" self.__variables["current.state_name"] = "" self.__variables["current.conditionset_id"] = "" @@ -1854,6 +1902,8 @@ def cli_detail(self, handler): self.get_previousstate_conditionset_name())) handler.push("\tPrevious conditionset: {0} ('{1}')\n".format(self.get_previousconditionset_id(), self.get_previousconditionset_name())) + handler.push("\tNext conditionset: {0} ('{1}')\n".format(self.get_nextconditionset_id(), + self.get_nextconditionset_name())) handler.push(self.__startup_delay.get_text("\t", "\n")) handler.push("\tCycle: {0}\n".format(cycles)) handler.push("\tCron: {0}\n".format(crons)) @@ -1878,6 +1928,14 @@ def get_condition_age(self): self.__logger.warning('No item for last condition id given. Can not determine age!') return 0 + # return id of new (upcoming) conditionset + def get_nextconditionset_id(self): + return self.__nextconditionset_id + + # return name of new (upcoming) conditionset + def get_nextconditionset_name(self): + return self.__nextconditionset_name + # return id of last state def get_laststate_id(self): return self.__laststate_internal_id diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index c8986e21b..422d5bb80 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -193,13 +193,13 @@ def can_enter(self): self.__is_copy_for.write_to_logger() self.__releasedby.write_to_logger() self.__can_release.write_to_logger() - result = self.__conditions.one_conditionset_matching(self) + result, conditionset = self.__conditions.one_conditionset_matching(self) self._log_decrease_indent() if result: - self._log_info("State {} can be entered", self.id) + self._log_info("State {} can be entered based on conditionset {}", self.id, conditionset) else: self._log_info("State {} can not be entered", self.id) - return result + return result, conditionset # log state data def write_to_log(self): diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 5897f722a..f8f0f6f56 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -853,6 +853,12 @@ item_structs: eval: sh..suspendduration(sh..suspendduration(), "Init", "Start") crontab: init = True + conditionset_leaveactions: + suspend: + type: str + initial_value: '.*' + cache: True + suspendduration: remark: duration of suspend mode in minutes (gets converted automatically) type: num @@ -942,24 +948,29 @@ item_structs: - 'function: set' - 'to: False' - 'order: 2' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_visu: - 'function: set' - 'to: False' - 'order: 3' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_end: - 'function: set' - 'to: ' - 'order: 4' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_start: - 'function: set' - 'to: ' - 'order: 5' - 'delay: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_retrigger: - 'function: special' - 'value: retrigger:..retrigger' - 'delay: -1' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') @@ -1049,6 +1060,12 @@ item_structs: eval: (sh..suspendduration(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration0(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration1(sh..suspendvariant.suspendduration1(), "Init", "Start"), sh..suspendvariant.suspendduration2(sh..suspendvariant.suspendduration2(), "Init", "Start")) crontab: init = True + conditionset_leaveactions: + suspend: + type: str + initial_value: '.*' + cache: True + suspendvariant: remark: number between 0 and 2 to define which suspendduration should be used type: num @@ -1246,28 +1263,34 @@ item_structs: - 'function: set' - 'to: eval:se_eval.get_relative_itemproperty("..settings.suspendvariant", "last_value")' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend: - 'function: set' - 'to: False' - 'order: 2' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_visu: - 'function: set' - 'to: False' - 'order: 3' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_end: - 'function: set' - 'to: ' - 'order: 4' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_suspend_start: - 'function: set' - 'to: ' - 'order: 5' - 'delay: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' se_action_retrigger: - 'function: special' - 'value: retrigger:..retrigger' - 'delay: -1' - 'order: 1' + - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend' enter_manuell: se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path') From 65ca7efd82b903b30f7feca77a4427be142ba1e0 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 1 Sep 2024 20:01:28 +0200 Subject: [PATCH 061/121] stateengine plugin: update docu for next conditionset --- stateengine/user_doc/06_aktionen.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 339427c70..57343185f 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -409,6 +409,17 @@ Die Abfrage erfolgt dabei nach den gleichen Regeln wie bei ``conditionset`` oben soll, wenn die Bedingungsgruppe, mit der der vorherige Zustand eingenommen wurde, mit dem angegebenen Ausdruck übereinstimmt. Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben. +**next_conditionset: ** + +.. code-block:: yaml + + next_conditionset: regex:enter_(.*)_test" + +Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden +soll, wenn die Bedingungsgruppe, mit der der zukünftige Zustand eingenommen wird, mit dem angegebenen Ausdruck übereinstimmt. +Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben. +Diese Angabe ist primär bei leave_actions sinnvoll. + Templates für Aktionen ---------------------- From 42b90c8a15cec54430570c454361d642569039be Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 2 Sep 2024 00:03:54 +0200 Subject: [PATCH 062/121] stateengine plugin: minor logging and docu improvements --- stateengine/StateEngineAction.py | 2 +- stateengine/StateEngineState.py | 7 +++++-- stateengine/user_doc/10_funktionen_variablen.rst | 13 +++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 69ee3dc2c..1f6530e03 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -298,7 +298,7 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - self._log_develop("Check item {} status {} value {} use {} evals_items {}", check_item, check_status, check_value, use, evals_items) + self._log_develop("For action {} check item {} status {} mindelta {} value {} actiontype {}, use {} evals_items {}", self, check_item, check_status, check_mindelta, check_value, action_type, use, evals_items) try: _name = evals_items.get(self.name) if _name is not None: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 422d5bb80..c0c25c592 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -379,13 +379,16 @@ def update_name(self, item_state, recursion_depth=0): # use item name as state name if "se_name" in item_state.conf: self.__text.set_from_attr(item_state, "se_name") + self._log_develop("Updated name of state {} to {} based on se_name.", item_state, self.__name) elif str(item_state) != item_state.property.path or (self.__name == "" and recursion_depth == 0): _name = str(item_state).split('.')[-1] self.__text.set(_name) + self._log_develop("Updated name of state {} to {} based on item name (recursion_depth = {}).", + item_state, self.__name, recursion_depth) elif self.__text.is_empty() and recursion_depth == 0: self.__text.set("value:" + self.__name) + self._log_develop("Set name of state {} to {} as it was empty.", item_state, self.__name) self.__name = self.text - self._log_develop("Updated name of state {} to {}.", item_state, self.__name) return self.__name def __fill_list(self, item_states, recursion_depth, se_use=None, use=None): @@ -679,7 +682,7 @@ def update_action_status(action_status, actiontype): if child_name in action_mapping: action_name, action_method = action_mapping[child_name] for attribute in child_item.conf: - self._log_develop("Filling state with {} action named {}", child_name, attribute) + self._log_develop("Filling state with {} action named {} based on {}", child_name, attribute, state) _action_counts[action_name] += 1 _, _action_status = action_method.update(attribute, child_item.conf[attribute]) if _action_status: diff --git a/stateengine/user_doc/10_funktionen_variablen.rst b/stateengine/user_doc/10_funktionen_variablen.rst index c1ee17a7b..e84e1f0e7 100755 --- a/stateengine/user_doc/10_funktionen_variablen.rst +++ b/stateengine/user_doc/10_funktionen_variablen.rst @@ -282,13 +282,10 @@ die Situation deutlich vereinfachen würde. **current.conditionset_name:** *Der Name der Bedingungsgruppe, die gerade geprüft wird* -Beide current.conditionset Variablen können ebenso wie die oben erwähnten current.state_id/name -nur während der Prüfung der Bedingungen genutzt werden, nicht jedoch für Aktionen. - Das Beispiel zeigt einen Windzustand. Dieser übernimmt keine Funktionen, sondern dient lediglich der Visualisierung (Sicherheitsrelevante Features sollten unbedingt z.B. über den KNX Bus erfolgen!). Außerdem wird davon -ausgegangen, dass es einen untergeordneten Zustand names x gibt. +ausgegangen, dass es einen untergeordneten Zustand namens x gibt. - enter_normal wird angenommen, sobald das Wind-Item aktiv ist, zuvor aber nicht der x-Zustand aktiv war. @@ -364,6 +361,14 @@ Zustand aktiv gewesen ist. Ansonsten gelten alle vorhin beschriebenen Regeln. **previous.state_conditionset_name:** *Der Name der Bedingungsgruppe, die beim vorigen Zustand zuletzt aktiv war* +**next.conditionset_id:** +*Die Id der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist* + +**next.conditionset_name:** +*Der Name der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist* + +Beide next.conditionset Variablen können sinnvoll nur für Aktionen genutzt werden. + **release.can_be_released_by:** *Die Definitionen, die den aktuellen Zustand generell auflösen könnten* From f67c86d2c8d93e74fc1ab3dad89aa8d0a9504ad1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 2 Sep 2024 00:04:28 +0200 Subject: [PATCH 063/121] stateengine plugin: introduce caching of eval results to improve performance --- stateengine/StateEngineItem.py | 10 +++++++++ stateengine/StateEngineValue.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index f64adf7b2..122a7807d 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -171,6 +171,14 @@ def previousstate_conditionset_name(self): _returnvalue = None if self.__previousstate_conditionset_item_name is None else self.__previousstate_conditionset_item_name.property.value return _returnvalue + @property + def cache(self): + return self.__cache + + @cache.setter + def cache(self, value): + self.__cache.update(value) + @property def ab_alive(self): return self.__ab_alive @@ -195,6 +203,7 @@ def __init__(self, smarthome, item, se_plugin): self.__se_plugin = se_plugin self.__active_schedulers = [] self.__release_info = {} + self.__cache = {} self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, "bool") self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") try: @@ -549,6 +558,7 @@ def run_queue(self): (_, item, caller, source, dest) = job item_id = item.property.path if item is not None else "(no item)" self.__logger.update_logfile() + self.__cache = {} self.__logger.header("Update state of item {0}".format(self.__name)) if caller: self.__logger.debug("Update triggered by {0} (item={1} source={2} dest={3})", caller, item_id, diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index bfc2c87b8..a191f16a4 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -744,13 +744,28 @@ def __get_from_regex(self): def __get_eval(self): # noinspection PyUnusedLocal sh = self._sh + # noinspection PyUnusedLocal shtime = self._shtime + patterns = [ + "get_variable('current.", + 'get_variable("current.', + "get_variable('next.", + 'get_variable("next.' + ] if isinstance(self.__eval, str): self.__eval = StateEngineTools.parse_relative(self.__eval, 'sh.', ['()', '.property.']) if "stateengine_eval" in self.__eval or "se_eval" in self.__eval: # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) self._log_debug("Checking eval: {0}", self.__eval) + if self.__eval in self._abitem.cache: + self._log_increase_indent() + result = self._abitem.cache.get(self.__eval) + self._log_debug("Loading eval from cache: {}", result) + self._log_decrease_indent() + if 'eval:{}'.format(self.__eval) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + return result self._log_increase_indent() try: _newvalue, _issue = self.__do_cast(eval(self.__eval)) @@ -762,6 +777,8 @@ def __get_eval(self): values = _newvalue self._log_decrease_indent() self._log_debug("Eval result: {0} ({1}).", values, type(values)) + if not any(pattern in self.__eval for pattern in patterns): + self._abitem.cache = {self.__eval: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() @@ -785,6 +802,14 @@ def __get_eval(self): pass self._log_debug("Checking eval {0} from list {1}.", val, self.__eval) self._log_increase_indent() + if val in self._abitem.cache: + result = self._abitem.cache.get(val) + self._log_debug("Loading eval in list from cache: {} ({})", result, type(result)) + self._log_decrease_indent() + values.append(result) + if 'eval:{}'.format(val) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(val))] = [result] + continue if isinstance(val, str): if "stateengine_eval" in val or "se_eval" in val: # noinspection PyUnusedLocal @@ -830,9 +855,19 @@ def __get_eval(self): value = None if value is not None: values.append(value) + if not any(pattern in val for pattern in patterns): + self._abitem.cache = {val: value} self._log_decrease_indent() else: self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) + if self.__eval in self._abitem.cache: + self._log_increase_indent() + result = self._abitem.cache.get(self.__eval) + self._log_debug("Loading eval (no str, no list) from cache: {}", result) + self._log_decrease_indent() + if 'eval:{}'.format(self.__eval) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + return result try: self._log_increase_indent() _newvalue, _issue = self.__do_cast(self.__eval()) @@ -844,6 +879,7 @@ def __get_eval(self): values = _newvalue self._log_decrease_indent() self._log_debug("Eval result (no str, no list): {0}.", values) + self._abitem.cache = {self.__eval: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() From dbf079e34b707d38263ee2f95fb4f852100a2957 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Sep 2024 14:37:03 +0200 Subject: [PATCH 064/121] stateengine plugin: add nextconditionset to web interface --- stateengine/StateEngineWebif.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 3181788b2..ab3e4df9b 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -59,10 +59,10 @@ def __init__(self, abitem): def __repr__(self): return "WebInterface item: {}, id {}".format(self.__states, self.__name) if REQUIRED_PACKAGE_IMPORTED else "None" - def _actionlabel(self, state, label_type, conditionset, previousconditionset, previousstate_conditionset): + def _actionlabel(self, state, label_type, conditionset, previousconditionset, previousstate_conditionset, next_conditionset): # Check if conditions for action are met or not # action_dict: abitem[state]['on_enter'/'on_stay'/'on_enter_or_stay'/'on_leave'].get(action) - # condition_to_meet: 'conditionset'/'previousconditionset''previousstate_conditionset' + # condition_to_meet: 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' # conditionset: name of conditionset that should get checked def _strip_regex(regex_list): pattern_strings = [] @@ -122,6 +122,9 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s count, condition3, previousstate_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousstate_conditionset', previousstate_conditionset) condition_count += count condition_necessary += min(1, necessary) + count, condition4, next_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'nextconditionset', next_conditionset) + condition_count += count + condition_necessary += min(1, necessary) if condition_count < condition_necessary: condition_met = False @@ -136,6 +139,7 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s condition_info = _strip_regex(condition_to_meet) if condition1 is False \ else _strip_regex(previouscondition_to_meet) if condition2 is False \ else _strip_regex(previousstate_condition_to_meet) if condition3 is False \ + else _strip_regex(next_condition_to_meet) if condition4 is False \ else "" if _issue: if tooltip_count > 0: @@ -388,6 +392,7 @@ def drawgraph(self, filename): previous_conditionset = '' previousconditionset = '' previousstate_conditionset = '' + next_conditionset = '' for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) if isinstance(self.__states[state], (OrderedDict, dict)): @@ -457,20 +462,20 @@ def drawgraph(self, filename): if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ - self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset) + self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ - self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset) + self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) if len(actions_leave) > 0: actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ - self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset) + self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor position = '{},{}!'.format(0.5, new_y) conditionset_positions.append(new_y) - #self._log_debug('conditionset: {} {}, previous {}', conditionset, position, previous_conditionset) + #self._log_debug('conditionset: {} {}, previous {} next {}', conditionset, position, previous_conditionset, next_conditionset) conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset, i) cond3 = conditionset == '' From 8d94e2a6716b8170575cf57cab802c642f0b7885 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Sep 2024 14:41:42 +0200 Subject: [PATCH 065/121] stateenginge plugin: add nextcondition to source evaluation method --- stateengine/StateEngineAction.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 1f6530e03..cb8de3bfa 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -216,9 +216,9 @@ def write_to_logger(self): self.__mode.write_to_logger() self.__order.write_to_logger() - def set_source(self, current_condition, previous_condition, previousstate_condition): + def set_source(self, current_condition, previous_condition, previousstate_condition, next_condition): source = [] - if current_condition in [[], None] and previous_condition in [[], None] and previousstate_condition in [[], None]: + if current_condition in [[], None] and previous_condition in [[], None] and previousstate_condition in [[], None] and next_condition in [[], None]: source = self._parent else: if current_condition != []: @@ -227,6 +227,8 @@ def set_source(self, current_condition, previous_condition, previousstate_condit source.append("previouscondition={}".format(previous_condition)) if previousstate_condition != []: source.append("previousstate_condition={}".format(previousstate_condition)) + if next_condition != []: + source.append("nextcondition={}".format(next_condition)) source = ", ".join(source) return source @@ -733,7 +735,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition, next_condition): self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'{3}", actionname, item.property.path, value, repeat_text) - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable @@ -820,7 +822,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s return value self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) self.update_webif_actionstatus(state, self._name, 'True') - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) for item in self._sh.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) @@ -1123,7 +1125,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_debug(text, actionname, self.__item.property.path, value, additionaltext, delta, mindelta) self.update_webif_actionstatus(state, self._name, 'False') return - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) # Set to different value first ("force") current_value = self.__item() if current_value == value: @@ -1334,7 +1336,7 @@ def retrigger_get_value(self, value): def suspend_execute(self, state=None, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): suspend_item, _issue = self._abitem.return_item(self.__value[0]) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': state.id, 'action': 'suspend'}]}} - source = "SuspendAction, {}".format(self.set_source(current_condition, previous_condition, previousstate_condition)) + source = "SuspendAction, {}".format(self.set_source(current_condition, previous_condition, previousstate_condition, next_condition)) if self._abitem.get_update_trigger_source() == self.__value[1]: # triggered by manual-item: Update suspend item if suspend_item.property.value: @@ -1391,7 +1393,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value = value if isinstance(value, list) else [value] self._log_debug("{0}: Add '{1}' to '{2}'.{3}", actionname, value, item.property.path, repeat_text) value = item.property.value + value - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) @@ -1446,7 +1448,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove first entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) @@ -1502,7 +1504,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove last entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) @@ -1556,7 +1558,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove all '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition) + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) From 09293d20364389d7fe0c2f5b29b8b0bd74954fc9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Sep 2024 14:44:55 +0200 Subject: [PATCH 066/121] stateengine plugin: minor improvements --- stateengine/StateEngineAction.py | 4 ++-- stateengine/StateEngineItem.py | 2 +- stateengine/StateEngineState.py | 2 +- stateengine/StateEngineValue.py | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index cb8de3bfa..26e9737ea 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -1069,14 +1069,14 @@ def _can_execute(self, state): self._log_increase_indent() self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'No item defined') + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No item defined'.format(self._name)) return False if self.__value.is_empty(): self._log_increase_indent() self._log_warning("Action '{0}': No value defined for item {1}. Ignoring.", self._name, self.__item) self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'No value defined for item {}'.format(self.__item)) + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, self.__item)) return False self.update_webif_actionstatus(state, self._name, 'True') return True diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 122a7807d..aa4ce8285 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -773,7 +773,7 @@ def run_queue(self): self.__logger.info("Leave actions already run during state release.") elif last_state is not None and _leaveactions_run is True: self.__logger.info("Left {0} ('{1}')", last_state.id, last_state.name) - if last_state.leaveactions.count() > 0: + if last_state.actions_leave.count() > 0: self.__logger.info( "Maybe some actions were performed directly after leave - see log above.") elif last_state is not None: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index c0c25c592..31c066957 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -57,7 +57,7 @@ def name(self): # Return leave actions @property - def leaveactions(self): + def actions_leave(self): return self.__actions_leave # Return text of state diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index a191f16a4..061f231df 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -470,6 +470,7 @@ def get_type(self): # Write condition to logger def write_to_logger(self): + eval_result = None if self.__template is not None: self._log_info("{0}: Using template(s) {1}", self.__name, self.__template) if self.__value is not None: @@ -508,7 +509,8 @@ def write_to_logger(self): if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) _original_listorder = self.__listorder.copy() - self._log_debug("Currently eval results in {}. ", self.__get_eval()) + eval_result = self.__get_eval() + self._log_debug("Currently eval results in {}. ", eval_result) self.__listorder = _original_listorder if self.__varname is not None: if isinstance(self.__varname, list): @@ -517,6 +519,7 @@ def write_to_logger(self): self._log_debug("{0} from variable: {1}", self.__name, i) else: self._log_debug("{0} from variable: {1}", self.__name, self.__varname) + return eval_result # Get Text (similar to logger text) # prefix: Prefix for text From 0764ba8570a38ed2f55b9e5f2f08b15d07a3cce4 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Sep 2024 14:49:06 +0200 Subject: [PATCH 067/121] stateengine plugin: add action_type to action(s) for better webif handling (coming) --- stateengine/StateEngineAction.py | 21 ++++++++++++++------- stateengine/StateEngineActions.py | 10 ++++++++-- stateengine/StateEngineState.py | 8 ++++---- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 26e9737ea..bc4ea2e2f 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -84,6 +84,7 @@ def __init__(self, abitem, name: str): self._retrigger_issue = None self._suspend_issue = None self.__queue = abitem.queue + self.__action_type = None def update_delay(self, value): _issue_list = [] @@ -548,7 +549,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible @@ -653,7 +654,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items, use) self._action_status = _issue @@ -803,7 +805,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -861,7 +864,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -936,7 +940,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} @@ -1035,7 +1040,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items, use) self._action_status = _issue @@ -1221,7 +1227,8 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type if isinstance(self.__value, list): item = self.__value[0].property.path else: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 0cb1b2c9b..35723c2b7 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -33,6 +33,8 @@ class SeActions(StateEngineTools.SeItemChild): def __init__(self, abitem): super().__init__(abitem) self.__actions = {} + self.__action_type = None + self.__state = None self.__unassigned_delays = {} self.__unassigned_repeats = {} self.__unassigned_instantevals = {} @@ -527,16 +529,20 @@ def __raise_missing_parameter_error(self, parameter, param_name): # Check the actions optimize and complete them # state: state (item) to read from - def complete(self, state, evals_items=None, use=None): + def complete(self, state, action_type, evals_items=None, use=None): + self.__action_type = action_type + self.__state = state + _status = {} if use is None: use = state.use.get() for name in self.__actions: try: - _status.update(self.__actions[name].complete(state, evals_items, use)) + _status.update(self.__actions[name].complete(state, action_type, evals_items, use)) except ValueError as ex: _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) raise ValueError("State '{0}', Action '{1}': {2}".format(state.id, name, ex)) + self._log_debug("Completing {} for state {} status {}", self.__actions, state, _status) return _status def set(self, value): diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 31c066957..4a236e031 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -710,19 +710,19 @@ def update_action_status(action_status, actiontype): if recursion_depth == 0: self.__conditions.complete(self, use) - _action_status = self.__actions_enter.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_enter.complete(self, 'actions_enter', self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'enter') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_stay.complete(self,'actions_stay', self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_enter_or_stay.complete(self, 'actions_enter_or_stay', self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(self, self.__conditions.evals_items, use) + _action_status = self.__actions_leave.complete(self, 'actions_leave',self.__conditions.evals_items, use) if _action_status: update_action_status(_action_status, 'leave') self._abitem.update_action_status(self.__action_status) From 705e7487cfe395ecfbb992ed5da3f5055f7f76f8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 3 Sep 2024 14:49:34 +0200 Subject: [PATCH 068/121] stateengine plugin: minor code improvement --- stateengine/StateEngineAction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index bc4ea2e2f..5f7258798 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -686,14 +686,14 @@ def _can_execute(self, state): self._log_increase_indent() self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'No item defined') + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No item defined'.format(self._name)) return False if self.__value.is_empty(): self._log_increase_indent() self._log_warning("Action '{0}': No value for item {1} defined. Ignoring.", self._name, self.__item) self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'No value for item {}'.format(self.__item)) + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, self.__item)) return False self.update_webif_actionstatus(state, self._name, 'True') return True From d361a3ef836a92b30db4117cb3fd280ffa8b6e1b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Tue, 10 Sep 2024 15:35:22 +0200 Subject: [PATCH 069/121] stateengine plugin: fix previousonditionset variable --- stateengine/StateEngineConditionSet.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index ed4e4b6d0..a89aeebdf 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -181,18 +181,12 @@ def __currentconditionset_set(self, conditionsetid, name): self._abitem.set_variable('current.conditionset_id', conditionsetid) self._abitem.set_variable('current.conditionset_name', name) - def __previousconditionset_set(self, conditionsetid, name): - self._abitem.set_variable('previous.conditionset_id', conditionsetid) - self._abitem.set_variable('previous.conditionset_name', name) - # Check all conditions in the condition set. Return # returns: True = all conditions in set are matching, False = at least one condition is not matching def all_conditions_matching(self, state): try: self._log_info("Check condition set '{0}'", self.__name) self._log_increase_indent() - self.__previousconditionset_set(self._abitem.get_variable('current.conditionset_id'), - self._abitem.get_variable('current.conditionset_name')) self.__currentconditionset_set(self.__id.property.path, self.__name) for name in self.__conditions: From a3aceb22372ff4648e04939a4724a49a8fd9ae88 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 09:21:14 +0200 Subject: [PATCH 070/121] stateengine plugin: import fix when checking regexes for conditions --- stateengine/StateEngineCondition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index abfe71745..e2ebabbe1 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -508,7 +508,7 @@ def __convert(convert_value, convert_current): self._abitem.update_webif(_key_match, 'yes') return True if regex_check is True: - self._log_debug("Regex '{}' result: {}, element {}", element, regex_result) + self._log_debug("Regex '{0}' result: {1}.", element, regex_result) if negate: self._log_debug("{0} not in list -> matching", current) From a45bbb52a7b248498c2d802664c64ffca1adcc7a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 14:20:33 +0200 Subject: [PATCH 071/121] stateengine plugin: minor code adjustments --- stateengine/StateEngineAction.py | 1 - stateengine/StateEngineActions.py | 8 ++-- stateengine/StateEngineCondition.py | 70 ++++++++++++++--------------- stateengine/StateEngineFunctions.py | 12 ++--- stateengine/StateEngineItem.py | 41 ++++++++--------- stateengine/StateEngineState.py | 10 ++--- stateengine/StateEngineStructs.py | 2 +- stateengine/StateEngineValue.py | 6 +-- stateengine/StateEngineWebif.py | 39 +++++++++------- 9 files changed, 98 insertions(+), 91 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 5f7258798..00b236358 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -301,7 +301,6 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - self._log_develop("For action {} check item {} status {} mindelta {} value {} actiontype {}, use {} evals_items {}", self, check_item, check_status, check_mindelta, check_value, action_type, use, evals_items) try: _name = evals_items.get(self.name) if _name is not None: diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 35723c2b7..5bae5e249 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -334,12 +334,12 @@ def __ensure_action_exists(self, func, name): return True, _issue_list def __handle_combined_action_attribute(self, name, value_list): - def remove_action(ex): + def remove_action(e): if name in self.__actions: del self.__actions[name] - _issue = {name: {'issue': [ex], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} - _issue_list.append(_issue) - self._log_warning("Ignoring action {0} because: {1}", name, ex) + i = {name: {'issue': [e], 'issueorigin': [{'state': 'unknown', 'action': parameter['function']}], 'ignore': True}} + _issue_list.append(i) + self._log_warning("Ignoring action {0} because: {1}", name, e) parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'nextconditionset': None, 'conditionset': None, 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index e2ebabbe1..737130c34 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -292,14 +292,14 @@ def complete(self, state, use): if all(item is None for item in [self.__item, self.__status, self.__eval, self.__status_eval]): raise ValueError("Neither 'item' nor 'status' nor '(status)eval' given!") - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__changedby.is_empty() and self.__changedbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__changedby.is_empty() and self.__changedbynegate is None: self.__changedbynegate = False - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__updatedby.is_empty() and self.__updatedbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__updatedby.is_empty() and self.__updatedbynegate is None: self.__updatedbynegate = False - if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval])\ - and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: + if any(item is not None for item in [self.__item, self.__status, self.__eval, self.__status_eval]) \ + and not self.__triggeredby.is_empty() and self.__triggeredbynegate is None: self.__triggeredbynegate = False # cast stuff @@ -462,19 +462,19 @@ def __convert(convert_value, convert_current): self.__value.set_cast(StateEngineTools.cast_str) convert_value = StateEngineTools.cast_str(convert_value) convert_current = StateEngineTools.cast_str(convert_current) - if not type(_oldvalue) == type(convert_value): + if not type(_oldvalue) is type(convert_value): self._log_debug("Value {} was type {} and therefore not the same" " type as item value {}. It got converted to {}.", _oldvalue, type(_oldvalue), convert_current, type(convert_value)) return convert_value, convert_current - current = self.__get_current(eval_type='changedby') if valuetype == "changedby" else\ - self.__get_current(eval_type='updatedby') if valuetype == "updatedby" else\ - self.__get_current(eval_type='triggeredby') if valuetype == "triggeredby" else\ + current = self.__get_current(eval_type='changedby') if valuetype == "changedby" else \ + self.__get_current(eval_type='updatedby') if valuetype == "updatedby" else \ + self.__get_current(eval_type='triggeredby') if valuetype == "triggeredby" else \ self.__get_current(eval_type='value') - negate = self.__changedbynegate if valuetype == "changedby" else\ - self.__updatedbynegate if valuetype == "updatedby" else\ - self.__triggeredbynegate if valuetype == "triggeredby" else\ + negate = self.__changedbynegate if valuetype == "changedby" else \ + self.__updatedbynegate if valuetype == "updatedby" else \ + self.__triggeredbynegate if valuetype == "triggeredby" else \ self.__negate _key_current = ['{}'.format(state.id), 'conditionsets', '{}'.format( self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), @@ -490,20 +490,20 @@ def __convert(convert_value, convert_current): for i, element in enumerate(value): regex_result = None regex_check = False - if valuetype == "value" and type(element) != type(current) and current is not None: + if valuetype == "value" and type(element) is not type(current) and current is not None: element, current = __convert(element, current) if isinstance(element, re.Pattern): regex_result = element.fullmatch(str(current)) regex_check = True if negate: - if (regex_result is not None and regex_check is True)\ - or (current == element and regex_check is False): + if (regex_result is not None and regex_check is True) \ + or (current == element and regex_check is False): self._log_debug("{0} found but negated -> not matching", element) self._abitem.update_webif(_key_match, 'no') return False else: - if (regex_result is not None and regex_check is True)\ - or (current == element and regex_check is False): + if (regex_result is not None and regex_check is True) \ + or (current == element and regex_check is False): self._log_debug("{0} found -> matching", element) self._abitem.update_webif(_key_match, 'yes') return True @@ -522,7 +522,7 @@ def __convert(convert_value, convert_current): regex_result = None regex_check = False # If current and value have different types, convert both to string - if valuetype == "value" and type(value) != type(current) and current is not None: + if valuetype == "value" and type(value) is not type(current) and current is not None: value, current = __convert(value, current) text = "Condition '{0}': {1}={2} negate={3} current={4}" self._abitem.update_webif(_key_current, str(current)) @@ -532,14 +532,14 @@ def __convert(convert_value, convert_current): regex_result = value.fullmatch(str(current)) regex_check = True if negate: - if (regex_result is None and regex_check is True)\ - or (current != value and regex_check is False): + if (regex_result is None and regex_check is True) \ + or (current != value and regex_check is False): self._log_debug("not OK but negated -> matching") self._abitem.update_webif(_key_match, 'yes') return True else: - if (regex_result is not None and regex_check is True)\ - or (current == value and regex_check is False): + if (regex_result is not None and regex_check is True) \ + or (current == value and regex_check is False): self._log_debug("OK -> matching") self._abitem.update_webif(_key_match, 'yes') return True @@ -823,10 +823,10 @@ def check_eval(eval_or_status_eval): eval_result = eval(eval_or_status_eval) if isinstance(eval_result, self.__itemClass): value = eval_result.property.last_change_age if eval_type == 'age' else \ - eval_result.property.last_change_by if eval_type == 'changedby' else \ - eval_result.property.last_update_by if eval_type == 'updatedby' else \ - eval_result.property.last_trigger_by if eval_type == 'triggeredby' else \ - eval_result.property.value + eval_result.property.last_change_by if eval_type == 'changedby' else \ + eval_result.property.last_update_by if eval_type == 'updatedby' else \ + eval_result.property.last_trigger_by if eval_type == 'triggeredby' else \ + eval_result.property.value else: value = eval_result except Exception as ex: @@ -840,10 +840,10 @@ def check_eval(eval_or_status_eval): if self.__status is not None: # noinspection PyUnusedLocal self._log_debug("Trying to get {} of status item {}", eval_type, self.__status.property.path) - 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.last_trigger_by if eval_type == 'triggeredby' else\ + 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.last_trigger_by if eval_type == 'triggeredby' else \ self.__status.property.value elif self.__status_eval is not None: self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) @@ -852,10 +852,10 @@ def check_eval(eval_or_status_eval): elif self.__item is not None: # noinspection PyUnusedLocal self._log_debug("Trying to get {} of item {}", eval_type, self.__item.property.path) - 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\ - self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ + 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 \ + self.__item.property.last_trigger_by if eval_type == 'triggeredby' else \ self.__item.property.value elif self.__eval is not None: self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) diff --git a/stateengine/StateEngineFunctions.py b/stateengine/StateEngineFunctions.py index ae5c2d7b1..1e869d033 100755 --- a/stateengine/StateEngineFunctions.py +++ b/stateengine/StateEngineFunctions.py @@ -85,14 +85,14 @@ def check_include_exclude(entry_type): # If current value is in list -> Return "Trigger" for e in conf_entry: e = re.compile(e, re.IGNORECASE) - result = e.match(original) - elog.info("Checking regex result {}", result) - if result is not None: + r = e.match(original) + elog.info("Checking regex result {}", r) + if r is not None: elog.info("{0}: matching.", e) elog.decrease_indent() - returnvalue = retval_trigger if entry_type == "include" else retval_no_trigger - elog.info("Writing value {0}", returnvalue) - return returnvalue + retval = retval_trigger if entry_type == "include" else retval_no_trigger + elog.info("Writing value {0}", retval) + return retval elog.info("{0}: not matching", e) elog.decrease_indent() return None diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index aa4ce8285..57da47a73 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -217,8 +217,9 @@ def __init__(self, smarthome, item, se_plugin): self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") _default_log_level = self.__logger.default_log_level.get() - _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, "se_log_level", - _default_log_level) + _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, + "se_log_level", + _default_log_level) self.__using_default_log_level = _using_default _returnvalue = self.__log_level.get() if isinstance(_returnvalue, list) and len(_returnvalue) == 1: @@ -880,10 +881,10 @@ def __update_can_release(self, can_release, new_state=None): def __handle_releasedby(self, new_state, last_state, instant_leaveaction): def update_can_release_list(): - for e in _returnvalue: - e = self.__update_release_item_value(e, new_state) - e = e if isinstance(e, list) else [e] - for entry in e: + for r in _returnvalue: + r = self.__update_release_item_value(r, new_state) + r = r if isinstance(r, list) else [r] + for entry in r: if entry and state.id not in can_release.setdefault(entry, [state.id]): can_release[entry].append(state.id) @@ -1046,19 +1047,19 @@ def update_list(existing, new_entries): existing.append(entry) return existing - combined_dict = dict1.copy() + comb_dict = dict1.copy() for key, value in dict2.items(): - if key not in combined_dict: - combined_dict[key] = value + if key not in comb_dict: + comb_dict[key] = value continue - combined_entry = combined_dict[key] + combined_entry = comb_dict[key] if 'issue' in value: combined_entry['issue'] = update_list(combined_entry.get('issue', []), value['issue']) if 'issueorigin' in value: combined_entry['issueorigin'] = update_list(combined_entry.get('issueorigin', []), value['issueorigin']) - return combined_dict + return comb_dict if issue_type == "state": combined_dict = combine_dicts(issues, self.__state_issues) @@ -1317,7 +1318,7 @@ def __initialize_state(self, item_state, _statecount): _issue = _state.update_order(_statecount) if _issue: self.__config_issues.update({item_state.property.path: - {'issue': _issue, 'attribute': 'se_stateorder'}}) + {'issue': _issue, 'attribute': 'se_stateorder'}}) self.__logger.error("Issue with state {0} while setting order: {1}", item_state.property.path, _issue) self.__states.append(_state) @@ -1330,13 +1331,13 @@ def __initialize_state(self, item_state, _statecount): return _statecount + 1 except ValueError as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'ValueError'}]}}) + [{'conditionset': 'None', 'condition': 'ValueError'}]}}) self.__logger.error("Ignoring state {0} because ValueError: {1}", item_state.property.path, ex) return _statecount except Exception as ex: self.update_issues('state', {item_state.property.path: {'issue': ex, 'issueorigin': - [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) + [{'conditionset': 'None', 'condition': 'GeneralError'}]}}) self.__logger.error("Ignoring state {0} because: {1}", item_state.property.path, ex) return _statecount @@ -1790,11 +1791,11 @@ def process_returnvalue(value): return _returnvalue_issue def update_can_release_list(): - for i, value in enumerate(_convertedlist): - if _converted_typelist[i] == 'item': - value = self.__update_release_item_value(_converted_evaluatedlist[i], state) - elif _converted_typelist[i] == 'eval': - value = _converted_evaluatedlist[i] + for z, value in enumerate(_convertedlist): + if _converted_typelist[z] == 'item': + value = self.__update_release_item_value(_converted_evaluatedlist[z], state) + elif _converted_typelist[z] == 'eval': + value = _converted_evaluatedlist[z] value = value if isinstance(value, list) else [value] for v in value: if v and can_release.get(v) and state.id not in can_release.get(v): @@ -1913,7 +1914,7 @@ def cli_detail(self, handler): handler.push("\tPrevious conditionset: {0} ('{1}')\n".format(self.get_previousconditionset_id(), self.get_previousconditionset_name())) handler.push("\tNext conditionset: {0} ('{1}')\n".format(self.get_nextconditionset_id(), - self.get_nextconditionset_name())) + self.get_nextconditionset_name())) handler.push(self.__startup_delay.get_text("\t", "\n")) handler.push("\tCycle: {0}\n".format(cycles)) handler.push("\tCron: {0}\n".format(crons)) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 4a236e031..5d241a940 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -489,9 +489,8 @@ def __initialize_se_use(self, state, recursion_depth): _configorigvalue[i] in item)): _issue_list = [item for key, value in _issues.items() if value for item in value] self._log_warning("se_use {} points to invalid item. Ignoring.", _configorigvalue[i]) - self._abitem.update_issues('config', {state.id: - {'issue': _issue_list, - 'attribute': 'se_use', 'origin': state_type}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) self.__use_ignore_list.append(_configorigvalue[i]) _path = None elif _returntype[i] in ['item', 'eval']: @@ -505,9 +504,8 @@ def __initialize_se_use(self, state, recursion_depth): _issue_list = [item for key, value in _issues.items() if value for item in value] self._log_warning("se_use {} defined by invalid item/eval. Ignoring.", _path) - self._abitem.update_issues('config', {state.id: - {'issue': _issue_list, - 'attribute': 'se_use', 'origin': state_type}}) + self._abitem.update_issues('config', {state.id: {'issue': _issue_list, + 'attribute': 'se_use', 'origin': state_type}}) self.__use_ignore_list.append(_path) _path = None if _path is None: diff --git a/stateengine/StateEngineStructs.py b/stateengine/StateEngineStructs.py index de630d0ab..98a6afeb2 100755 --- a/stateengine/StateEngineStructs.py +++ b/stateengine/StateEngineStructs.py @@ -25,7 +25,7 @@ def create(_abitem, struct): - _find_result = next((item for item in __allstructs if item["name"] == struct), False) + _find_result = next((item for item in __allstructs if item["name"] == struct), {}) if not _find_result: created_struct = StateEngineStruct.SeStructMain(_abitem, struct, global_struct) __allstructs.append({'name': struct, 'struct': created_struct}) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 061f231df..b5aefdf75 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -136,7 +136,8 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at self._log_develop("Setting value {0}, attribute name {1}, reset {2}, type {3}", value, attribute_name, reset, attr_type) _returnvalue, _returntype, _issue, _origvalue = self.set(value, attribute_name, reset) - self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}, original {}", _returnvalue, _returntype, _issue, _origvalue) + self._log_develop("Set from attribute returnvalue {}, returntype {}, issue {}, original {}", + _returnvalue, _returntype, _issue, _origvalue) return _returnvalue, _returntype, _using_default, _issue, _origvalue def _set_additional(self, _additional_sources): @@ -470,7 +471,6 @@ def get_type(self): # Write condition to logger def write_to_logger(self): - eval_result = None if self.__template is not None: self._log_info("{0}: Using template(s) {1}", self.__name, self.__template) if self.__value is not None: @@ -1007,6 +1007,6 @@ def update_value(varname): var, values[-1], self.__listorder) else: values = update_value(self.__varname) - self._log_debug("Variable result: {0}", values) + self._log_debug("Variable result: {0}", values) return values diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index ab3e4df9b..4a9b99b06 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -59,19 +59,27 @@ def __init__(self, abitem): def __repr__(self): return "WebInterface item: {}, id {}".format(self.__states, self.__name) if REQUIRED_PACKAGE_IMPORTED else "None" + def _strip_regex(self, regex_list): + pattern_strings = [] + if not isinstance(regex_list, list): + regex_list = [regex_list] + for item in regex_list: + if isinstance(item, re.Pattern): + pattern_strings.append(item.pattern) + else: + pattern_match = re.search(r"re\.compile\('([^']*)'", item) + if pattern_match: + item = f"regex:{pattern_match.group(1)}" + pattern_strings.append(str(item)) + if len(pattern_strings) <= 1: + pattern_strings = pattern_strings[0] + return str(pattern_strings) + def _actionlabel(self, state, label_type, conditionset, previousconditionset, previousstate_conditionset, next_conditionset): # Check if conditions for action are met or not # action_dict: abitem[state]['on_enter'/'on_stay'/'on_enter_or_stay'/'on_leave'].get(action) # condition_to_meet: 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' # conditionset: name of conditionset that should get checked - def _strip_regex(regex_list): - pattern_strings = [] - for item in regex_list: - if isinstance(item, re.Pattern): - pattern_strings.append(item.pattern) - else: - pattern_strings.append(str(item)) - return str(pattern_strings) def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: str): _condition_check = action_dict.get(condition_to_meet) @@ -136,10 +144,10 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s (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 or _issue else "black" - condition_info = _strip_regex(condition_to_meet) if condition1 is False \ - else _strip_regex(previouscondition_to_meet) if condition2 is False \ - else _strip_regex(previousstate_condition_to_meet) if condition3 is False \ - else _strip_regex(next_condition_to_meet) if condition4 is False \ + condition_info = self._strip_regex(condition_to_meet) if condition1 is False \ + else self._strip_regex(previouscondition_to_meet) if condition2 is False \ + else self._strip_regex(previousstate_condition_to_meet) if condition3 is False \ + else self._strip_regex(next_condition_to_meet) if condition4 is False \ else "" if _issue: if tooltip_count > 0: @@ -237,7 +245,7 @@ def _conditionlabel(self, state, conditionset, i): text = " Current {}".format(current_clean) if current and len(current) > 0 else " Not evaluated." conditionlist += ('' - '' + '' '
    {}:{}
    ').format(condition.upper(), text) conditions_done.append(condition) conditionlist += '' @@ -246,6 +254,7 @@ def _conditionlabel(self, state, conditionset, i): info_eval = str(condition_dict.get('eval') or '') info_status_eval = str(condition_dict.get('status_eval') or '') info_compare = str(condition_dict.get(compare) or '') + info_compare = self._strip_regex(info_compare) if not status_none: textlength = len(info_status) if textlength > self.__textlimit: @@ -502,7 +511,7 @@ def drawgraph(self, filename): self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( '{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond", label=label, pos=position) - #self._log_debug('Node {} {} drawn', state, conditionset) + #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist) position = '{},{}!'.format(0.2, new_y) xlabel = '1 tooltip' if condition_tooltip_count == 1\ else '{} tooltips'.format(condition_tooltip_count)\ @@ -562,7 +571,7 @@ def drawgraph(self, filename): xlabel = "" if j == 0: self.__graph.add_edge(pydotplus.Edge(self.__nodes[state], self.__nodes['{}_right'.format(state)], - style='bold', color='black', dir='none', + style='bold', color='black', dir='none', xlabel=xlabel, edgetooltip='check first conditionset')) self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_right'.format(state)], self.__nodes['{}_{}'.format(state, conditionset)], From 194d08e1cf072bd0af1b4ec4f391ec82c6e51923 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 14:39:03 +0200 Subject: [PATCH 072/121] stateengine plugin: improve web interface --- stateengine/StateEngineWebif.py | 35 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index 4a9b99b06..af031d02a 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -102,8 +102,9 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s actionlabel = actionstart = '<' action_tooltip = '' originaltype = label_type - types = [label_type] if label_type == 'actions_leave' else ['actions_enter_or_stay', label_type] + types = [label_type] if label_type in ['actions_leave', 'actions_pass'] else ['actions_enter_or_stay', label_type] tooltip_count = 0 + for label_type in types: for action in self.__states[state].get(label_type): action_dict = self.__states[state][label_type].get(action) @@ -211,12 +212,12 @@ def _conditionlabel(self, state, conditionset, i): if _empty_set: return '', '', 0 conditionlist = '<
    ' + conditionlist += ''.format(conditionset) tooltip_count = 0 for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)): condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition) current = condition_dict.get('current') match = condition_dict.get('match') - status_none = str(condition_dict.get('status')) == 'None' item_none = str(condition_dict.get('item')) == 'None' or not status_none status_eval_none = condition_dict.get('status_eval') == 'None' @@ -236,23 +237,19 @@ def _conditionlabel(self, state, conditionset, i): 'updatedbynegate', 'triggeredbynegate', 'status', 'current', 'match', 'status_eval'] if cond1 and compare not in excluded_values: - try: - list_index = list(self.__states.keys()).index(self.__active_state) - except Exception: - list_index = 0 if condition not in conditions_done: current_clean = ", ".join(f"{k} = {v}" for k, v in current.items()) - text = " Current {}".format(current_clean) if current and len(current) > 0 else " Not evaluated." + text = " Current {}".format(current_clean) if current is not None and len(current) > 0 else " Not evaluated." conditionlist += ('').format(condition.upper(), text) conditions_done.append(condition) conditionlist += ''.format(info) comparison = ">=" if not min_none and compare == "min"\ else "<=" if not max_none and compare == "max"\ @@ -345,10 +343,9 @@ def _conditionlabel(self, state, conditionset, i): 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 '' - active = i < list_index or (i == list_index and conditionset in ['', self.__active_conditionset]) - match_info = '' if match_info == 'yes' and active\ - else '' if match_info == 'no' and active\ - else '' if match_info and len(match_info) > 0 and active\ + match_info = '' if match_info == 'yes'\ + else '' if match_info == 'no'\ + else '' if match_info and len(match_info) > 0 \ else '' conditionlist += ''.format(match_info) conditionlist += '
    {}
    ' '' '
    {}:{}
    ' - info_status = str(condition_dict.get('status') or '') - info_item = str(condition_dict.get('item') or '') - info_eval = str(condition_dict.get('eval') or '') - info_status_eval = str(condition_dict.get('status_eval') or '') + info_status = str(condition_dict.get('status') or 'n/a') + info_item = str(condition_dict.get('item') or 'n/a') + info_eval = str(condition_dict.get('eval') or 'n/a') + info_status_eval = str(condition_dict.get('status_eval') or 'n/a') info_compare = str(condition_dict.get(compare) or '') info_compare = self._strip_regex(info_compare) if not status_none: @@ -307,7 +304,8 @@ def _conditionlabel(self, state, conditionset, i): elif not item_none: info = info_item else: - info = "" + info = "n/a" + conditionlist += '{}{}
    >' @@ -357,14 +354,14 @@ def _conditionlabel(self, state, conditionset, i): def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, cond2): cond4 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond5 = self.__states[state]['conditionsets'].get(conditionset) is not None - cond_enter = action_type == 'actions_enter' and self.__states[state].get('enter') is False - cond_stay = action_type == 'actions_stay' and self.__states[state].get('stay') is False + cond_enter = action_type in ['actions_enter', 'actions_enter_or_stay'] and self.__states[state].get('enter') is False + cond_stay = action_type in ['actions_stay', 'actions_enter_or_stay'] and self.__states[state].get('stay') is False color_enter = "gray" if (cond1 and cond2 and cond5) or \ (cond_enter and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" color_stay = "gray" if (cond1 and cond2 and cond5) or \ (cond_stay and cond4 and cond5) else "olivedrab" if cond4 else "indianred2" - label = 'first enter' if action_type == 'actions_enter' else 'staying at state' + label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying at state' position = '{},{}!'.format(0.63, new_y) color = color_enter if label == 'first enter' else color_stay From 611745d884213ad125588ca297366f28c7100ddb Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 14:53:06 +0200 Subject: [PATCH 073/121] stateengine plugin: major code updates, introduce eval cache feature, improve web interface update, improve data handling when writing logs, etc. --- stateengine/StateEngineAction.py | 759 +++++++++---------------- stateengine/StateEngineActions.py | 12 - stateengine/StateEngineCondition.py | 96 ++-- stateengine/StateEngineConditionSet.py | 6 +- stateengine/StateEngineItem.py | 56 +- stateengine/StateEngineState.py | 76 ++- stateengine/StateEngineValue.py | 26 +- 7 files changed, 421 insertions(+), 610 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 00b236358..3a33966c4 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -84,7 +84,9 @@ def __init__(self, abitem, name: str): self._retrigger_issue = None self._suspend_issue = None self.__queue = abitem.queue - self.__action_type = None + self._action_type = None + self._state = None + self._info_dict = {} def update_delay(self, value): _issue_list = [] @@ -162,60 +164,56 @@ def get_order(self): def update_webif_actionstatus(self, state, name, success, issue=None): try: - if self._abitem.webif_infos[state.id].get('actions_stay'): - _key = ['{}'.format(state.id), 'actions_stay', '{}'.format(name), 'actionstatus', 'success'] - self._abitem.update_webif(_key, success) - _key = ['{}'.format(state.id), 'actions_stay', '{}'.format(name), 'actionstatus', 'issue'] - self._abitem.update_webif(_key, issue) - except Exception: - pass - try: - if self._abitem.webif_infos[state.id].get('actions_enter'): - _key = ['{}'.format(state.id), 'actions_enter', '{}'.format(name), 'actionstatus', 'success'] - self._abitem.update_webif(_key, success) - _key = ['{}'.format(state.id), 'actions_enter', '{}'.format(name), 'actionstatus', 'issue'] - self._abitem.update_webif(_key, issue) - except Exception: - pass - try: - if self._abitem.webif_infos[state.id].get('actions_enter_or_stay'): - _key = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(name), 'actionstatus', 'success'] - self._abitem.update_webif(_key, success) - _key = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(name), 'actionstatus', 'issue'] - self._abitem.update_webif(_key, issue) - except Exception: - pass - try: - state.update_name(state.state_item) - _key_name = ['{}'.format(state.id), 'name'] - self._abitem.update_webif(_key_name, state.name) - if self._abitem.webif_infos[state.id].get('actions_leave'): - _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(name), 'actionstatus', 'success'] + if self._action_type == "actions_leave": + state.update_name(state.state_item) + _key_name = ['{}'.format(state.id), 'name'] + self._abitem.update_webif(_key_name, state.name, True) + if self._abitem.webif_infos[state.id].get(self._action_type): + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'success'] self._abitem.update_webif(_key, success) - _key = ['{}'.format(state.id), 'actions_leave', '{}'.format(name), 'actionstatus', 'issue'] + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'issue'] self._abitem.update_webif(_key, issue) - except Exception: - pass + except Exception as ex: + self._log_warning("Error setting action status {}: {}", name, ex) # Write action to logger def write_to_logger(self): self._log_info("function: {}", self._function) - self.__delay.write_to_logger() + delay = self.__delay.write_to_logger() or 0 if self.__repeat is not None: - self.__repeat.write_to_logger() + repeat = self.__repeat.write_to_logger() + else: + repeat = False if self.__instanteval is not None: - self.__instanteval.write_to_logger() + instanteval = self.__instanteval.write_to_logger() + else: + instanteval = False if self.nextconditionset is not None: - self.nextconditionset.write_to_logger() + nextconditionset = self.nextconditionset.write_to_logger() + else: + nextconditionset = None if self.conditionset is not None: - self.conditionset.write_to_logger() + conditionset = self.conditionset.write_to_logger() + else: + conditionset = None if self.previousconditionset is not None: - self.previousconditionset.write_to_logger() + previousconditionset = self.previousconditionset.write_to_logger() + else: + previousconditionset = None if self.previousstate_conditionset is not None: - self.previousstate_conditionset.write_to_logger() + previousstate_conditionset = self.previousstate_conditionset.write_to_logger() + else: + previousstate_conditionset = None if self.__mode is not None: - self.__mode.write_to_logger() - self.__order.write_to_logger() + mode = self.__mode.write_to_logger() + else: + mode = None + order = self.__order.write_to_logger() or 0 + self._info_dict.update({'function': str(self._function), 'nextconditionset': nextconditionset, + 'conditionset': conditionset, 'repeat': str(repeat), 'delay': str(delay), 'mode': mode, + 'order': str(order), 'previousconditionset': previousconditionset, + 'instanteval': str(instanteval), 'previousstate_conditionset': previousstate_conditionset, + 'actionstatus': {}}) def set_source(self, current_condition, previous_condition, previousstate_condition, next_condition): source = [] @@ -422,21 +420,25 @@ def _check_condition(condition: str): # update web interface with delay info # action_type: 'actions_enter', etc. # delay_info: delay information - def _update_delay_webif(action_type: str, delay_info: str): + def _update_delay_webif(delay_info: str): + if self._action_type == "actions_leave": + try: + state.update_name(state.state_item) + _key_name = ['{}'.format(state.id), 'name'] + self._abitem.update_webif(_key_name, state.name, True) + except Exception: + pass try: - _key = ['{}'.format(state.id), '{}'.format(action_type), '{}'.format(self._name), 'delay'] - self._abitem.update_webif(_key, delay_info) + _key = ['{}'.format(state.id), self._action_type, '{}'.format(self._name), 'delay'] + self._abitem.update_webif(_key, delay_info, True) except Exception: pass # update web interface with repeat info # value: bool type True or False for repeat value def _update_repeat_webif(value: bool): - _key1 = ['{}'.format(state.id), 'actions_stay', '{}'.format(self._name), 'repeat'] - _key2 = ['{}'.format(state.id), 'actions_enter_or_stay', '{}'.format(self._name), 'repeat'] - result = self._abitem.update_webif(_key1, value) - if result is False: - self._abitem.update_webif(_key2, value) + _key1 = ['{}'.format(state.id), self._action_type, '{}'.format(self._name), 'repeat'] + self._abitem.update_webif(_key1, value, True) self._log_decrease_indent(50) self._log_increase_indent() @@ -530,16 +532,7 @@ def _update_repeat_webif(value: bool): else: self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met, next_condition_met) - _update_delay_webif('actions_stay', str(_delay_info)) - _update_delay_webif('actions_enter', str(_delay_info)) - _update_delay_webif('actions_enter_or_stay', str(_delay_info)) - try: - state.update_name(state.state_item) - _key_name = ['{}'.format(state.id), 'name'] - self._abitem.update_webif(_key_name, state.name) - _update_delay_webif('actions_leave', str(_delay_info)) - except Exception: - pass + _update_delay_webif(str(_delay_info)) # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute @@ -555,9 +548,6 @@ def complete(self, state, action_type, evals_items=None, use=None): def _can_execute(self, state): return True - def get(self): - return True - def _waitforexecute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", delay: int = 0, current_condition: list[str] = None, previous_condition: list[str] = None, previousstate_condition: list[str] = None, next_condition: list[str] = None): if current_condition is None: current_condition = [] @@ -616,83 +606,91 @@ def _getitem_fromeval(self): return -# Class representing a single "se_set" action -class SeActionSetItem(SeActionBase): - # Initialize the action - # abitem: parent SeItem instance - # name: Name of action +# Class with methods that are almost the same for set and force +class SeActionMixSetForce: def __init__(self, abitem, name: str): super().__init__(abitem, name) - self.__item = None - self.__eval_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" - - def __repr__(self): - return "SeAction Set {}".format(self._name) + self._item = None + self._eval_item = None + self._status = None + self._delta = 0 + self._value = StateEngineValue.SeValue(self._abitem, "value") + self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") def _getitem_fromeval(self): - if self.__item is None: + if self._item is None: return - self.__eval_item = self.__item - self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, - self.__mindelta) - if self.__item is None: + self._eval_item = self._item + self._item, self._value, self._mindelta, _issue = self.check_getitem_fromeval(self._item, self._value, + self._mindelta) + if self._item is None: self._action_status = _issue - raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) + raise Exception("Problem evaluating item '{}' from eval.".format(self._item)) # set the action based on a set_(action_name) attribute # value: Value of the set_(action_name) attribute def update(self, value): - _, _, _issue, _ = self.__value.set(value) + _, _, _issue, _ = self._value.set(value) _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue - # Complete action - # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type - self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "set", evals_items, use) - self._action_status = _issue - return _issue - - # Write action to logger def write_to_logger(self): SeActionBase.write_to_logger(self) - if isinstance(self.__item, str): + if isinstance(self._item, str): try: - self._log_debug("item from eval: {0}", self.__item) + self._log_debug("item from eval: {0}", self._item) self._log_increase_indent() - current, _, _, _ = self.check_getitem_fromeval(self.__item) + current, _, _, _ = self.check_getitem_fromeval(self._item) self._log_debug("Currently eval results in {}", current) self._log_decrease_indent() except Exception as ex: + current = None self._log_warning("Issue while getting item from eval {}", ex) - elif self.__item is not None: - self._log_debug("item: {0}", self.__item.property.path) + item = current + elif self._item is not None: + self._log_debug("item: {0}", self._item.property.path) + item = self._item.property.path else: self._log_debug("item is not defined! Check log file.") - self.__mindelta.write_to_logger() - self.__value.write_to_logger() + item = None + mindelta = self._mindelta.write_to_logger() or 0 + value = self._value.write_to_logger() + self._info_dict.update({'item': item, 'mindelta': str(mindelta), 'delta': str(self._delta), 'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + return value + + # Complete action + # state: state (item) to read from + def complete(self, state, action_type, evals_items=None, use=None): + self._log_develop('Completing action {}', self._name) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', state.name) + self._action_type = action_type + self._state = state + + self._item, self._status, self._mindelta, self._value, _issue = self.check_complete( + state, self._item, self._status, self._mindelta, self._value, "set/force", evals_items, use) + self._action_status = _issue + + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') + return _issue # Check if execution is possible def _can_execute(self, state): - if self.__item is None: + if self._item is None: self._log_increase_indent() self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) self._log_decrease_indent() self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No item defined'.format(self._name)) return False - if self.__value.is_empty(): + if self._value.is_empty(): self._log_increase_indent() - self._log_warning("Action '{0}': No value for item {1} defined. Ignoring.", self._name, self.__item) + self._log_warning("Action '{0}': No value for item {1} defined. Ignoring.", self._name, self._item) self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, self.__item)) + self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, self._item)) return False self.update_webif_actionstatus(state, self._name, 'True') return True @@ -702,7 +700,10 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() if value is None: - value = self.__value.get() + value = self._value.get() + self._info_dict.update({'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) if value is None: self._log_debug("{0}: Value is None", actionname) @@ -714,71 +715,98 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_decrease_indent() return value - if not self.__mindelta.is_empty(): - mindelta = self.__mindelta.get() - if self.__status is not None: + if not self._mindelta.is_empty(): + mindelta = self._mindelta.get() + if self._status is not None: # noinspection PyCallingNonCallable - delta = float(abs(self.__status() - value)) + delta = float(abs(self._status() - value)) additionaltext = "of statusitem " else: - delta = float(abs(self.__item() - value)) + delta = float(abs(self._item() - value)) additionaltext = "" - self.__delta = delta + self._delta = delta + self._info_dict.update({'delta': str(delta), 'mindelta': str(mindelta)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) if 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) + self._log_debug(text, actionname, self._item.property.path, value, additionaltext, delta, mindelta) self.update_webif_actionstatus(state, self._name, 'False') return + source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) + self._force_set(actionname, self._item, value, source) + self._execute_set_add_remove(state, actionname, namevar, repeat_text, self._item, value, source, current_condition, previous_condition, previousstate_condition, next_condition) - self._execute_set_add_remove(state, actionname, namevar, repeat_text, self.__item, value, current_condition, previous_condition, previousstate_condition, next_condition) + def _force_set(self, actionname, item, value, source): + pass - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition, previous_condition, previousstate_condition, next_condition): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition, previous_condition, previousstate_condition, next_condition): self._log_decrease_indent() self._log_debug("{0}: Set '{1}' to '{2}'{3}", actionname, item.property.path, value, repeat_text) - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) - self.__item = self.__eval_item + self._item = self._eval_item - def get(self): - orig_item = self.__item - try: - self._getitem_fromeval() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - item_from_eval = orig_item if orig_item != self.__item else False - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) + +# Class representing a single "se_set" action +class SeActionSetItem(SeActionMixSetForce, SeActionBase): + # Initialize the action + # abitem: parent SeItem instance + # name: Name of action + def __init__(self, abitem, name: str): + super().__init__(abitem, name) + self._function = "set" + + def __repr__(self): + return "SeAction Set {}".format(self._name) + + # Write action to logger + def write_to_logger(self): + super().write_to_logger() + self._log_debug("force update: no") + + +# Class representing a single "se_force" action +class SeActionForceItem(SeActionMixSetForce, SeActionBase): + # Initialize the action + # abitem: parent SeItem instance + # name: Name of action + def __init__(self, abitem, name: str): + super().__init__(abitem, name) + self._function = "force set" + + def __repr__(self): + return "SeAction Force {}".format(self._name) + + # Write action to logger + def write_to_logger(self): + super().write_to_logger() + self._log_debug("force update: yes") + + def _force_set(self, actionname, item, value, source): + # Set to different value first ("force") + current_value = item() + if current_value == value: + if self._item._type == 'bool': + self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, not value) + item(not value, caller=self._caller, source=source) + elif self._item._type == 'str': + if value != '': + self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, '') + item('', caller=self._caller, source=source) + else: + self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, '-') + item('-', caller=self._caller, source=source) + elif self._item._type == 'num': + self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, current_value+0.1) + item(current_value+0.1, caller=self._caller, source=source) else: - value = None - except Exception: - value = None - self.__item = orig_item - mindelta = self.__mindelta.get() - if mindelta is None: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} + self._log_warning("{0}: Force not implemented for item type '{1}'", actionname, item._type) else: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(mindelta)} - return result + self._log_debug("{0}: New value differs from old value, no force required.", actionname) # Class representing a single "se_setbyattr" action @@ -805,10 +833,16 @@ def update(self, value): # Complete action # state: state (item) to read from def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type + self._log_develop('Completing action {}', self._name) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', state.name) + self._action_type = action_type + self._state = state self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -816,6 +850,9 @@ def write_to_logger(self): SeActionBase.write_to_logger(self) if self.__byattr is not None: self._log_debug("set by attribute: {0}", self.__byattr) + self._info_dict.update({'byattr': self.__byattr}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): @@ -829,12 +866,6 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) - def get(self): - result = {'function': str(self._function), 'byattr': str(self.__byattr), 'nextconditionset': self.nextconditionset.get(), - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_trigger" action class SeActionTrigger(SeActionBase): @@ -864,10 +895,16 @@ def update(self, value): # Complete action # state: state (item) to read from def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type + self._log_develop('Completing action {}', self._name) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', state.name) + self._action_type = action_type + self._state = state self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -876,7 +913,13 @@ def write_to_logger(self): if self.__logic is not None: self._log_debug("trigger logic: {0}", self.__logic) if self.__value is not None: - self._log_debug("value: {0}", self.__value) + value = self.__value.write_to_logger() + else: + value = None + self._info_dict.update( + {'logic': self.__logic, 'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): @@ -889,26 +932,14 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value + self._info_dict.update({'value': str(value)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) self.update_webif_actionstatus(state, self._name, 'True') self._log_info("{0}: Triggering logic '{1}' using value '{2}'.{3}", actionname, self.__logic, value, repeat_text) add_logics = 'logics.{}'.format(self.__logic) if not self.__logic.startswith('logics.') else self.__logic self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) - def get(self): - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'logic': str(self.__logic), - 'value': value, 'nextconditionset': self.nextconditionset.get(), - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_run" action class SeActionRun(SeActionBase): @@ -940,10 +971,16 @@ def update(self, value): # Complete action # state: state (item) to read from def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type + self._log_develop('Completing action {}', self._name) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', state.name) + self._action_type = action_type + self._state = state self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -952,10 +989,26 @@ def write_to_logger(self): if self.__eval is not None: self._log_debug("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) + self._info_dict.update({'eval': str(self.__eval)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + # Really execute the action def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): + def log_conditions(): + if current_condition: + self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) + if previous_condition: + self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) + if previousstate_condition: + self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, + previousstate_condition) + if next_condition: + self._log_debug("Running eval {0} based on next conditionset {1}", self.__eval, next_condition) + self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() + eval_result = '' if isinstance(self.__eval, str): # noinspection PyUnusedLocal sh = self._sh @@ -967,15 +1020,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return eval(self.__eval) - if current_condition: - self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) - if previous_condition: - self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) - if previousstate_condition: - self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) - if next_condition: - self._log_debug("Running eval {0} based on next_condition {1}", self.__eval, next_condition) - eval(self.__eval) + log_conditions() + eval_result = eval(self.__eval) self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: @@ -988,15 +1034,8 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return self.__eval() - if current_condition: - self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) - if previous_condition: - self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) - if previousstate_condition: - self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, previousstate_condition) - if next_condition: - self._log_debug("Running eval {0} based on next conditionset {1}", self.__eval, next_condition) - self.__eval() + log_conditions() + eval_result = self.__eval() self.update_webif_actionstatus(state, self._name, 'True') self._log_decrease_indent() except Exception as ex: @@ -1004,196 +1043,9 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) - - def get(self): - result = {'function': str(self._function), 'eval': str(self.__eval), 'nextconditionset': self.nextconditionset.get(), - 'conditionset': self.conditionset.get(), 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - - -# Class representing a single "se_force" action -class SeActionForceItem(SeActionBase): - # Initialize the action - # abitem: parent SeItem instance - # name: Name of action - def __init__(self, abitem, name: str): - super().__init__(abitem, name) - self.__item = None - self.__eval_item = None - self.__status = None - self.__value = StateEngineValue.SeValue(self._abitem, "value") - self.__delta = 0 - self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") - self._function = "force set" - - def __repr__(self): - return "SeAction Force {}".format(self._name) - - # set the action based on a set_(action_name) attribute - # value: Value of the set_(action_name) attribute - def update(self, value): - _, _, _issue, _ = self.__value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - return _issue - - # Complete action - # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type - self.__item, self.__status, self.__mindelta, self.__value, _issue = self.check_complete( - state, self.__item, self.__status, self.__mindelta, self.__value, "force", evals_items, use) - self._action_status = _issue - return _issue - - # Write action to logger - def write_to_logger(self): - SeActionBase.write_to_logger(self) - if isinstance(self.__item, str): - try: - self._log_debug("item from eval: {0}", self.__item) - self._log_increase_indent() - current, _, _, _ = self.check_getitem_fromeval(self.__item) - self._log_debug("Currently eval results in {}", current) - self._log_decrease_indent() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - elif self.__item is not None: - self._log_debug("item: {0}", self.__item.property.path) - else: - self._log_debug("item is not defined! Check log file.") - 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") - - # Check if execution is possible - def _can_execute(self, state): - if self.__item is None: - self._log_increase_indent() - self._log_warning("Action '{0}': No item defined. Ignoring.", self._name) - self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No item defined'.format(self._name)) - return False - - if self.__value.is_empty(): - self._log_increase_indent() - self._log_warning("Action '{0}': No value defined for item {1}. Ignoring.", self._name, self.__item) - self._log_decrease_indent() - self.update_webif_actionstatus(state, self._name, 'False', 'Action {}: No value for item {}'.format(self._name, self.__item)) - return False - self.update_webif_actionstatus(state, self._name, 'True') - return True - - def _getitem_fromeval(self): - if self.__item is None: - return - self.__eval_item = self.__item - self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, - self.__mindelta) - if self.__item is None: - self._action_status = _issue - raise Exception("Problem evaluating item '{}' from eval.".format(self.__item)) - - # Really execute the action (needs to be implemented in derived classes) - # noinspection PyProtectedMember - def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): - self._abitem.set_variable('current.action_name', namevar) - self._log_increase_indent() - if value is None: - value = self.__value.get() - - if value is None: - self._log_debug("{0}: Value is None", actionname) - pat = r"(?:[^,(]*)\'(.*?)\'" - self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'False', 'Value is None') - return - - if returnvalue: - self._log_decrease_indent() - return value - - if not self.__mindelta.is_empty(): - mindelta = self.__mindelta.get() - 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 = "" - - self.__delta = delta - if 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) - self.update_webif_actionstatus(state, self._name, 'False') - return - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) - # Set to different value first ("force") - current_value = self.__item() - if current_value == value: - if self.__item._type == 'bool': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, not value) - self.__item(not value, caller=self._caller, source=source) - elif self.__item._type == 'str': - if value != '': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, '') - self.__item('', caller=self._caller, source=source) - else: - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, '-') - self.__item('-', caller=self._caller, source=source) - elif self.__item._type == 'num': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, self.__item.property.path, current_value+0.1) - self.__item(current_value+0.1, caller=self._caller, source=source) - else: - self._log_warning("{0}: Force not implemented for item type '{1}'", actionname, self.__item._type) - else: - self._log_debug("{0}: New value differs from old value, no force required.", actionname) - self._log_decrease_indent() - self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, self.__item.property.path, value, repeat_text) - self.update_webif_actionstatus(state, self._name, 'True') - # noinspection PyCallingNonCallable - self.__item(value, caller=self._caller, source=source) - self.__item = self.__eval_item - - def get(self): - orig_item = self.__item - try: - self._getitem_fromeval() - except Exception as ex: - self._log_warning("Issue while getting item from eval {}", ex) - item_from_eval = orig_item if orig_item != self.__item else False - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - self.__item = orig_item - mindelta = self.__mindelta.get() - if mindelta is None: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - else: - result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}, - 'delta': str(self.__delta), 'mindelta': str(mindelta)} - return result + self._info_dict.update({'value': str(eval_result)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Class representing a single "se_special" action @@ -1227,13 +1079,19 @@ def update(self, value): # Complete action # state: state (item) to read from def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type + self._log_develop('Completing action {}', self._name) + self._abitem.set_variable('current.action_name', self._name) + self._abitem.set_variable('current.state_name', state.name) + self._action_type = action_type + self._state = state if isinstance(self.__value, list): item = self.__value[0].property.path else: item = self.__value.property.path self._scheduler_name = "{}_{}-SeSpecialDelayTimer".format(self.__special, item) _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._abitem.set_variable('current.action_name', '') + self._abitem.set_variable('current.state_name', '') return _issue # Write action to logger @@ -1244,6 +1102,10 @@ def write_to_logger(self): self._log_debug("value: {0}", self.__value) else: self._log_debug("Retrigger item: {0}", self.__value.property.path) + self._info_dict.update({'value': str(self.__value)}) + self._info_dict.update({'special': str(self.__special)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) # Really execute the action def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): @@ -1361,23 +1223,6 @@ def suspend_execute(self, state=None, current_condition=None, previous_condition self._log_debug("Updated variable 'item.suspend_remaining' to {0}", suspend_remaining) self._action_status = _issue - def get(self): - try: - value_result = self.__value.property.path - except Exception: - value_result = self.__value - if isinstance(value_result, list): - for i, val in enumerate(value_result): - try: - value_result[i] = val.property.path - except Exception: - pass - result = {'function': str(self._function), 'special': str(self.__special), - 'value': str(value_result), 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_add" action class SeActionAddItem(SeActionSetItem): @@ -1392,40 +1237,17 @@ def __repr__(self): return "SeAction Add {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): value = value if isinstance(value, list) else [value] self._log_debug("{0}: Add '{1}' to '{2}'.{3}", actionname, value, item.property.path, repeat_text) value = item.property.value + value - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) - def get(self): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_remove" action class SeActionRemoveFirstItem(SeActionSetItem): @@ -1440,10 +1262,10 @@ def __repr__(self): return "SeAction RemoveFirst {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1454,32 +1276,9 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove first entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) - def get(self): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_remove" action class SeActionRemoveLastItem(SeActionSetItem): @@ -1494,10 +1293,10 @@ def __repr__(self): return "SeAction RemoveLast {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1510,32 +1309,9 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove last entry '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) - def get(self): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result - # Class representing a single "se_removeall" action class SeActionRemoveAllItem(SeActionSetItem): @@ -1550,10 +1326,10 @@ def __repr__(self): return "SeAction RemoveAll {}".format(self._name) def write_to_logger(self): - SeActionSetItem.write_to_logger(self) SeActionBase.write_to_logger(self) + SeActionSetItem.write_to_logger(self) - def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: @@ -1564,28 +1340,5 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, except Exception as ex: self._log_warning("{0}: Remove all '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) - source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self.update_webif_actionstatus(state, self._name, 'True') item(currentvalue, caller=self._caller, source=source) - - def get(self): - try: - if self.__item is not None: - item = str(self.__item.property.path) - else: - item = None - except Exception: - item = None - try: - val = self.__value.get() - if val is not None: - value = str(val) - else: - value = None - except Exception: - value = None - result = {'function': str(self._function), 'item': item, - 'value': value, 'nextconditionset': self.nextconditionset.get(), 'conditionset': self.conditionset.get(), - 'previousconditionset': self.previousconditionset.get(), - 'previousstate_conditionset': self.previousstate_conditionset.get(), 'actionstatus': {}} - return result diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 5bae5e249..06a34d1cc 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -51,18 +51,6 @@ def __init__(self, abitem): def __repr__(self): return "SeActions, count {}".format(self.count()) - def dict_actions(self, action_type, state): - result = {} - for name in self.__actions: - self._abitem.initactionname = name - result.update({name: self.__actions[name].get()}) - try: - result[name].update({'actionstatus': self._abitem.webif_infos[state][action_type][name].get('actionstatus')}) - except Exception: - pass - self._abitem.initactionname = None - return result - def reset(self): self.__actions = {} diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index 737130c34..19e872d6f 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -59,6 +59,7 @@ def __init__(self, abitem, name: str): self.__triggeredbynegate = None self.__agenegate = None self.__error = None + self.__state = None self.__itemClass = Item def __repr__(self): @@ -228,6 +229,7 @@ def get(self): # state: state (item) to read from # abitem_object: Related SeItem instance for later determination of current age and current delay def complete(self, state, use): + self.__state = state # check if it is possible to complete this condition if self.__min.is_empty() and self.__max.is_empty() and self.__value.is_empty() \ and self.__agemin.is_empty() and self.__agemax.is_empty() \ @@ -368,50 +370,80 @@ def check(self, state): # Write condition to logger def write_to_logger(self): + def write_item(item_type, value): + item_list = [] + if value is not None: + if isinstance(value, list): + for i in value: + try: + itm = i.property.path + except Exception: + itm = i + item_list.append(itm) + self._log_info("{0}: {1} ({2})", item_type, self.__name, itm) + else: + try: + itm = value.property.path + except Exception: + itm = value + item_list.append(itm) + self._log_info("{0}: {1} ({2})", item_type, self.__name, itm) + item_list = StateEngineTools.flatten_list(item_list) + item_list = None if len(item_list) == 0 else str(item_list[0]) if len(item_list) == 1 else str(item_list) + return item_list + + def write_eval(eval_type, value): + eval_list = [] + if value is not None: + if isinstance(value, list): + for e in value: + name = StateEngineTools.get_eval_name(e) + eval_list.append(name) + self._log_info("{0}: {1}", eval_type, name) + else: + name = StateEngineTools.get_eval_name(value) + eval_list.append(name) + self._log_info("{0}: {1}", eval_type, name) + eval_list = StateEngineTools.flatten_list(eval_list) + eval_list = None if len(eval_list) == 0 else str(eval_list[0]) if len(eval_list) == 1 else str(eval_list) + return str(eval_list) + if self.__error is not None: self._log_warning("error: {0}", self.__error) - if self.__item is not None: - if isinstance(self.__item, list): - for i in self.__item: - 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.__eval, list): - for e in self.__eval: - self._log_info("eval: {0}", StateEngineTools.get_eval_name(e)) - else: - self._log_info("eval: {0}", StateEngineTools.get_eval_name(self.__eval)) - if self.__status_eval is not None: - if isinstance(self.__status_eval, list): - for e in self.__status_eval: - self._log_info("status eval: {0}", StateEngineTools.get_eval_name(e)) - else: - self._log_info("status eval: {0}", StateEngineTools.get_eval_name(self.__status_eval)) - self.__value.write_to_logger() - self.__min.write_to_logger() - self.__max.write_to_logger() + item_result = self.__item + status_result = self.__status + item = write_item("item", item_result) + status = write_item("status item", status_result) + eval_result = self.__eval + status_eval_result = self.__status_eval + eval_result = write_eval("eval", eval_result) + status_eval_result = write_eval("status", status_eval_result) + val = self.__value.write_to_logger() + value_result = self.__value.get_for_webif(val) + min_result = self.__min.write_to_logger() + max_result = self.__max.write_to_logger() if self.__negate is not None: self._log_info("negate: {0}", self.__negate) - self.__agemin.write_to_logger() - self.__agemax.write_to_logger() + agemin = self.__agemin.write_to_logger() + agemax = self.__agemax.write_to_logger() if self.__agenegate is not None: self._log_info("age negate: {0}", self.__agenegate) - self.__changedby.write_to_logger() + changedby = self.__changedby.write_to_logger() if self.__changedbynegate is not None and not self.__changedby.is_empty(): self._log_info("changedby negate: {0}", self.__changedbynegate) - self.__updatedby.write_to_logger() + updatedby = self.__updatedby.write_to_logger() if self.__updatedbynegate is not None and not self.__updatedby.is_empty(): self._log_info("updatedby negate: {0}", self.__updatedbynegate) - self.__triggeredby.write_to_logger() + triggeredby = self.__triggeredby.write_to_logger() if self.__updatedbynegate is not None and not self.__triggeredby.is_empty(): self._log_info("triggeredby negate: {0}", self.__triggeredbynegate) + return {self.name: {'item': item, 'status': status, 'eval': eval_result, 'status_eval': status_eval_result, + 'value': value_result, 'min': str(min_result), 'max': str(max_result), 'agemin': str(agemin), + 'agemax': str(agemax), 'negate': str(self.__negate), 'agenegate': str(self.__agenegate), + 'changedby': str(changedby), 'updatedby': str(updatedby), + 'triggeredby': str(triggeredby), 'triggeredbynegate': str(self.__triggeredbynegate), + 'changedbynegate': str(self.__changedbynegate), + 'updatedbynegate': str(self.__updatedbynegate), 'current': {}, 'match': {}}} # Cast 'value', 'min' and 'max' using given cast function # cast_func: cast function to use diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index a89aeebdf..176b6a7c9 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -78,6 +78,7 @@ def __init__(self, abitem, name, conditionid): self.__evals_items = {} self.__unused_attributes = {} self.__used_attributes = {} + self.__state = None def __repr__(self): return "SeConditionSet {}".format(self.__conditions) @@ -150,6 +151,7 @@ def update(self, item, grandparent_item): # Check the condition set, optimize and complete it # state: state (item) to read from def complete(self, state, use): + self.__state = state conditions_to_remove = [] # try to complete conditions @@ -174,7 +176,9 @@ def write_to_logger(self): for name in self.__conditions: self._log_info("Condition '{0}':", name) self._log_increase_indent() - self.__conditions[name].write_to_logger() + _webif = self.__conditions[name].write_to_logger() + _key = [self.__state.id, 'conditionsets', self.name] + self._abitem.update_webif(_key, _webif, True) self._log_decrease_indent() def __currentconditionset_set(self, conditionsetid, name): diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 57da47a73..895eeb46a 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -480,6 +480,15 @@ def remove_all_schedulers(self): # region Updatestate *********************************************************************************************** # run queue def run_queue(self): + def update_current_to_empty(d): + if isinstance(d, dict): # Check if the current level is a dictionary + for key, val in d.items(): + if key in ['current', 'match', 'actionstatus'] and isinstance(val, dict): # If the key is 'current', and value is a dict + d[key] = {} # Set it to an empty dict + else: + # Recur for nested dictionaries + update_current_to_empty(val) + if not self.__ab_alive: self.__logger.debug("{} not running (anymore). Queue not activated.", StateEngineDefaults.plugin_identification) @@ -638,6 +647,9 @@ def run_queue(self): evaluated_instant_leaveaction = False _previousstate_conditionset_id = '' _previousstate_conditionset_name = '' + + update_current_to_empty(self.__webif_infos) + self.__logger.develop("Resetted current info for webif info. It is now: {}", self.__webif_infos) for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -680,7 +692,7 @@ def run_queue(self): self.set_variable('next.conditionset_name', self.__nextconditionset_name) self.__logger.info("No matching state found, no previous state available. Doing nothing.") else: - if last_state.conditions.count() == 0: + if last_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' @@ -745,7 +757,7 @@ def run_queue(self): _last_conditionset_id = self.__lastconditionset_internal_id _last_conditionset_name = self.__lastconditionset_internal_name - if new_state.conditions.count() == 0: + if new_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' @@ -785,7 +797,7 @@ def run_queue(self): _original_conditionset_name, self.__nextconditionset_id, self.__nextconditionset_name) last_state.run_leave(self.__repeat_actions.get()) _leaveactions_run = True - if new_state.conditions.count() == 0: + if new_state.conditionsets.count() == 0: self.lastconditionset_set('', '') _last_conditionset_id = '' _last_conditionset_name = '' @@ -995,16 +1007,26 @@ def update_can_release_list(): self.__logger.info("".ljust(80, "_")) return all_released_by - def update_webif(self, key, value): + def update_webif(self, key, value, update=False): def _nested_set(dic, keys, val): for nestedkey in keys[:-1]: dic = dic.setdefault(nestedkey, {}) - dic[keys[-1]] = val + # Check if both existing value and new value are dictionaries + if update is True and isinstance(dic.get(keys[-1]), dict) and isinstance(val, dict): + # Update the existing dictionary with the new dictionary + dic[keys[-1]].update(val) + self.__logger.develop("Updating WEBIF with list {}, value: {}. infos is {}", key, value, + self.__webif_infos) + else: + # Otherwise, set the value as is + dic[keys[-1]] = val + self.__logger.develop("Setting WEBIF with list {}, value: {}. infos is {}", key, value, + self.__webif_infos) def _nested_test(dic, keys): for nestedkey in keys[:-2]: dic = dic.setdefault(nestedkey, {}) - return dic[keys[-2]] + return dic.get(keys[-2], {}) if isinstance(key, list): try: @@ -1014,21 +1036,26 @@ def _nested_test(dic, keys): except Exception: return False else: - self.__webif_infos[key] = value + if update is True: + self.__webif_infos.setdefault(key, {}).update(value) + self.__logger.develop("Updating WEBIF {}, value: {}. infos is {}", key, value, self.__webif_infos) + else: + self.__webif_infos[key] = value + self.__logger.develop("Setting WEBIF {}, value: {}. infos is {}", key, value, self.__webif_infos) return True def update_action_status(self, action_status): def combine_dicts(dict1, dict2): - combined_dict = dict1.copy() + combined = dict1.copy() for key, value in dict2.items(): - if key in combined_dict: - for k, v in combined_dict.items(): + if key in combined: + for k, v in combined.items(): v['issueorigin'].extend( - [item for item in v['issueorigin'] if item not in combined_dict[k]['issueorigin']]) - v['issue'].extend([item for item in v['issue'] if item not in combined_dict[k]['issue']]) + [item for item in v['issueorigin'] if item not in combined[k]['issueorigin']]) + v['issue'].extend([item for item in v['issue'] if item not in combined[k]['issue']]) else: - combined_dict[key] = value - return combined_dict + combined[key] = value + return combined combined_dict = combine_dicts(action_status, self.__action_status) self.__action_status = combined_dict @@ -1120,6 +1147,7 @@ def print_readable_dict(data): self.__logger.info("- {}: {}", key, ', '.join(formatted_entries)) else: self.__logger.info("- {}: {}", key, value) + def list_issues(v): _issuelist = StateEngineTools.flatten_list(v.get('issue')) if isinstance(_issuelist, list) and len(_issuelist) > 1: diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 5d241a940..b852779d9 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -60,6 +60,18 @@ def name(self): def actions_leave(self): return self.__actions_leave + @property + def actions_enter(self): + return self.__actions_enter + + @property + def actions_enter_or_stay(self): + return self.__actions_enter_or_stay + + @property + def actions_stay(self): + return self.__actions_stay + # Return text of state @property def text(self): @@ -67,8 +79,8 @@ def text(self): # Return conditions @property - def conditions(self): - return self.__conditions + def conditionsets(self): + return self.__conditionsets # Return orphaned definitions @property @@ -168,7 +180,7 @@ def __init__(self, abitem, item_state): self.__use_done = [] self.__use_list = [] self.__use_ignore_list = [] - self.__conditions = StateEngineConditionSets.SeConditionSets(self._abitem) + self.__conditionsets = StateEngineConditionSets.SeConditionSets(self._abitem) self.__actions_enter_or_stay = StateEngineActions.SeActions(self._abitem) self.__actions_enter = StateEngineActions.SeActions(self._abitem) self.__actions_stay = StateEngineActions.SeActions(self._abitem) @@ -193,7 +205,7 @@ def can_enter(self): self.__is_copy_for.write_to_logger() self.__releasedby.write_to_logger() self.__can_release.write_to_logger() - result, conditionset = self.__conditions.one_conditionset_matching(self) + result, conditionset = self.__conditionsets.one_conditionset_matching(self) self._log_decrease_indent() if result: self._log_info("State {} can be entered based on conditionset {}", self.id, conditionset) @@ -219,7 +231,7 @@ def write_to_log(self): self._log_info("Updating Web Interface...") self._log_increase_indent() self._abitem.update_webif(self.id, {'name': self.name, - 'conditionsets': self.__conditions.get(), + 'conditionsets': {}, 'actions_enter': {}, 'actions_enter_or_stay': {}, 'actions_stay': {}, @@ -229,10 +241,10 @@ def write_to_log(self): self._log_decrease_indent() self._log_info("Finished Web Interface Update") - if self.__conditions.count() > 0: + if self.__conditionsets.count() > 0: self._log_info("Condition sets to enter state:") self._log_increase_indent() - self.__conditions.write_to_logger() + self.__conditionsets.write_to_logger() self._log_decrease_indent() if self.__actions_enter.count() > 0: @@ -240,28 +252,24 @@ def write_to_log(self): self._log_increase_indent() self.__actions_enter.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions('actions_enter', self.id)) if self.__actions_stay.count() > 0: self._log_info("Actions to perform on stay:") self._log_increase_indent() self.__actions_stay.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions('actions_stay', self.id)) if self.__actions_enter_or_stay.count() > 0: self._log_info("Actions to perform on enter or stay:") self._log_increase_indent() self.__actions_enter_or_stay.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) if self.__actions_leave.count() > 0: self._log_info("Actions to perform on leave (instant leave: {})", self._abitem.instant_leaveaction) self._log_increase_indent() self.__actions_leave.write_to_logger() self._log_decrease_indent() - self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions('actions_leave', self.id)) self._abitem.set_variable("current.state_name", "") self._abitem.set_variable("current.state_id", "") self._log_decrease_indent() @@ -296,15 +304,6 @@ def run_enter(self, allow_item_repeat: bool): self._abitem.update_webif(_key_enter, True) self.__actions_enter.execute(False, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) - self._log_increase_indent() - self._log_debug("Update web interface enter {}", self.id) - self._log_increase_indent() - if self.__actions_enter_or_stay.count() > 0: - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) - if self.__actions_enter.count() > 0: - self._abitem.update_webif([self.id, 'actions_enter'], self.__actions_enter.dict_actions('actions_enter', self.id)) - self._log_decrease_indent() - self._log_decrease_indent() # run actions when staying at the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -318,15 +317,16 @@ def run_stay(self, allow_item_repeat: bool): self._abitem.update_webif(_key_enter, False) self.__actions_stay.execute(True, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) + + # run actions when passing the state + # item_allow_repeat: Is repeating actions generally allowed for the item? + def run_pass(self, allow_item_repeat: bool): self._log_increase_indent() - self._log_debug("Update web interface stay {}", self.id) - self._log_increase_indent() - if self.__actions_enter_or_stay.count() > 0: - self._abitem.update_webif([self.id, 'actions_enter_or_stay'], self.__actions_enter_or_stay.dict_actions('actions_enter_or_stay', self.id)) - if self.__actions_stay.count() > 0: - self._abitem.update_webif([self.id, 'actions_stay'], self.__actions_stay.dict_actions('actions_stay', self.id)) - self._log_decrease_indent() - self._log_decrease_indent() + for elem in self._abitem.webif_infos: + _key_pass = ['{}'.format(elem), 'pass'] + self._abitem.update_webif(_key_pass, False) + self.__actions_pass.execute(False, allow_item_repeat, self) + self._log_decrease_indent(50) # run actions when leaving the state # item_allow_repeat: Is repeating actions generally allowed for the item? @@ -337,13 +337,6 @@ def run_leave(self, allow_item_repeat: bool): self._abitem.update_webif(_key_leave, False) self.__actions_leave.execute(False, allow_item_repeat, self) self._log_decrease_indent(50) - self._log_increase_indent() - self._log_debug("Update web interface leave {}", self.id) - self._log_increase_indent() - if self.__actions_leave.count() > 0: - self._abitem.update_webif([self.id, 'actions_leave'], self.__actions_leave.dict_actions('actions_leave', self.id)) - self._log_decrease_indent() - self._log_decrease_indent() def refill(self): cond_refill = not self.__use.is_empty() and ("eval" in self.__use.get_type() or "item" in self.__use.get_type()) @@ -405,7 +398,6 @@ def __fill_list(self, item_states, recursion_depth, se_use=None, use=None): self.__use_done.append(element) self.__fill(element, recursion_depth, se_use, use) - def __initialize_se_use(self, state, recursion_depth): # Import data from other item if attribute "use" is found if isinstance(state, SeState): @@ -611,7 +603,7 @@ def update_action_status(action_status, actiontype): self._log_develop("Fill state {} type {}, called by {}, recursion {}", item_state, type(item_state), se_use, recursion_depth) if se_use == "reinit": self._log_develop("Resetting conditions and actions at re-init use is {}", use) - self.__conditions.reset() + self.__conditionsets.reset() self.__actions_enter_or_stay.reset() self.__actions_enter.reset() self.__actions_stay.reset() @@ -642,7 +634,7 @@ def update_action_status(action_status, actiontype): try: if child_name == "enter" or child_name.startswith("enter_"): _conditioncount += 1 - _unused_attributes, _used_attributes = self.__conditions.update(child_name, child_item, parent_item) + _unused_attributes, _used_attributes = self.__conditionsets.update(child_name, child_item, parent_item) self.__unused_attributes = copy(_unused_attributes) self.__used_attributes = copy(_used_attributes) for item in self.__unused_attributes.keys(): @@ -707,16 +699,16 @@ def update_action_status(action_status, actiontype): # Complete condition sets and actions at the end if recursion_depth == 0: - self.__conditions.complete(self, use) - _action_status = self.__actions_enter.complete(self, 'actions_enter', self.__conditions.evals_items, use) + self.__conditionsets.complete(self, use) + _action_status = self.__actions_enter.complete(self, 'actions_enter', self.__conditionsets.evals_items, use) if _action_status: update_action_status(_action_status, 'enter') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(self,'actions_stay', self.__conditions.evals_items, use) + _action_status = self.__actions_stay.complete(self, 'actions_stay', self.__conditionsets.evals_items, use) if _action_status: update_action_status(_action_status, 'stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(self, 'actions_enter_or_stay', self.__conditions.evals_items, use) + _action_status = self.__actions_enter_or_stay.complete(self, 'actions_enter_or_stay', self.__conditionsets.evals_items, use) if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index b5aefdf75..84aedafdd 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -447,10 +447,14 @@ def get(self, default=None, originalorder=True): else: return returnvalues - def get_for_webif(self): - returnvalues = self.get() + def get_for_webif(self, value=None): + if value is None: + returnvalues = self.get() + else: + returnvalues = value returnvalues = self.__varname if returnvalues == '' else returnvalues - return str(returnvalues) + returnvalues = str(returnvalues) + return returnvalues def get_type(self): if len(self.__listorder) <= 1: @@ -480,6 +484,7 @@ def write_to_logger(self): self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) + return self.__value if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -487,6 +492,7 @@ def write_to_logger(self): self._log_debug("{0} from regex: {1}", self.__name, i) else: self._log_debug("{0} from regex: {1}", self.__name, self.__regex) + return f"regex:{self.__regex}" if self.__struct is not None: if isinstance(self.__struct, list): for i in self.__struct: @@ -495,23 +501,30 @@ def write_to_logger(self): else: self._log_debug("{0} from struct: {1}", self.__name, self.__struct.property.path) + return self.__struct if self.__item is not None: _original_listorder = self.__listorder.copy() + items = [] if isinstance(self.__item, list): for i, item in enumerate(self.__item): if item is not None: self._log_debug("{0} from item: {1}", self.__name, item.property.path) - self._log_debug("Currently item results in {}", self.__get_from_item()[i]) + current = self.__get_from_item()[i] + items.append(current) + self._log_debug("Currently item results in {}", current) else: self._log_debug("{0} from item: {1}", self.__name, self.__item.property.path) - self._log_debug("Currently item results in {}", self.__get_from_item()) + items = self.__get_from_item() + self._log_debug("Currently item results in {}", items) self.__listorder = _original_listorder + return items if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) _original_listorder = self.__listorder.copy() eval_result = self.__get_eval() self._log_debug("Currently eval results in {}. ", eval_result) self.__listorder = _original_listorder + return eval_result if self.__varname is not None: if isinstance(self.__varname, list): for i in self.__varname: @@ -519,7 +532,8 @@ def write_to_logger(self): self._log_debug("{0} from variable: {1}", self.__name, i) else: self._log_debug("{0} from variable: {1}", self.__name, self.__varname) - return eval_result + return self.__get_from_variable() + return None # Get Text (similar to logger text) # prefix: Prefix for text From dfc14fb488fd699da7a624ab9d16c4beef0b5644 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 14:53:27 +0200 Subject: [PATCH 074/121] stateengine plugin: introduce actions_pass, first step --- stateengine/StateEngineState.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index b852779d9..d047f12fc 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -72,6 +72,10 @@ def actions_enter_or_stay(self): def actions_stay(self): return self.__actions_stay + @property + def actions_pass(self): + return self.__actions_pass + # Return text of state @property def text(self): @@ -185,6 +189,7 @@ def __init__(self, abitem, item_state): self.__actions_enter = StateEngineActions.SeActions(self._abitem) self.__actions_stay = StateEngineActions.SeActions(self._abitem) self.__actions_leave = StateEngineActions.SeActions(self._abitem) + self.__actions_pass = StateEngineActions.SeActions(self._abitem) self.__order = StateEngineValue.SeValue(self._abitem, "State Order", False, "num") self._log_increase_indent() try: @@ -236,6 +241,7 @@ def write_to_log(self): 'actions_enter_or_stay': {}, 'actions_stay': {}, 'actions_leave': {}, + 'actions_pass': {}, 'leave': False, 'enter': False, 'stay': False, 'is_copy_for': None, 'releasedby': None}) self._log_decrease_indent() @@ -270,6 +276,13 @@ def write_to_log(self): self._log_increase_indent() self.__actions_leave.write_to_logger() self._log_decrease_indent() + + if self.__actions_pass.count() > 0: + self._log_info("Actions to perform on pass / transit:") + self._log_increase_indent() + self.__actions_pass.write_to_logger() + self._log_decrease_indent() + self._abitem.set_variable("current.state_name", "") self._abitem.set_variable("current.state_id", "") self._log_decrease_indent() @@ -608,6 +621,7 @@ def update_action_status(action_status, actiontype): self.__actions_enter.reset() self.__actions_stay.reset() self.__actions_leave.reset() + self.__actions_pass.reset() self.__use_done = [] use = self.__use.get() @@ -624,7 +638,7 @@ def update_action_status(action_status, actiontype): parent_item = None child_items = item_state.return_children() _conditioncount = 0 - _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0} + _action_counts = {"enter": 0, "stay": 0, "enter_or_stay": 0, "leave": 0, "pass": 0} _unused_attributes = {} _used_attributes = {} _action_status = {} @@ -666,7 +680,8 @@ def update_action_status(action_status, actiontype): "on_enter": ("enter", self.__actions_enter), "on_stay": ("stay", self.__actions_stay), "on_enter_or_stay": ("enter_or_stay", self.__actions_enter_or_stay), - "on_leave": ("leave", self.__actions_leave) + "on_leave": ("leave", self.__actions_leave), + "on_pass": ("pass", self.__actions_pass) } if child_name in action_mapping: @@ -712,22 +727,26 @@ def update_action_status(action_status, actiontype): if _action_status: update_action_status(_action_status, 'enter_or_stay') self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(self, 'actions_leave',self.__conditions.evals_items, use) + _action_status = self.__actions_pass.complete(self, 'actions_pass', self.__conditionsets.evals_items, use) + if _action_status: + update_action_status(_action_status, 'pass') + self._abitem.update_action_status(self.__action_status) + _action_status = self.__actions_leave.complete(self, 'actions_leave', self.__conditionsets.evals_items, use) if _action_status: update_action_status(_action_status, 'leave') self._abitem.update_action_status(self.__action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) - _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave" + _summary = "{} on_enter, {} on_stay , {} on_enter_or_stay, {} on_leave, {} on_pass" if self.__action_status: _ignore_list = [entry for entry in self.__action_status if self.__action_status[entry].get('ignore') is True] if _ignore_list: self._log_info("Ignored {} action(s) due to errors: {}", len(_ignore_list), _ignore_list) if se_use is not None: self._log_debug("Added {} action(s) based on se_use {}. " + _summary, _total_actioncount, se_use, - _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"], _action_counts["pass"]) self._log_debug("Added {} condition set(s) based on se_use: {}", _conditioncount, se_use) else: self._log_debug("Added {} action(s) based on item configuration: " + _summary, _total_actioncount, - _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"]) + _action_counts["enter"], _action_counts["stay"], _action_counts["enter_or_stay"], _action_counts["leave"], _action_counts["pass"]) self._log_debug("Added {} condition set(s) based on item configuration", _conditioncount) From a4576e87977fcf88da928b979ee02dedd7a04478 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 15:23:08 +0200 Subject: [PATCH 075/121] stateengine plugin: run pass actions --- stateengine/StateEngineItem.py | 5 ++++- stateengine/StateEngineState.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 895eeb46a..4d71e26e0 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1429,7 +1429,10 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): self.__logger.develop("Current variables: {}", self.__variables) if refill: state.refill() - return state.can_enter() + can_enter = state.can_enter() + if can_enter is False: + state.run_pass() + return can_enter except Exception as ex: self.__logger.warning("Problem with currentstate {0}. Error: {1}", state.id, ex) # The variables where originally reset in a finally: statement. No idea why... ;) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index d047f12fc..d96ed24b8 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -334,6 +334,7 @@ def run_stay(self, allow_item_repeat: bool): # run actions when passing the state # item_allow_repeat: Is repeating actions generally allowed for the item? def run_pass(self, allow_item_repeat: bool): + self._log_info("Passing state {}, running pass actions.", self.id) self._log_increase_indent() for elem in self._abitem.webif_infos: _key_pass = ['{}'.format(elem), 'pass'] From 6db656b8d857d3a6c1498bf98687059d929c934b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 15:40:47 +0200 Subject: [PATCH 076/121] smartvisu plugin: minor code adjustments --- smartvisu/svgenerator.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/smartvisu/svgenerator.py b/smartvisu/svgenerator.py index e4852f887..d55f6f669 100755 --- a/smartvisu/svgenerator.py +++ b/smartvisu/svgenerator.py @@ -25,13 +25,13 @@ from lib.item import Items + class SmartVisuGenerator: valid_sv_page_entries = ['room', 'overview', 'separator', 'seperator', - 'category', 'cat_overview', 'cat_separator','cat_seperator', + 'category', 'cat_overview', 'cat_separator', 'cat_seperator', 'room_lite', 'sv_overview'] - def __init__(self, plugin_instance, visu_definition=None): self.items = Items.get_instance() @@ -102,7 +102,6 @@ def initialize_visu_menu(self, nav_config, menu): self.add_menuentry_to_list(menu, menu_entry) self.logger.debug("initialize_visu_menu: '{}' menu_entry={}".format(menu, menu_entry)) - def handle_heading_attributes(self, room): if 'sv_heading_right' in room.conf: heading_right = room.conf['sv_heading_right'] @@ -122,7 +121,6 @@ def handle_heading_attributes(self, room): heading = '' return heading - def get_widgetblocksize(self, item): """ Returns the blocksize for the block in which the item is to be displayed. @@ -134,13 +132,12 @@ def get_widgetblocksize(self, item): """ if 'sv_blocksize' in item.conf: blocksize = item.conf['sv_blocksize'] - if not blocksize in ['1','2','3']: + if blocksize not in ['1', '2', '3']: blocksize = '2' else: blocksize = '2' return blocksize - def get_attribute(self, attr, item): if attr in item.conf: attrvalue = item.conf[attr] @@ -148,7 +145,6 @@ def get_attribute(self, attr, item): attrvalue = '' return attrvalue - def create_page(self, room, menu_entry): """ Interpretation of the room-specific item-attributes. @@ -159,7 +155,7 @@ def create_page(self, room, menu_entry): :return: html code to be included in the visu file for the room :rtype: str """ - block_style = 'std' # 'std' or 'noh' + block_style = 'std' # 'std' or 'noh' widgetblocktemplate = 'widgetblock_' + self.visu_style + '_' + block_style + '.html' widgetblocktemplate2 = 'widgetblock2_' + self.visu_style + '_' + block_style + '.html' widgets = '' @@ -215,7 +211,6 @@ def create_page(self, room, menu_entry): menu_entry['content'] += widgets return r - def pages(self): if not self.remove_oldpages(): return @@ -284,13 +279,13 @@ def pages(self): def create_menuentry(self, menu, entry_name, item_path, separator, img_name, nav_aside, nav_aside2, from_navconfig=False): for menu_entry in self.navigation[menu]: if menu_entry['name'] == entry_name: - if menu_entry.get('img', '') == '' and menu_entry.get('img_set', False) == False: + if menu_entry.get('img', '') == '' and menu_entry.get('img_set', False) is False: menu_entry['img'] = img_name if menu_entry['item_path'] == '': menu_entry['item_path'] = item_path - if menu_entry.get('nav_aside', '') == ''and menu_entry.get('nav_aside_set', False) == False: + if menu_entry.get('nav_aside', '') == '' and menu_entry.get('nav_aside_set', False) is False: menu_entry['nav_aside'] = nav_aside - if menu_entry.get('nav_aside2', '') == '' and menu_entry.get('nav_aside2_set', False) == False: + if menu_entry.get('nav_aside2', '') == '' and menu_entry.get('nav_aside2_set', False) is False: menu_entry['nav_aside2'] = nav_aside2 return menu_entry @@ -299,7 +294,7 @@ def create_menuentry(self, menu, entry_name, item_path, separator, img_name, nav menu_entry['item_path'] = item_path menu_entry['separator'] = separator menu_entry['page'] = menu + '.' + entry_name - for ch in [' ',':','/','\\']: + for ch in [' ', ':', '/', '\\']: if ch in menu_entry['page']: menu_entry['page'] = menu_entry['page'].replace(ch, '_') menu_entry['heading'] = '' @@ -329,7 +324,6 @@ def create_menuentry(self, menu, entry_name, item_path, separator, img_name, nav return menu_entry - def add_menuentry_to_list(self, menu, menu_entry): for entry in self.navigation[menu]: if entry['name'] == menu_entry['name']: @@ -358,7 +352,7 @@ def write_navigation_and_pages(self, menu, navigation_file): ('{{ visu_aside2 }}', menu_entry['nav_aside2']), ('item.name', menu_entry['name']), ("'item", "'" + menu_entry['item_path']) ] - if menu_entry['separator'] == True: + if menu_entry['separator'] is True: nav_list += self.parse_tpl('navi_sep.html', [('{{ name }}', menu_entry['name'])]) else: #menu_entry['html'] = self.parse_tpl('navi.html', parse_list) @@ -371,7 +365,7 @@ def write_navigation_and_pages(self, menu, navigation_file): nav_list += self.parse_tpl('navi.html', parse_list) # build page code - if menu_entry['separator'] == False: + if menu_entry['separator'] is False: # build and write file for a single room r = self.parse_tpl(menu+'_page.html', [('{{ visu_name }}', menu_entry['name']), ('{{ visu_widgets }}', menu_entry['content']), ('{{ visu_img }}', menu_entry['img']), ('{{ visu_heading }}', menu_entry['heading'])]) @@ -420,7 +414,6 @@ def parse_tpl(self, template, replace): tpl = tpl.replace(s, rs) return tpl - def write_parseresult(self, htmlfile, parseresult): try: with open(self.pages_dir + '/' + htmlfile, 'w') as f: @@ -428,7 +421,6 @@ def write_parseresult(self, htmlfile, parseresult): except Exception as e: self.logger.warning("Could not write to {0}/{1}: {2}".format(self.pages_dir, htmlfile, e)) - def copy_tpl(self, tplname, destname=''): if destname == '': destname = tplname @@ -487,7 +479,7 @@ def copy_templates(self): if self.smartvisu_version >= '2.9': for fn in os.listdir(self.shng_tpldir): - if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.gen_tpldir, fn)) ): + if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.gen_tpldir, fn))): self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{} ({})".format(fn, self.smartvisu_version, self.gen_tpldir)) shutil.copy2(os.path.join(self.shng_tpldir, fn), self.gen_tpldir) shutil.copy2(os.path.join(self.sv_tpldir, 'index.html'), self.gen_tpldir) @@ -503,10 +495,10 @@ def copy_templates(self): pass # Open file for twig import statements (for root.html) for fn in os.listdir(self.shng_tpldir): - if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.gen_tpldir, fn)) ): + if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.gen_tpldir, fn))): self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{}".format(fn, self.smartvisu_version)) try: - shutil.copy2( os.path.join(self.shng_tpldir, fn), self.gen_tpldir ) + shutil.copy2(os.path.join(self.shng_tpldir, fn), self.gen_tpldir) except Exception as e: self.logger.error("Could not copy {0} from {1} to {2}".format(fn, self.shng_tpldir, self.gen_tpldir)) return From c10b45d4d6e09e53fb3f41640fe777340194f7ce Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 15:41:22 +0200 Subject: [PATCH 077/121] smartvisu plugin: important fix in logger message when visu style unknown --- smartvisu/svgenerator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartvisu/svgenerator.py b/smartvisu/svgenerator.py index d55f6f669..b2fb6cdc9 100755 --- a/smartvisu/svgenerator.py +++ b/smartvisu/svgenerator.py @@ -43,9 +43,9 @@ def __init__(self, plugin_instance, visu_definition=None): self.smartvisu_version = plugin_instance.smartvisu_version self.overwrite_templates = plugin_instance.overwrite_templates self.visu_style = plugin_instance.visu_style.lower() - if not self.visu_style in ['std','blk']: + if self.visu_style not in ['std', 'blk']: self.visu_style = 'std' - self.logger.warning("SmartVisuGenerator: visu_style '{}' unknown, using visu_style '{1}'".format(plugin_instance.visu_style, self.visu_style)) + self.logger.warning("SmartVisuGenerator: visu_style '{0}' unknown, using visu_style '{1}'".format(plugin_instance.visu_style, self.visu_style)) self.list_deprecated_warnings = plugin_instance.list_deprecated_warnings self.logger.info("Generating pages for smartVISU v{}".format(self.smartvisu_version)) From 6676d3d24dbbd17e37fbb695b93cf1284713e8df Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 15:43:44 +0200 Subject: [PATCH 078/121] visu_smartvisu plugin: minor code adjustments --- visu_smartvisu/__init__.py | 92 ++++++++++++++------------------------ 1 file changed, 34 insertions(+), 58 deletions(-) diff --git a/visu_smartvisu/__init__.py b/visu_smartvisu/__init__.py index 504d35b83..8eeac6012 100755 --- a/visu_smartvisu/__init__.py +++ b/visu_smartvisu/__init__.py @@ -41,10 +41,9 @@ ######################################################################### class SmartVisu(SmartPlugin): - PLUGIN_VERSION="1.3.4" + PLUGIN_VERSION = "1.3.4" ALLOW_MULTIINSTANCE = True - def my_to_bool(self, value, attr='', default=False): try: result = self.to_bool(value) @@ -53,15 +52,14 @@ def my_to_bool(self, value, attr='', default=False): self.logger.error("smartVISU: Invalid value '"+str(value)+"' configured for attribute "+attr+" in plugin.conf, using '"+str(result)+"' instead") return result - - def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite_templates='Yes', visu_style = 'std', handle_widgets='True' ): + def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite_templates=True, visu_style='std', handle_widgets='True'): self.logger = logging.getLogger(__name__) self._sh = smarthome self.smartvisu_dir = str(smartvisu_dir) self._generate_pages = self.my_to_bool(generate_pages, 'generate_pages', True) self.overwrite_templates = self.my_to_bool(overwrite_templates, 'overwrite_templates', True) - if visu_style.lower() in ['std','blk']: + if visu_style.lower() in ['std', 'blk']: self.visu_style = visu_style.lower() else: self.visu_style = 'std' @@ -74,7 +72,6 @@ def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite # else: # self.logger.log(logging.WARNING, "Handling for smartVISU v{} in directory {}".format(self.smartvisu_version, self.smartvisu_dir)) - def run(self): self.alive = True if self.smartvisu_dir != '': @@ -97,11 +94,9 @@ def run(self): self.logger.exception("SmartVisuGenerator: Exception: {}".format(e)) self.logger.info("Finished smartVISU v{} handling".format(self.smartvisu_version)) - def stop(self): self.alive = False - def parse_item(self, item): # Relative path support (release 1.3 and up) item.expand_relativepathes('sv_widget', "'", "'") @@ -109,15 +104,12 @@ def parse_item(self, item): item.expand_relativepathes('sv_nav_aside', "'", "'") item.expand_relativepathes('sv_nav_aside2', "'", "'") - def parse_logic(self, logic): pass - def update_item(self, item, caller=None, source=None, dest=None): pass - def get_smartvisu_version(self): """ Determine which smartVISU version is installed in 'smartvisu_dir' @@ -147,7 +139,7 @@ def get_smartvisu_version(self): class SmartVisuGenerator: - def __init__(self, smarthome, smartvisu_dir='', overwrite_templates='Yes', visu_style='std', smartvisu_version=''): + def __init__(self, smarthome, smartvisu_dir='', overwrite_templates=True, visu_style='std', smartvisu_version=''): self.logger = logging.getLogger(__name__) self._sh = smarthome self.items = Items.get_instance() @@ -174,7 +166,6 @@ def __init__(self, smarthome, smartvisu_dir='', overwrite_templates='Yes', visu_ self.pages() self.logger.info("Generating pages for smartVISU v{} End".format(self.smartvisu_version)) - def handle_heading_attributes(self, room): if 'sv_heading_right' in room.conf: heading_right = room.conf['sv_heading_right'] @@ -194,7 +185,6 @@ def handle_heading_attributes(self, room): heading = '' return heading - def get_widgetblocksize(self, item): """ Returns the blocksize for the block in which the item is to be displayed. @@ -206,13 +196,12 @@ def get_widgetblocksize(self, item): """ if 'sv_blocksize' in item.conf: blocksize = item.conf['sv_blocksize'] - if not blocksize in ['1','2','3']: + if blocksize not in ['1', '2', '3']: blocksize = '2' else: blocksize = '2' return blocksize - def get_attribute(self, attr, item): if attr in item.conf: attrvalue = item.conf[attr] @@ -220,14 +209,12 @@ def get_attribute(self, attr, item): attrvalue = '' return attrvalue - def room(self, room): """ Interpretation of the room-specific item-attributes. This routine is called once per 'sv_page'. :param room: Items (with room configuration) - :param tpldir: Directory where the template files are stored (within smartVISU) :return: html code to be included in the visu file for the room :rtype: str @@ -292,7 +279,6 @@ def room(self, room): r = self.parse_tpl('roomlite.html', [('{{ visu_name }}', str(room)), ('{{ visu_widgets }}', widgets), ('{{ visu_img }}', rimg), ('{{ visu_heading }}', heading)]) return r - def pages(self): if not self.remove_oldpages(): return @@ -341,7 +327,6 @@ def pages(self): lite_lis += self.parse_tpl('navi.html', [('{{ visu_page }}', item.property.path), ('{{ visu_name }}', str(item)), ('{{ visu_img }}', img), ('{{ visu_aside }}', nav_aside), ('{{ visu_aside2 }}', nav_aside2), ('item.name', str(item)), ("'item", "'" + item.property.path)]) self.write_parseresult(item.property.path+'.html', r) - nav = self.parse_tpl('navigation.html', [('{{ visu_navis }}', nav_lis)]) self.write_parseresult('room_nav.html', nav) @@ -351,7 +336,6 @@ def pages(self): nav = self.parse_tpl('navigation.html', [('{{ visu_navis }}', lite_lis)]) self.write_parseresult('roomlite_nav.html', nav) - self.copy_tpl('rooms.html') self.copy_tpl('roomslite.html') self.copy_tpl('category.html') @@ -373,7 +357,6 @@ def parse_tpl(self, template, replace): tpl = tpl.replace(s, r) return tpl - def write_parseresult(self, htmlfile, parseresult): try: with open(self.outdir + '/' + htmlfile, 'w') as f: @@ -435,9 +418,9 @@ def copy_templates(self): if self.smartvisu_version == '2.9': for fn in os.listdir(srcdir): - if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn)) ): + if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn))): self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{}".format(fn, self.smartvisu_version)) - shutil.copy2( os.path.join(srcdir, fn), self.tpldir ) + shutil.copy2(os.path.join(srcdir, fn), self.tpldir) else: # create output directory @@ -450,10 +433,10 @@ def copy_templates(self): # Open file for twig import statements (for root.html) for fn in os.listdir(srcdir): - if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn)) ): + if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn))): self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{}".format(fn, self.smartvisu_version)) try: - shutil.copy2( os.path.join(srcdir, fn), self.tpldir ) + shutil.copy2(os.path.join(srcdir, fn), self.tpldir) except Exception as e: self.logger.error("Could not copy {0} from {1} to {2}".format(fn, srcdir, self.tpldir)) return @@ -487,17 +470,16 @@ def __init__(self, smarthome, smartvisu_dir='', smartvisu_version=''): self.install_widgets(self._sh) - def install_widgets(self, smarthome): if not self.remove_oldfiles(): return if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8': # make a backup copy of root.html if it doesn't exist (for full integeration) - if not os.path.isfile( self.pgbdir + '/root_master.html' ): - self.logger.warning( "install_widgets: Creating a copy of root.html" ) + if not os.path.isfile(self.pgbdir + '/root_master.html'): + self.logger.warning("install_widgets: Creating a copy of root.html") try: - shutil.copy2( self.pgbdir + '/root.html', self.pgbdir + '/root_master.html' ) + shutil.copy2(self.pgbdir + '/root.html', self.pgbdir + '/root_master.html') except Exception as e: self.logger.error("Could not copy {} from {} to {}".format('root.html', self.pgbdir, self.pgbdir + '/root_master.html')) return @@ -506,17 +488,17 @@ def install_widgets(self, smarthome): f_root = open(self.pgbdir + '/root_master.html', "r") root_contents = f_root.readlines() f_root.close() - self.logger.debug( "root_contents: {0}".format(root_contents) ) + self.logger.debug("root_contents: {0}".format(root_contents)) # find insert points in original root.html - iln_html = self.findinsertline( root_contents, '{% import "plot.html" as plot %}' ) - iln_js = self.findinsertline( root_contents, "{% if isfile('pages/'~config_pages~'/visu.js') %}" ) - iln_css = self.findinsertline( root_contents, "{% if isfile('pages/'~config_pages~'/visu.css') %}" ) + iln_html = self.findinsertline(root_contents, '{% import "plot.html" as plot %}') + iln_js = self.findinsertline(root_contents, "{% if isfile('pages/'~config_pages~'/visu.js') %}") + iln_css = self.findinsertline(root_contents, "{% if isfile('pages/'~config_pages~'/visu.css') %}") # copy widgets from plugin directories of configured plugins # read plungin.conf _conf = lib.config.parse(smarthome._plugin_conf) - self.logger.debug( "install_widgets: _conf = {}".format(str(_conf)) ) + self.logger.debug("install_widgets: _conf = {}".format(str(_conf))) mypluginlist = [] for plugin in _conf: self.logger.debug("install_widgets: Plugin section '{}', class_path = '{}', plugin_name = '{}'".format(plugin, str(_conf[plugin].get('class_path', '')), str(_conf[plugin].get('plugin_name', '')))) @@ -525,15 +507,15 @@ def install_widgets(self, smarthome): plgdir = 'plugins.' + _conf[plugin].get('plugin_name', '') if plgdir not in mypluginlist: # process each plugin only once - mypluginlist.append( plgdir ) + mypluginlist.append(plgdir) if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8': - self.copy_widgets( plgdir.replace('.', '/'), root_contents, iln_html, iln_js, iln_css ) + self.copy_widgets(plgdir.replace('.', '/'), root_contents, iln_html, iln_js, iln_css) else: - self.copy_widgets( plgdir.replace('.', '/') ) + self.copy_widgets(plgdir.replace('.', '/')) if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8': # write root.html with additions for widgets - self.logger.info( "Adding import statements to root.html" ) + self.logger.info("Adding import statements to root.html") f_root = open(self.pgbdir + '/root.html', "w") root_contents = "".join(root_contents) f_root.write(root_contents) @@ -542,16 +524,15 @@ def install_widgets(self, smarthome): ######################################################################### - def findinsertline(self, root_contents, searchstring ): + def findinsertline(self, root_contents, searchstring): # look for insert point in root.html: find and return line that contains the searchstring iln = '' for ln in root_contents: - if ln.find( searchstring ) != -1: + if ln.find(searchstring) != -1: iln = ln if iln == '': self.logger.warning("findinsertline: No insert point for pattern {0}".format(searchstring)) - return( iln ) - + return(iln) def copy_widgets(self, plgdir, root_contents='', iln_html='', iln_js='', iln_css=''): wdgdir = 'sv_widgets' @@ -569,47 +550,45 @@ def copy_widgets(self, plgdir, root_contents='', iln_html='', iln_js='', iln_css if self.smartvisu_version == '2.9': if os.path.splitext(fn)[1] == '.png': # copy icons to the icons directory - shutil.copy2( os.path.join(srcdir, fn), self.icndir ) + shutil.copy2(os.path.join(srcdir, fn), self.icndir) else: # the rest to the widgets directory & strip 'widgets_' from name if fn.startswith('widget_'): dn = fn[len('widget_'):] - shutil.copy2( os.path.join(srcdir, fn), os.path.join(self.outdir, dn) ) + shutil.copy2(os.path.join(srcdir, fn), os.path.join(self.outdir, dn)) else: - shutil.copy2( os.path.join(srcdir, fn), self.outdir ) + shutil.copy2(os.path.join(srcdir, fn), self.outdir) else: # v2.7 & v2.8 - shutil.copy2( srcdir + '/' + fn, self.outdir ) + shutil.copy2(srcdir + '/' + fn, self.outdir) if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8': if (fn[0:7] == "widget_") and (fn[-5:] == ".html"): self.logger.info("- Installing for SV v{} from '{}': {}".format(self.smartvisu_version, plgdir, '\t' + fn)) if iln_html != '': - self.create_htmlinclude(fn, fn[7:-5] , root_contents, iln_html) + self.create_htmlinclude(fn, fn[7:-5], root_contents, iln_html) if (fn[0:7] == "widget_") and (fn[-3:] == ".js"): if iln_js != '': - self.create_jsinclude(fn, fn[7:-3] , root_contents, iln_js) + self.create_jsinclude(fn, fn[7:-3], root_contents, iln_js) if (fn[0:7] == "widget_") and (fn[-4:] == ".css"): if iln_css != '': - self.create_cssinclude(fn, fn[7:-4] , root_contents, iln_css) + self.create_cssinclude(fn, fn[7:-4], root_contents, iln_css) return - def create_htmlinclude(self, filename, classname, root_contents, iln_html): insertln = root_contents.index(iln_html) +1 # Insert widget statements to root_contents if insertln != 0: - self.logger.debug( "create_htmlinclude: Inserting in root.html at line {0} after '{1}'".format(insertln, iln_html) ) + self.logger.debug("create_htmlinclude: Inserting in root.html at line {0} after '{1}'".format(insertln, iln_html)) twig_statement = '\t{% import "' + self.shwdgdir + '/' + filename + '" as ' + classname + ' %}' root_contents.insert(insertln, twig_statement+'\n') - def create_jsinclude(self, filename, classname, root_contents, iln_js): insertln = root_contents.index(iln_js) # Insert widget statements to root_contents if insertln > -1: - self.logger.debug( "create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_js) ) + self.logger.debug("create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_js)) twig_statement1 = "\t{% if isfile('widgets/sh_widgets/" + filename + "') %}" twig_statement2 = '\t\t{% endif %}' self.logger.debug('create_jsinclude: {0}'.format(twig_statement1)) @@ -617,12 +596,11 @@ def create_jsinclude(self, filename, classname, root_contents, iln_js): root_contents.insert(insertln, twig_statement2+'\n') root_contents.insert(insertln, twig_statement1+'\n') - def create_cssinclude(self, filename, classname, root_contents, iln_css): insertln = root_contents.index(iln_css) # Insert widget statements to root_contents if insertln > -1: - self.logger.debug( "create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_css) ) + self.logger.debug("create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_css)) twig_statement1 = "\t{% if isfile('widgets/sh_widgets/" + filename + "') %}" twig_statement2 = '\t\t{% endif %}' self.logger.debug('create_cssinclude: {0}'.format(twig_statement1)) @@ -630,7 +608,6 @@ def create_cssinclude(self, filename, classname, root_contents, iln_css): root_contents.insert(insertln, twig_statement2+'\n') root_contents.insert(insertln, twig_statement1+'\n') - def remove_oldfiles(self): # clear temp directory if not os.path.isdir(self.tmpdir): @@ -668,4 +645,3 @@ def remove_oldfiles(self): self.logger.warning("Could not delete file {0}: {1}".format(fp, e)) return True - From 919e5473f8f96453241891baba0b10af7f2c95e7 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 15:44:16 +0200 Subject: [PATCH 079/121] visu_smartvisu plugin: important log message updates when visu style unknoen and copying template failed --- visu_smartvisu/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/visu_smartvisu/__init__.py b/visu_smartvisu/__init__.py index 8eeac6012..feaaa1d9f 100755 --- a/visu_smartvisu/__init__.py +++ b/visu_smartvisu/__init__.py @@ -148,9 +148,9 @@ def __init__(self, smarthome, smartvisu_dir='', overwrite_templates=True, visu_s self.smartvisu_version = smartvisu_version self.overwrite_templates = overwrite_templates self.visu_style = visu_style.lower() - if not self.visu_style in ['std','blk']: + if self.visu_style not in ['std', 'blk']: self.visu_style = 'std' - self.logger.warning("SmartVisuGenerator: visu_style '{}' unknown, using visu_style '{1}'".format(visu_style, self.visu_style)) + self.logger.warning("SmartVisuGenerator: visu_style '{0}' unknown, using visu_style '{1}'".format(visu_style, self.visu_style)) self.logger.info("Generating pages for smartVISU v{}".format(self.smartvisu_version)) @@ -364,14 +364,13 @@ def write_parseresult(self, htmlfile, parseresult): except Exception as e: self.logger.warning("Could not write to {0}/{1}: {2}".format(self.outdir, htmlfile, e)) - def copy_tpl(self, tplname, destname=''): if destname == '': destname = tplname try: shutil.copy(self.tpldir + '/' + tplname, self.outdir + '/' + destname) except Exception as e: - self.logger.error("Could not copy {0} from {1} to {2}".format(tplname, tpldir, destdir)) + self.logger.error("Could not copy {0} from {1} to {2}".format(tplname, self.tpldir, self.outdir)) ######################################################################### From 3537c36bc548b4f66043078caaa075d6ce3f5ce4 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 16:11:03 +0200 Subject: [PATCH 080/121] stateengine plugin: important fix when logging wrongly defined item for action --- stateengine/StateEngineAction.py | 1 - stateengine/StateEngineActions.py | 4 ++-- stateengine/StateEngineItem.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 3a33966c4..a8973b525 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -668,7 +668,6 @@ def complete(self, state, action_type, evals_items=None, use=None): self._abitem.set_variable('current.state_name', state.name) self._action_type = action_type self._state = state - self._item, self._status, self._mindelta, self._value, _issue = self.check_complete( state, self._item, self._status, self._mindelta, self._value, "set/force", evals_items, use) self._action_status = _issue diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 06a34d1cc..9c2d44a00 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -529,7 +529,7 @@ def complete(self, state, action_type, evals_items=None, use=None): _status.update(self.__actions[name].complete(state, action_type, evals_items, use)) except ValueError as ex: _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) - raise ValueError("State '{0}', Action '{1}': {2}".format(state.id, name, ex)) + raise ValueError("Completing State '{0}', Action '{1}': {2}".format(state.id, name, ex)) self._log_debug("Completing {} for state {} status {}", self.__actions, state, _status) return _status @@ -538,7 +538,7 @@ def set(self, value): try: self.__actions[name].update(value) except ValueError as ex: - raise ValueError("State '{0}', Action '{1}': {2}".format(value.property.path, name, ex)) + raise ValueError("Setting State '{0}', Action '{1}': {2}".format(value.property.path, name, ex)) # Execute all actions # is_repeat: Indicate if this is a repeated action without changing the state diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 4d71e26e0..7c8571cda 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -2131,7 +2131,7 @@ def return_item(self, item_id): _, _, item = item.partition(":") return item, None elif match: - _issue = ("Item '{0}' has to be defined as an item path " + _issue = ("Item '{}' has to be defined as an item path " "or eval expression without {}.").format(match.group(1), item_id) self.__logger.warning(_issue) return None, [_issue] From df90163bf5f53109ba800055823705abe60e1504 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 11 Sep 2024 16:41:02 +0200 Subject: [PATCH 081/121] stateengine plugin: fix on_pass actions and implement function to suspend state --- stateengine/StateEngineItem.py | 10 ++++------ stateengine/StateEngineState.py | 17 +++++++++++++---- stateengine/plugin.yaml | 28 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 7c8571cda..12957bcb5 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1015,13 +1015,11 @@ def _nested_set(dic, keys, val): if update is True and isinstance(dic.get(keys[-1]), dict) and isinstance(val, dict): # Update the existing dictionary with the new dictionary dic[keys[-1]].update(val) - self.__logger.develop("Updating WEBIF with list {}, value: {}. infos is {}", key, value, - self.__webif_infos) + #self.__logger.develop("Updating WEBIF with list {}, value: {}. infos is {}", key, value, self.__webif_infos) else: # Otherwise, set the value as is dic[keys[-1]] = val - self.__logger.develop("Setting WEBIF with list {}, value: {}. infos is {}", key, value, - self.__webif_infos) + #self.__logger.develop("Setting WEBIF with list {}, value: {}. infos is {}", key, value, self.__webif_infos) def _nested_test(dic, keys): for nestedkey in keys[:-2]: @@ -1430,8 +1428,8 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): if refill: state.refill() can_enter = state.can_enter() - if can_enter is False: - state.run_pass() + if can_enter[0] is False: + state.run_pass(self.__repeat_actions.get()) return can_enter except Exception as ex: self.__logger.warning("Problem with currentstate {0}. Error: {1}", state.id, ex) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index d96ed24b8..d8adfb96d 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -312,9 +312,11 @@ def run_enter(self, allow_item_repeat: bool): _key_leave = ['{}'.format(self.id), 'leave'] _key_stay = ['{}'.format(self.id), 'stay'] _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] self._abitem.update_webif(_key_leave, False) self._abitem.update_webif(_key_stay, False) self._abitem.update_webif(_key_enter, True) + self._abitem.update_webif(_key_pass, False) self.__actions_enter.execute(False, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) @@ -325,9 +327,11 @@ def run_stay(self, allow_item_repeat: bool): _key_leave = ['{}'.format(self.id), 'leave'] _key_stay = ['{}'.format(self.id), 'stay'] _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] self._abitem.update_webif(_key_leave, False) self._abitem.update_webif(_key_stay, True) self._abitem.update_webif(_key_enter, False) + self._abitem.update_webif(_key_pass, False) self.__actions_stay.execute(True, allow_item_repeat, self, self.__actions_enter_or_stay) self._log_decrease_indent(50) @@ -336,10 +340,15 @@ def run_stay(self, allow_item_repeat: bool): def run_pass(self, allow_item_repeat: bool): self._log_info("Passing state {}, running pass actions.", self.id) self._log_increase_indent() - for elem in self._abitem.webif_infos: - _key_pass = ['{}'.format(elem), 'pass'] - self._abitem.update_webif(_key_pass, False) - self.__actions_pass.execute(False, allow_item_repeat, self) + _key_leave = ['{}'.format(self.id), 'leave'] + _key_stay = ['{}'.format(self.id), 'stay'] + _key_enter = ['{}'.format(self.id), 'enter'] + _key_pass = ['{}'.format(self.id), 'pass'] + self._abitem.update_webif(_key_leave, False) + self._abitem.update_webif(_key_stay, False) + self._abitem.update_webif(_key_enter, False) + self._abitem.update_webif(_key_pass, True) + self.__actions_pass.execute(True, allow_item_repeat, self) self._log_decrease_indent(50) # run actions when leaving the state diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index f8f0f6f56..0d1ea337a 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -943,6 +943,20 @@ item_structs: - 'repeat: True' - 'order: 5' + on_pass: + se_action_suspend: + - 'function: set' + - 'to: False' + se_action_suspend_visu: + - 'function: set' + - 'to: False' + se_action_suspend_end: + - 'function: set' + - 'to: ' + se_action_suspend_start: + - 'function: set' + - 'to: ' + on_leave: se_action_suspend: - 'function: set' @@ -1258,6 +1272,20 @@ item_structs: - 'repeat: True' - 'order: 5' + on_pass: + se_action_suspend: + - 'function: set' + - 'to: False' + se_action_suspend_visu: + - 'function: set' + - 'to: False' + se_action_suspend_end: + - 'function: set' + - 'to: ' + se_action_suspend_start: + - 'function: set' + - 'to: ' + on_leave: se_action_suspendvariant: - 'function: set' From 2a0b5cc40b30f3f33c9391b287089e60ce94c0c2 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 18 Sep 2024 11:49:59 +0200 Subject: [PATCH 082/121] stateengine plugin: improve on_pass actions --- stateengine/StateEngineItem.py | 43 ++++++++++++++++++++++++++------- stateengine/StateEngineState.py | 9 +++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 12957bcb5..829f3a53a 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -103,6 +103,14 @@ def logger(self): def instant_leaveaction(self): return self.__instant_leaveaction.get() + @property + def last_run(self): + return self.__last_run + + @last_run.setter + def last_run(self, value_dict): + self.__last_run.update(value_dict) + @property def default_instant_leaveaction(self): return self.__default_instant_leaveaction.get() @@ -204,6 +212,8 @@ def __init__(self, smarthome, item, se_plugin): self.__active_schedulers = [] self.__release_info = {} self.__cache = {} + self.__last_run = {} + self.__pass_repeat = {} self.__default_instant_leaveaction = StateEngineValue.SeValue(self, "Default Instant Leave Action", False, "bool") self.__instant_leaveaction = StateEngineValue.SeValue(self, "Instant Leave Action", False, "num") try: @@ -640,6 +650,7 @@ def update_current_to_empty(d): # find new state _leaveactions_run = False + _pass_state = None if _instant_leaveaction >= 1 and caller != "Released_by Retrigger": evaluated_instant_leaveaction = True @@ -649,7 +660,7 @@ def update_current_to_empty(d): _previousstate_conditionset_name = '' update_current_to_empty(self.__webif_infos) - self.__logger.develop("Resetted current info for webif info. It is now: {}", self.__webif_infos) + self.__logger.develop("Reset current info for webif info. It is now: {}", self.__webif_infos) for state in self.__states: if not self.__ab_alive: self.__logger.debug("StateEngine Plugin not running (anymore). Stop state evaluation.") @@ -673,14 +684,25 @@ def update_current_to_empty(d): self.__conditionsets.update( {state.state_item.property.path: [_last_conditionset_id, _last_conditionset_name]}) # New state is different from last state - - if result is False and last_state == state and evaluated_instant_leaveaction is True: - self.__logger.info("Leaving {0} ('{1}'). Running actions immediately.", last_state.id, - last_state.name) - last_state.run_leave(self.__repeat_actions.get()) - _leaveactions_run = True + if result is False and last_state == state: + if evaluated_instant_leaveaction is True: + self.__logger.info("Leaving {0} ('{1}'). Running actions immediately.", last_state.id, + last_state.name) + last_state.run_leave(self.__repeat_actions.get()) + _leaveactions_run = True + elif result is False and last_state != state and state.actions_pass.count() > 0: + _pass_state = state + state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get()) + _key_pass = ['{}'.format(last_state.id), 'pass'] + self.update_webif(_key_pass, True) + self.__pass_repeat.update({state: True}) if result is True: new_state = state + for repeat_state in self.__pass_repeat: + if new_state.order < repeat_state.order: + self.__pass_repeat.update({repeat_state: False}) + _key_pass = ['{}'.format(repeat_state.id), 'pass'] + self.update_webif(_key_pass, False) break # no new state -> stay @@ -743,10 +765,12 @@ def update_current_to_empty(d): _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] _key_enter = ['{}'.format(last_state.id), 'enter'] + _key_pass = ['{}'.format(last_state.id), 'pass'] self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) + self.update_webif(_key_pass, False) self.__handle_releasedby(new_state, last_state, _instant_leaveaction) if self.update_lock.locked(): @@ -817,16 +841,19 @@ def update_current_to_empty(d): new_state.id, new_state.name, _last_conditionset_id, _last_conditionset_name) new_state.run_enter(self.__repeat_actions.get()) + self.__laststate_set(new_state) self.__previousstate_set(last_state) if _leaveactions_run is True and self.__ab_alive: _key_leave = ['{}'.format(last_state.id), 'leave'] _key_stay = ['{}'.format(last_state.id), 'stay'] _key_enter = ['{}'.format(last_state.id), 'enter'] + _key_pass = ['{}'.format(last_state.id), 'pass'] self.update_webif(_key_leave, True) self.update_webif(_key_stay, False) self.update_webif(_key_enter, False) + self.update_webif(_key_pass, False) self.__logger.debug("State evaluation finished") all_released_by = self.__handle_releasedby(new_state, last_state, _instant_leaveaction) @@ -1428,8 +1455,6 @@ def __update_check_can_enter(self, state, instant_leaveaction, refill=True): if refill: state.refill() can_enter = state.can_enter() - if can_enter[0] is False: - state.run_pass(self.__repeat_actions.get()) return can_enter except Exception as ex: self.__logger.warning("Problem with currentstate {0}. Error: {1}", state.id, ex) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index d8adfb96d..4695ceaf3 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -337,7 +337,7 @@ def run_stay(self, allow_item_repeat: bool): # run actions when passing the state # item_allow_repeat: Is repeating actions generally allowed for the item? - def run_pass(self, allow_item_repeat: bool): + def run_pass(self, is_repeat: bool, allow_item_repeat: bool): self._log_info("Passing state {}, running pass actions.", self.id) self._log_increase_indent() _key_leave = ['{}'.format(self.id), 'leave'] @@ -348,7 +348,7 @@ def run_pass(self, allow_item_repeat: bool): self._abitem.update_webif(_key_stay, False) self._abitem.update_webif(_key_enter, False) self._abitem.update_webif(_key_pass, True) - self.__actions_pass.execute(True, allow_item_repeat, self) + self.__actions_pass.execute(is_repeat, allow_item_repeat, self) self._log_decrease_indent(50) # run actions when leaving the state @@ -642,7 +642,6 @@ def update_action_status(action_status, actiontype): use = StateEngineTools.flatten_list(use) self.__fill_list(use, recursion_depth, se_use, use) # Get action sets and condition sets - self._log_develop("Use is {}", use) parent_item = item_state.return_parent() if parent_item == Items.get_instance(): parent_item = None @@ -697,9 +696,9 @@ def update_action_status(action_status, actiontype): if child_name in action_mapping: action_name, action_method = action_mapping[child_name] for attribute in child_item.conf: - self._log_develop("Filling state with {} action named {} based on {}", child_name, attribute, state) + self._log_develop("Filling state with {} action named {} for state {} with config {}", child_name, attribute, state.id, child_item.conf) _action_counts[action_name] += 1 - _, _action_status = action_method.update(attribute, child_item.conf[attribute]) + _, _action_status = action_method.update(attribute, child_item.conf.get(attribute)) if _action_status: update_action_status(_action_status, action_name) self._abitem.update_action_status(self.__action_status) From f1e6223072ad0f9083aa329add9b25b06c972d99 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 18 Sep 2024 11:50:39 +0200 Subject: [PATCH 083/121] stateengine plugin: fix variable logging/handling --- stateengine/StateEngineValue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 84aedafdd..aeb4b22b4 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -532,7 +532,10 @@ def write_to_logger(self): self._log_debug("{0} from variable: {1}", self.__name, i) else: self._log_debug("{0} from variable: {1}", self.__name, self.__varname) - return self.__get_from_variable() + _original_listorder = self.__listorder.copy() + var_result = self.__get_from_variable() + self.__listorder = _original_listorder + return var_result return None # Get Text (similar to logger text) @@ -1016,11 +1019,10 @@ def update_value(varname): if isinstance(self.__varname, list): for var in self.__varname: - values.append(update_value(var)) self._log_debug("Checking variable in loop '{0}', value {1} from list {2}", var, values[-1], self.__listorder) + values.append(update_value(var)) else: values = update_value(self.__varname) self._log_debug("Variable result: {0}", values) - return values From 928ae6092f8772c4916a608a9d03fd3f5e9e63ec Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 18 Sep 2024 11:56:05 +0200 Subject: [PATCH 084/121] stateengine plugin: improve web interface - include pass actions, optimize visualization, optimize info text for actions that are not run, some fixes --- stateengine/StateEngineAction.py | 74 ++++++---- stateengine/StateEngineWebif.py | 227 +++++++++++++++---------------- 2 files changed, 162 insertions(+), 139 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index a8973b525..bdf68a4ba 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -162,7 +162,7 @@ def get_order(self): order = 1 return order - def update_webif_actionstatus(self, state, name, success, issue=None): + def update_webif_actionstatus(self, state, name, success, issue=None, reason=None): try: if self._action_type == "actions_leave": state.update_name(state.state_item) @@ -171,8 +171,12 @@ def update_webif_actionstatus(self, state, name, success, issue=None): if self._abitem.webif_infos[state.id].get(self._action_type): _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'success'] self._abitem.update_webif(_key, success) - _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'issue'] - self._abitem.update_webif(_key, issue) + if issue is not None: + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'issue'] + self._abitem.update_webif(_key, issue) + if reason is not None: + _key = ['{}'.format(state.id), self._action_type, '{}'.format(name), 'actionstatus', 'reason'] + self._abitem.update_webif(_key, reason) except Exception as ex: self._log_warning("Error setting action status {}: {}", name, ex) @@ -412,6 +416,7 @@ def _check_condition(condition: str): _conditions_met_count += 1 else: self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated_current_condition) + self.update_webif_actionstatus(state, self._name, 'False', None, f"({condition} {_orig_cond} not met)") except Exception as ex: if cond is not None: self._log_warning("Given {} {} is not a valid regex: {}", condition, _orig_cond, ex) @@ -437,12 +442,12 @@ def _update_delay_webif(delay_info: str): # update web interface with repeat info # value: bool type True or False for repeat value def _update_repeat_webif(value: bool): - _key1 = ['{}'.format(state.id), self._action_type, '{}'.format(self._name), 'repeat'] + _key1 = [state.id, self._action_type, self._name, 'repeat'] self._abitem.update_webif(_key1, value, True) self._log_decrease_indent(50) self._log_increase_indent() - self._log_info("Action '{0}': Preparing", self._name) + self._log_info("Action '{0}' defined in '{1}': Preparing", self._name, self._action_type) self._log_increase_indent() try: self._getitem_fromeval() @@ -480,7 +485,8 @@ def _update_repeat_webif(value: bool): if conditions_met < condition_necessary: self._log_info("Action '{0}': Skipping because not all conditions are met.", self._name) return - + elif condition_necessary > 0 and conditions_met == condition_necessary: + self.update_webif_actionstatus(state, self._name, 'True', None, "(all conditions met)") if is_repeat: if self.__repeat is None: if allow_item_repeat: @@ -488,17 +494,26 @@ def _update_repeat_webif(value: bool): _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by item configuration.", self._name) + self.update_webif_actionstatus(state, self._name, 'False', None, "(no repeat by item)") _update_repeat_webif(False) return elif self.__repeat.get(): repeat_text = " Repeat allowed by action configuration." + self.update_webif_actionstatus(state, self._name, 'True') _update_repeat_webif(True) else: self._log_info("Action '{0}': Repeat denied by action configuration.", self._name) + self.update_webif_actionstatus(state, self._name, 'False', None, "(no repeat by action)") _update_repeat_webif(False) return else: - repeat_text = "" + if self.__repeat is None: + repeat_text = "" + elif self.__repeat.get(): + repeat_text = " Repeat allowed by action configuration but not applicable." + self.update_webif_actionstatus(state, self._name, 'True') + else: + repeat_text = "" self._log_increase_indent() if _validitem: delay = 0 if self.__delay.is_empty() else self.__delay.get() @@ -530,6 +545,7 @@ def _update_repeat_webif(value: bool): self._log_decrease_indent() _delay_info = -1 else: + _delay_info = delay self._waitforexecute(state, actionname, self._name, repeat_text, delay, current_condition_met, previous_condition_met, previousstate_condition_met, next_condition_met) _update_delay_webif(str(_delay_info)) @@ -715,24 +731,34 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s return value if not self._mindelta.is_empty(): - mindelta = self._mindelta.get() - if self._status is not None: - # noinspection PyCallingNonCallable - delta = float(abs(self._status() - value)) - additionaltext = "of statusitem " - else: - delta = float(abs(self._item() - value)) + mindelta = float(self._mindelta.get()) + try: + 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 = "" + except Exception: + delta = None additionaltext = "" - - self._delta = delta - self._info_dict.update({'delta': str(delta), 'mindelta': str(mindelta)}) - _key = [self._state.id, self._action_type, self._name] - self._abitem.update_webif(_key, self._info_dict, True) - if 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) - self.update_webif_actionstatus(state, self._name, 'False') - return + self._log_warning("{0}: Can not evaluate delta as value '{1}' is no number.", self._name, value) + if delta is not None: + self._delta = delta + self._info_dict.update({'delta': str(delta), 'mindelta': str(mindelta)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + if delta < mindelta: + text = "{0}: Not setting '{1}' to '{2}' because delta {3}'{4:.2f}' is lower than mindelta '{5:.2f}'." + self._log_debug(text, actionname, self._item.property.path, value, additionaltext, delta, mindelta) + self.update_webif_actionstatus(state, self._name, 'False', None, f"(delta '{delta:.2f}' < '{mindelta:.2f})") + return + else: + text = "{0}: Proceeding because delta {1}'{2:.2f}' is lower than mindelta '{3:.2f}'." + self.update_webif_actionstatus(state, self._name, 'True', None, + f"(delta '{delta:.2f}' > '{mindelta:.2f})") + self._log_debug(text, actionname, additionaltext, delta, mindelta) source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) self._force_set(actionname, self._item, value, source) self._execute_set_add_remove(state, actionname, namevar, repeat_text, self._item, value, source, current_condition, previous_condition, previousstate_condition, next_condition) diff --git a/stateengine/StateEngineWebif.py b/stateengine/StateEngineWebif.py index af031d02a..040ff6209 100755 --- a/stateengine/StateEngineWebif.py +++ b/stateengine/StateEngineWebif.py @@ -75,33 +75,14 @@ def _strip_regex(self, regex_list): pattern_strings = pattern_strings[0] return str(pattern_strings) - def _actionlabel(self, state, label_type, conditionset, previousconditionset, previousstate_conditionset, next_conditionset): + def _actionlabel(self, state, label_type, conditionset, active): # Check if conditions for action are met or not - # action_dict: abitem[state]['on_enter'/'on_stay'/'on_enter_or_stay'/'on_leave'].get(action) - # condition_to_meet: 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' - # conditionset: name of conditionset that should get checked - - def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: str): - _condition_check = action_dict.get(condition_to_meet) - _condition_check = StateEngineTools.flatten_list(_condition_check) - _condition_necessary = 1 if _condition_check != 'None' else 0 - _condition_check = _condition_check if isinstance(_condition_check, list) else [_condition_check] - _condition_count = 0 - _condition = False - for cond in _condition_check: - try: - if isinstance(cond, str): - cond = re.compile(cond) - _matching = cond.fullmatch(conditionset) - except Exception: - _matching = True - _condition_count += 1 if _matching else 0 - _condition = True if _matching else False - return _condition_count, _condition, _condition_check, _condition_necessary + # state: state where action is defined in + # label_type: on_enter, on_stay, on_leave, on_pass + # active: if action is currently run actionlabel = actionstart = '<' action_tooltip = '' - originaltype = label_type types = [label_type] if label_type in ['actions_leave', 'actions_pass'] else ['actions_enter_or_stay', label_type] tooltip_count = 0 @@ -111,59 +92,33 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s if action_dict.get('actionstatus'): _success = action_dict['actionstatus'].get('success') _issue = action_dict['actionstatus'].get('issue') + _reason = action_dict['actionstatus'].get('reason') else: _success = None _issue = None + _reason = None _repeat = action_dict.get('repeat') _delay = int(float(action_dict.get('delay') or 0)) - _delta = action_dict.get('delta') or '0' - _mindelta = action_dict.get('mindelta') or '0' - - condition_necessary = 0 - condition_met = True - condition_count = 0 - count, condition1, condition_to_meet, necessary = _check_webif_conditions(action_dict, 'conditionset', conditionset) - condition_count += count - condition_necessary += min(1, necessary) - count, condition2, previouscondition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousconditionset', previousconditionset) - condition_count += count - condition_necessary += min(1, necessary) - count, condition3, previousstate_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'previousstate_conditionset', previousstate_conditionset) - condition_count += count - condition_necessary += min(1, necessary) - count, condition4, next_condition_to_meet, necessary = _check_webif_conditions(action_dict, 'nextconditionset', next_conditionset) - condition_count += count - condition_necessary += min(1, necessary) - - if condition_count < condition_necessary: - condition_met = False + cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state cond2 = self.__states[state]['conditionsets'].get(conditionset) is not None - 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 or _issue else "black" - condition_info = self._strip_regex(condition_to_meet) if condition1 is False \ - else self._strip_regex(previouscondition_to_meet) if condition2 is False \ - else self._strip_regex(previousstate_condition_to_meet) if condition3 is False \ - else self._strip_regex(next_condition_to_meet) if condition4 is False \ - else "" + + fontcolor = "white" if (_success == "False" and active) and ((cond1 and cond2 and _reason) or (_reason and label_type in ['actions_leave', 'actions_pass'])) \ + else "#f4c430" if _delay > 0 and active else "darkred" if _delay < 0 and active \ + else "#303030" if _issue else "black" + if _issue: if tooltip_count > 0: action_tooltip += ' ' tooltip_count += 1 action_tooltip += '{}'.format(_issue) if _issue is not None else '' - additionaltext = " (issue: see tooltip)" if _issue is not None\ - else " ({} 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 " (cancel delay!)" if _delay == -1 \ - else " (wrong delay!)" if _delay < -1 \ - else " (delta {} < {})".format(_delta, _mindelta) if cond_delta and cond1 and cond2\ - else "" + additionaltext = " (issue: see tooltip)" if _issue is not None \ + else _reason if _reason is not None \ + else " (delay: {})".format(_delay) if _delay > 0\ + else " (cancel delay!)" if _delay == -1 \ + else " (wrong delay!)" if _delay < -1 \ + else "" action1 = action_dict.get('function') if action1 in ['set', 'force set']: action2 = str(action_dict.get('item')) @@ -179,14 +134,10 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s else: action2 = 'None' action3 = "" - cond1 = conditionset in ['', self.__active_conditionset] and state == self.__active_state - cond_enter = originaltype == 'actions_enter' and self.__states[state].get('enter') is True - cond_stay = originaltype == 'actions_stay' and self.__states[state].get('stay') is True - active = True if (cond_enter or cond_stay) and cond1 else False success_info = '' \ if _issue is not None and active \ else '' \ - if (_success == 'False' or not condition_met) and active \ + if _success == 'False' and active \ else '' \ if _success == 'Scheduled' and active \ else '' \ @@ -205,7 +156,7 @@ def _check_webif_conditions(action_dict, condition_to_meet: str, conditionset: s #self._log_debug('actionlabel: {}', actionlabel) return actionlabel, action_tooltip, tooltip_count - def _conditionlabel(self, state, conditionset, i): + def _conditionlabel(self, state, conditionset): condition_tooltip = '' conditions_done = [] _empty_set = self.__states[state]['conditionsets'].get(conditionset) == '' @@ -363,7 +314,7 @@ def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, c label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying at state' - position = '{},{}!'.format(0.63, new_y) + position = '{},{}!'.format(0.38, new_y) color = color_enter if label == 'first enter' else color_stay self.__nodes['{}_{}_state_{}'.format(state, conditionset, action_type)] = \ pydotplus.Node('{}_{}_state_{}'.format(state, conditionset, action_type), style="filled", fillcolor=color, @@ -396,9 +347,6 @@ def drawgraph(self, filename): new_y = 2 previous_state = '' previous_conditionset = '' - previousconditionset = '' - previousstate_conditionset = '' - next_conditionset = '' for i, state in enumerate(self.__states): #self._log_debug('Adding state for webif {}', self.__states[state]) if isinstance(self.__states[state], (OrderedDict, dict)): @@ -415,13 +363,14 @@ def drawgraph(self, filename): new_y -= 1 * self.__scalefactor position = '{},{}!'.format(0, new_y) if not i == 0: - condition_node = 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ - else list(self.__states[previous_state]['conditionsets'].keys())[-1] + condition_node = 'pass' if self.__nodes.get('{}_pass'.format(previous_state)) \ + else 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \ + else list(self.__states[previous_state]['conditionsets'].keys())[-1] lastnode = self.__nodes['{}_{}'.format(previous_state, condition_node)] self.__nodes['{}_above'.format(state)] = pydotplus.Node('{}_above'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes['{}_above'.format(state)]) - position = '{},{}!'.format(0.5, new_y) + position = '{},{}!'.format(0.3, new_y) self.__nodes['{}_above_right'.format(state)] = pydotplus.Node('{}_above_right'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes['{}_above_right'.format(state)]) @@ -441,7 +390,7 @@ def drawgraph(self, filename): label='<

    ' '
    {}
    {}
    >'.format( state, self.__states[state]['name'])) - position = '{},{}!'.format(0.5, new_y) + position = '{},{}!'.format(0.3, new_y) self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position, shape="square", width="0", label="") self.__graph.add_node(self.__nodes[state]) @@ -450,66 +399,82 @@ def drawgraph(self, filename): actionlist_enter = '' actionlist_stay = '' actionlist_leave = '' + actionlist_pass = '' condition_tooltip = '' action_tooltip = '' j = 0 - new_x = 0.9 + new_x = 0.55 actions_enter = self.__states[state].get('actions_enter') or [] actions_enter_or_stay = self.__states[state].get('actions_enter_or_stay') or [] actions_stay = self.__states[state].get('actions_stay') or [] actions_leave = self.__states[state].get('actions_leave') or [] + actions_pass = self.__states[state].get('actions_pass') or [] action_tooltip_count_enter = 0 action_tooltip_count_stay = 0 action_tooltip_count_leave = 0 + action_tooltip_count_pass = 0 action_tooltip_enter = "" action_tooltip_stay = "" action_tooltip_leave = "" + action_tooltip_pass = "" for j, conditionset in enumerate(self.__states[state]['conditionsets']): - - if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: - actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ - self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) - - if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: - actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ - self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) - - if len(actions_leave) > 0: - actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ - self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset, next_conditionset) - - new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor - position = '{},{}!'.format(0.5, new_y) - conditionset_positions.append(new_y) - #self._log_debug('conditionset: {} {}, previous {} next {}', conditionset, position, previous_conditionset, next_conditionset) - - conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset, i) cond3 = conditionset == '' try: cond1 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception as ex: - #self._log_debug('Condition 1 problem {}'.format(ex)) + except Exception: cond1 = True try: cond4 = i == list(self.__states.keys()).index(self.__active_state) - except Exception as ex: - #self._log_debug('Condition 4 problem {}'.format(ex)) + except Exception: cond4 = True #self._log_debug('i {}, index of active state {}', i, list(self.__states.keys()).index(self.__active_state)) try: cond2 = (j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) or i > list(self.__states.keys()).index(self.__active_state)) - except Exception as ex: - #self._log_debug('Condition 2 problem {}'.format(ex)) + except Exception: cond2 = False if cond3 and cond4 else True color = "gray" if cond1 and cond2 else "olivedrab" \ if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2" + try: + cond5 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond5 = True + + cond6 = conditionset in ['', self.__active_conditionset] and state == self.__active_state + cond_enter = True if self.__states[state].get('enter') is True else False + cond_stay = True if self.__states[state].get('stay') is True else False + active = True if cond_enter and cond6 else False + + if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0: + actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \ + self._actionlabel(state, 'actions_enter', conditionset, active) + active = True if cond_stay and cond6 else False + if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0: + actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \ + self._actionlabel(state, 'actions_stay', conditionset, active) + cond_leave = True if self.__states[state].get('leave') is True else False + active = True if cond_leave else False + + if len(actions_leave) > 0: + actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \ + self._actionlabel(state, 'actions_leave', conditionset, active) + cond_pass = True if self.__states[state].get('pass') is True else False + active = False if (cond5 and not cond_pass) or cond_leave else True + if len(actions_pass) > 0: + actionlist_pass, action_tooltip_pass, action_tooltip_count_pass = \ + self._actionlabel(state, 'actions_pass', conditionset, active) + + new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor + position = '{},{}!'.format(0.3, new_y) + conditionset_positions.append(new_y) + conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset) + label = 'no condition' if conditionset == '' else conditionset self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node( '{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond", label=label, pos=position) #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist) - position = '{},{}!'.format(0.2, new_y) + position = '{},{}!'.format(0.1, new_y) xlabel = '1 tooltip' if condition_tooltip_count == 1\ else '{} tooltips'.format(condition_tooltip_count)\ if condition_tooltip_count > 1 else '' @@ -518,9 +483,12 @@ def drawgraph(self, filename): '{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color, shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel) self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)]) + # Create a dotted line between conditionlist and conditionset name + parenthesis_edge = pydotplus.Edge(self.__nodes['{}_{}_conditions'.format(state, conditionset)], self.__nodes['{}_{}'.format(state, conditionset)], arrowhead="none", color="black", style="dotted", constraint="false") + self.__graph.add_edge(parenthesis_edge) self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)]) - new_x = 0.9 + new_x = 0.55 if not actionlist_enter == '': position = '{},{}!'.format(new_x, new_y) xlabel = '1 tooltip' if action_tooltip_count_enter == 1\ @@ -548,11 +516,12 @@ def drawgraph(self, filename): self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)]) self._add_actioncondition(state, conditionset, 'actions_stay', new_y, cond1, cond2) - position = '{},{}!'.format(0.9, new_y) + position = '{},{}!'.format(0.55, new_y) cond1 = self.__nodes.get('{}_{}_actions_enter'.format(state, conditionset)) is None cond2 = self.__nodes.get('{}_{}_actions_stay'.format(state, conditionset)) is None cond3 = self.__nodes.get('{}_{}_actions_leave'.format(state, conditionset)) is None - if cond1 and cond2 and cond3: + cond4 = self.__nodes.get('{}_{}_actions_pass'.format(state, conditionset)) is None + if cond1 and cond2 and cond3 and cond4: self.__nodes['{}_{}_right'.format(state, conditionset)] = pydotplus.Node('{}_{}_right'.format( state, conditionset), shape="circle", width="0.7", pos=position, label="", fillcolor="black", style="filled", tooltip="No Action") @@ -582,18 +551,13 @@ def drawgraph(self, filename): if len(actions_leave) > 0: new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor - position = '{},{}!'.format(0.5, new_y) - #self._log_debug('leaveconditions {}', position) - try: - cond1 = j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset) - except Exception: - cond1 = True + position = '{},{}!'.format(0.3, new_y) try: cond2 = i >= list(self.__states.keys()).index(self.__active_state) - except Exception: + except Exception as ex: cond2 = True cond3 = True if self.__states[state].get('leave') is True else False - color = "gray" if cond1 and cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" self.__nodes['{}_leave'.format(state)] = pydotplus.Node('{}_leave'.format(state), style="filled", fillcolor=color, shape="diamond", label='leave', pos=position) @@ -616,6 +580,39 @@ def drawgraph(self, filename): self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)], self.__nodes['{}_actions_leave'.format(state)], style='bold', taillabel=" True", tooltip='run leave actions')) + previous_conditionset = self.__nodes['{}_leave'.format(state)] + + if len(actions_pass) > 0: + new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor + position = '{},{}!'.format(0.3, new_y) + try: + cond2 = i >= list(self.__states.keys()).index(self.__active_state) + except Exception: + cond2 = True + cond3 = True if self.__states[state].get('pass') is True else False + color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2" + self.__nodes['{}_pass'.format(state)] = pydotplus.Node('{}_pass'.format(state), + style="filled", fillcolor=color, shape="diamond", + label='pass', pos=position) + self.__graph.add_node(self.__nodes['{}_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_pass'.format(state)], + style='bold', color='black', tooltip='check pass')) + + position = '{},{}!'.format(new_x, new_y) + xlabel = '1 tooltip' if action_tooltip_count_pass == 1\ + else '{} tooltips'.format(action_tooltip_count_pass)\ + if action_tooltip_count_pass > 1 else '' + #self._log_debug('action pass: {}', position) + self.__nodes['{}_actions_pass'.format(state)] = pydotplus.Node('{}_actions_pass'.format(state), + style="filled", fillcolor=color, + shape="rectangle", label=actionlist_pass, + pos=position, align="center", + tooltip=action_tooltip_pass, + xlabel=xlabel) + self.__graph.add_node(self.__nodes['{}_actions_pass'.format(state)]) + self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_pass'.format(state)], + self.__nodes['{}_actions_pass'.format(state)], style='bold', + taillabel=" True", tooltip='run pass actions')) previous_state = state From 27abe95be7cf7c889123c92987682113db933c1e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 18 Sep 2024 11:57:06 +0200 Subject: [PATCH 085/121] stateengine plugin: introduce delta attribute for single actions, introduce minagedelta to run actions in a specific interval only --- stateengine/StateEngineAction.py | 114 +++++++++++++++++++++++++----- stateengine/StateEngineActions.py | 40 ++++++++++- 2 files changed, 136 insertions(+), 18 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index bdf68a4ba..ccd6a29ef 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -22,6 +22,7 @@ from . import StateEngineEval from . import StateEngineValue from . import StateEngineDefaults +from . import StateEngineCurrent import datetime from lib.shtime import Shtime import re @@ -77,6 +78,8 @@ def __init__(self, abitem, name: str): self.previousstate_conditionset = StateEngineValue.SeValue(self._abitem, "previousstate_conditionset", True, "str") self.__mode = StateEngineValue.SeValue(self._abitem, "mode", True, "str") self.__order = StateEngineValue.SeValue(self._abitem, "order", False, "num") + self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta") + self._agedelta = 0 self._scheduler_name = None self._function = None self.__template = None @@ -111,6 +114,20 @@ def update_instanteval(self, value): 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return _issue + def update_mindelta(self, value): + self._log_warning("Mindelta is only relevant for set (force) actions - ignoring") + _issue = {self._name: {'issue': 'Mindelta not relevant for this action type', 'attribute': 'mindelta', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue + + def update_minagedelta(self, value): + if self._minagedelta is None: + self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta", False, "num") + _, _, _issue, _ = self._minagedelta.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'minagedelta', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue + def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") @@ -300,7 +317,37 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} return check_item, check_value, check_mindelta, _issue - def check_complete(self, state, check_item, check_status, check_mindelta, check_value, action_type, evals_items=None, use=None): + def eval_minagedelta(self, actioninfo, state): + lastrun = self._abitem.last_run.get(self._name) + if not lastrun: + return False + if not self._minagedelta.is_empty(): + minagedelta = self._minagedelta.get() + try: + minagedelta = float(minagedelta) + except Exception: + self._log_warning("{0}: minagedelta {1} seems to be no number.", self._name, minagedelta) + minagedelta = 0.0 + self._agedelta = float((datetime.datetime.now() - lastrun).total_seconds()) + self._info_dict.update({'agedelta': self._agedelta, 'minagedelta': str(minagedelta)}) + _key = [self._state.id, self._action_type, self._name] + self._abitem.update_webif(_key, self._info_dict, True) + if self._agedelta < minagedelta: + text = "{0}: {1} because age delta '{2:.2f}' is lower than minagedelta '{3:.2f}'." + self._log_debug(text, self.name, actioninfo, self._agedelta, minagedelta) + self.update_webif_actionstatus(state, self._name, 'False', None, + f"(age delta '{self._agedelta:.2f}' < '{minagedelta:.2f})") + return True + else: + text = "{0}: Proceeding as age delta '{1:.2f}' is higher than minagedelta '{2:.2f}'." + self.update_webif_actionstatus(state, self._name, 'True', None, + f"(age delta '{self._agedelta:.2f}' > '{minagedelta:.2f})") + self._log_debug(text, self.name, self._agedelta, minagedelta) + return False + else: + return False + + def check_complete(self, state, check_item, check_status, check_mindelta, check_minagedelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} try: @@ -350,6 +397,11 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ if mindelta is not None: check_mindelta.set(mindelta) + if check_minagedelta.is_empty(): + minagedelta = StateEngineTools.find_attribute(self._sh, state, "se_minagedelta_" + self._name, 0, use) + if minagedelta is not None: + check_minagedelta.set(minagedelta) + if check_status is not None: check_value.set_cast(check_status.cast) check_mindelta.set_cast(check_status.cast) @@ -371,7 +423,7 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} - return check_item, check_status, check_mindelta, check_value, _issue + return check_item, check_status, check_mindelta, check_minagedelta, check_value, _issue # Execute action (considering delay, etc) # is_repeat: Indicate if this is a repeated action without changing the state @@ -526,7 +578,7 @@ def _update_repeat_webif(value: bool): except Exception: pass - actionname = "Action '{0}'".format(self._name) if delay == 0 else "Delayed Action ({0} seconds) '{1}'".format( + actionname = "Action '{0}'".format(self._name) if delay == 0 else "Delayed Action ({0} seconds) '{1}'.".format( delay, self._scheduler_name) _delay_info = 0 if delay is None: @@ -670,8 +722,9 @@ def write_to_logger(self): self._log_debug("item is not defined! Check log file.") item = None mindelta = self._mindelta.write_to_logger() or 0 + minagedelta = self._minagedelta.write_to_logger() or 0 value = self._value.write_to_logger() - self._info_dict.update({'item': item, 'mindelta': str(mindelta), 'delta': str(self._delta), 'value': str(value)}) + self._info_dict.update({'item': item, 'mindelta': str(mindelta), 'minagedelta': str(minagedelta), 'agedelta': str(self._agedelta), 'delta': str(self._delta), 'value': str(value)}) _key = [self._state.id, self._action_type, self._name] self._abitem.update_webif(_key, self._info_dict, True) return value @@ -684,8 +737,8 @@ def complete(self, state, action_type, evals_items=None, use=None): self._abitem.set_variable('current.state_name', state.name) self._action_type = action_type self._state = state - self._item, self._status, self._mindelta, self._value, _issue = self.check_complete( - state, self._item, self._status, self._mindelta, self._value, "set/force", evals_items, use) + self._item, self._status, self._mindelta, self._minagedelta, self._value, _issue = self.check_complete( + state, self._item, self._status, self._mindelta, self._minagedelta, self._value, "set/force", evals_items, use) self._action_status = _issue self._abitem.set_variable('current.action_name', '') @@ -729,7 +782,9 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: self._log_decrease_indent() return value - + minagedelta = self.eval_minagedelta(f"Not setting {self._item.property.path} to {value}", state) + if minagedelta: + return if not self._mindelta.is_empty(): mindelta = float(self._mindelta.get()) try: @@ -766,13 +821,22 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s def _force_set(self, actionname, item, value, source): pass + def update_mindelta(self, value): + if self._mindelta is None: + self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") + _, _, _issue, _ = self._mindelta.set(value) + _issue = {self._name: {'issue': _issue, 'attribute': 'mindelta', + 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + return _issue + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition, previous_condition, previousstate_condition, next_condition): self._log_decrease_indent() - self._log_debug("{0}: Set '{1}' to '{2}'{3}", actionname, item.property.path, value, repeat_text) + self._log_debug("{0}: Set '{1}' to '{2}'.{3}", actionname, item.property.path, value, repeat_text) pat = r"(?:[^,(]*)\'(.*?)\'" self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) + self._abitem.last_run = {self._name: datetime.datetime.now()} self._item = self._eval_item @@ -816,20 +880,20 @@ def _force_set(self, actionname, item, value, source): current_value = item() if current_value == value: if self._item._type == 'bool': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, not value) + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, not value) item(not value, caller=self._caller, source=source) elif self._item._type == 'str': if value != '': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, '') + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, '') item('', caller=self._caller, source=source) else: - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, '-') + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, '-') item('-', caller=self._caller, source=source) elif self._item._type == 'num': - self._log_debug("{0}: Set '{1}' to '{2}' (Force)", actionname, item.property.path, current_value+0.1) + self._log_debug("{0}: Set '{1}' to '{2}' (Force).", actionname, item.property.path, current_value+0.1) item(current_value+0.1, caller=self._caller, source=source) else: - self._log_warning("{0}: Force not implemented for item type '{1}'", actionname, item._type) + self._log_warning("{0}: Force not implemented for item type '{1}'.", actionname, item._type) else: self._log_debug("{0}: New value differs from old value, no force required.", actionname) @@ -884,12 +948,16 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._abitem.set_variable('current.action_name', namevar) if returnvalue: return value + minagedelta = self.eval_minagedelta(f"Not setting values by attribute {self.__byattr}", state) + if minagedelta: + return self._log_info("{0}: Setting values by attribute '{1}'.{2}", actionname, self.__byattr, repeat_text) self.update_webif_actionstatus(state, self._name, 'True') source = self.set_source(current_condition, previous_condition, previousstate_condition, next_condition) for item in self._sh.find_items(self.__byattr): self._log_info("\t{0} = {1}", item.property.path, item.conf[self.__byattr]) item(item.conf[self.__byattr], caller=self._caller, source=source) + self._abitem.last_run = {self._name: datetime.datetime.now()} # Class representing a single "se_trigger" action @@ -957,6 +1025,9 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s if returnvalue: return value + minagedelta = self.eval_minagedelta(f"Not triggering logic {self.__logic}", state) + if minagedelta: + return self._info_dict.update({'value': str(value)}) _key = [self._state.id, self._action_type, self._name] self._abitem.update_webif(_key, self._info_dict, True) @@ -964,6 +1035,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._log_info("{0}: Triggering logic '{1}' using value '{2}'.{3}", actionname, self.__logic, value, repeat_text) add_logics = 'logics.{}'.format(self.__logic) if not self.__logic.startswith('logics.') else self.__logic self._sh.trigger(add_logics, by=self._caller, source=self._name, value=value) + self._abitem.last_run = {self._name: datetime.datetime.now()} # Class representing a single "se_run" action @@ -1022,15 +1094,18 @@ def write_to_logger(self): def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: str = "", value=None, returnvalue=False, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): def log_conditions(): if current_condition: - self._log_debug("Running eval {0} based on conditionset {1}", self.__eval, current_condition) + self._log_debug("Running eval {0} based on conditionset {1}.", self.__eval, current_condition) if previous_condition: - self._log_debug("Running eval {0} based on previous conditionset {1}", self.__eval, previous_condition) + self._log_debug("Running eval {0} based on previous conditionset {1}.", self.__eval, previous_condition) if previousstate_condition: - self._log_debug("Running eval {0} based on previous state's conditionset {1}", self.__eval, + self._log_debug("Running eval {0} based on previous state's conditionset {1}.", self.__eval, previousstate_condition) if next_condition: - self._log_debug("Running eval {0} based on next conditionset {1}", self.__eval, next_condition) + self._log_debug("Running eval {0} based on next conditionset {1}.", self.__eval, next_condition) + minagedelta = self.eval_minagedelta(f"Not running eval {self.__eval}", state) + if minagedelta: + return self._abitem.set_variable('current.action_name', namevar) self._log_increase_indent() eval_result = '' @@ -1068,6 +1143,7 @@ def log_conditions(): self.update_webif_actionstatus(state, self._name, 'False', 'Problem calling: {}'.format(ex)) text = "{0}: Problem calling '{0}': {1}." self._log_error(text, actionname, StateEngineTools.get_eval_name(self.__eval), ex) + self._abitem.last_run = {self._name: datetime.datetime.now()} self._info_dict.update({'value': str(eval_result)}) _key = [self._state.id, self._action_type, self._name] self._abitem.update_webif(_key, self._info_dict, True) @@ -1137,6 +1213,9 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self._abitem.set_variable('current.action_name', namevar) if returnvalue: return None + minagedelta = self.eval_minagedelta(f"Not executing special action {self.__special}", state) + if minagedelta: + return try: _log_value = self.__value.property.path except Exception: @@ -1165,6 +1244,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'False', 'Unknown special value {}'.format(self.__special)) raise ValueError("{0}: Unknown special value '{1}'!".format(actionname, self.__special)) self._log_debug("Special action {0}: done", self.__special) + self._abitem.last_run = {self._name: datetime.datetime.now()} def suspend_get_value(self, value): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'suspend', 'action': 'suspend'}]}} diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 9c2d44a00..02d00391b 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -35,6 +35,8 @@ def __init__(self, abitem): self.__actions = {} self.__action_type = None self.__state = None + self.__unassigned_mindeltas = {} + self.__unassigned_minagedeltas = {} self.__unassigned_delays = {} self.__unassigned_repeats = {} self.__unassigned_instantevals = {} @@ -83,6 +85,22 @@ def update(self, attribute, value): else: _issue = self.__actions[name].update_delay(value) return _count, _issue + elif func == "se_mindelta": + # set mindelta + if name not in self.__actions: + # If we do not have the action yet (delay-attribute before action-attribute), ... + self.__unassigned_mindeltas[name] = value + else: + _issue = self.__actions[name].update_mindelta(value) + return _count, _issue + elif func == "se_minagedelta": + # set minagedelta + if name not in self.__actions: + # If we do not have the action yet (delay-attribute before action-attribute), ... + self.__unassigned_minagedeltas[name] = value + else: + _issue = self.__actions[name].update_minagedelta(value) + return _count, _issue elif func == "se_instanteval": # set instant calculation if name not in self.__actions: @@ -273,6 +291,18 @@ def __ensure_action_exists(self, func, name): _issue_list.append(_issue) del self.__unassigned_instantevals[name] + if name in self.__unassigned_mindeltas: + _issue = action.update_mindelta(self.__unassigned_mindeltas[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_mindeltas[name] + + if name in self.__unassigned_minagedeltas: + _issue = action.update_minagedelta(self.__unassigned_minagedeltas[name]) + if _issue: + _issue_list.append(_issue) + del self.__unassigned_minagedeltas[name] + if name in self.__unassigned_repeats: _issue = action.update_repeat(self.__unassigned_repeats[name]) if _issue: @@ -330,7 +360,7 @@ def remove_action(e): self._log_warning("Ignoring action {0} because: {1}", name, e) parameter = {'function': None, 'force': None, 'repeat': None, 'delay': 0, 'order': None, 'nextconditionset': None, 'conditionset': None, - 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None} + 'previousconditionset': None, 'previousstate_conditionset': None, 'mode': None, 'instanteval': None, 'mindelta': None, 'minagedelta': None} _issue = None _issue_list = [] # value_list needs to be string or list @@ -474,6 +504,14 @@ def remove_action(e): _issue = self.__actions[name].update_repeat(parameter['repeat']) if _issue: _issue_list.append(_issue) + if parameter['mindelta'] is not None: + _issue = self.__actions[name].update_mindelta(parameter['mindelta']) + if _issue: + _issue_list.append(_issue) + if parameter['minagedelta'] is not None: + _issue = self.__actions[name].update_minagedelta(parameter['minagedelta']) + if _issue: + _issue_list.append(_issue) if parameter['delay'] != 0: _issue = self.__actions[name].update_delay(parameter['delay']) if _issue: From 4a03688c6425fc416cb7e790b62432e7f5d33439 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Sep 2024 22:32:48 +0200 Subject: [PATCH 086/121] stateengine plugin: update docu --- stateengine/user_doc/06_aktionen.rst | 54 +++++++++++++++++++-- stateengine/user_doc/12_aktioneneinzeln.rst | 52 ++++++++++++++++++-- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst index 57343185f..c278d3752 100755 --- a/stateengine/user_doc/06_aktionen.rst +++ b/stateengine/user_doc/06_aktionen.rst @@ -18,8 +18,9 @@ definiert und benannt wurde. Die Herangehensweise ähnelt also stark der Deklara Zusätzlich zu ``se_item_`` lässt sich über den Eintrag ``se_mindelta_`` definieren, um welchen Wert -sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Im unten -stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta +sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Diese Konfiguration +kann für einzelne Aktionen individuell über die Angabe ``mindelta`` überschrieben werden. +Im unten stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta 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. @@ -300,6 +301,7 @@ ursprünglichen Zustands (regen) gesetzt werden soll, kann der Parameter ``insta .. code-block:: yaml + 'repeat: /' --> Ergebnis eines Eval-Ausdrucks oder eines Items 'repeat: [True|False]' Über das Attribut wird unabhängig vom globalen Setting für das @@ -393,7 +395,7 @@ regnet hingegen auf den Wert, der in den Settings hinterlegt ist. .. code-block:: yaml - previousconditionset: regex:enter_(.*)_test" + "previousconditionset: regex:enter_(.*)_test" Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden soll, wenn die vorherige Bedingungsgruppe des aktuellen Zustands mit dem angegebenen Ausdruck übereinstimmt. @@ -403,7 +405,7 @@ Die Abfrage erfolgt dabei nach den gleichen Regeln wie bei ``conditionset`` oben .. code-block:: yaml - previousstate_conditionset: regex:enter_(.*)_test" + "previousstate_conditionset: regex:enter_(.*)_test" Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden soll, wenn die Bedingungsgruppe, mit der der vorherige Zustand eingenommen wurde, mit dem angegebenen Ausdruck übereinstimmt. @@ -413,13 +415,55 @@ Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``condition .. code-block:: yaml - next_conditionset: regex:enter_(.*)_test" + "next_conditionset: regex:enter_(.*)_test" Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden soll, wenn die Bedingungsgruppe, mit der der zukünftige Zustand eingenommen wird, mit dem angegebenen Ausdruck übereinstimmt. Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben. Diese Angabe ist primär bei leave_actions sinnvoll. +**mindelta: ** + +Im folgenden Beispiel wird mindelta für eine einzelne Aktion gesetzt. Anstatt also eine minimale Änderung +für alle Aktionen mit bestimmtem Namen festzulegen, wird eine einzelne Aktion nur dann ausgeführt, +wenn sich der Wert um mindestens den angegeben Wert geändert hat. +Wird 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. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + 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 + Daemmerung: + <...> + se_action_height: + - 'function: set' + - 'to: value:100' + se_action_lamella: + - 'function: set' + - 'to: value:25' + - 'mindelta: 10' + <...> + +**minagedelta: ** + +.. code-block:: yaml + + minagedelta: 300 + +Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird. +Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens +5 Minuten zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird. +Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem +Betrieb gespeichert und überdauert einen Neustart nicht. + + Templates für Aktionen ---------------------- diff --git a/stateengine/user_doc/12_aktioneneinzeln.rst b/stateengine/user_doc/12_aktioneneinzeln.rst index 9363459ae..cb00bf9dc 100755 --- a/stateengine/user_doc/12_aktioneneinzeln.rst +++ b/stateengine/user_doc/12_aktioneneinzeln.rst @@ -150,8 +150,50 @@ Es ist möglich, eine Minimumabweichung für aktuellen Wert des Items und dem ermittelten neuen Wert kleiner ist als die festgelegte Minimumabweichung wird keine Änderung vorgenommen. Die Minimumabweichung wird über das Attribut -``se_mindelta_`` auf der Ebene des Regelwerk-Items -festgelegt. +``se_mindelta_`` auf der Ebene des Regelwerk-Items für alle +Aktionen mit dem entsprechenden Namen festgelegt. Alternativ kann dieses +Attribut aber auch für einzelne Aktionen festgelegt und/oder überschrieben werden. + +Im angeführten Beispiel werden die Lamellen beim Eintritt in einen Zustand und +beim Verlassen des Zustands nur dann gesetzt, wenn sich der Wert der Lamellen inzwischen +um mindestens 10 verändert hat. Wird der Zustand erneut eingenommen (stay), wird der +Wert hingegen mit 5 überschrieben. + +.. code-block:: yaml + + #items/item.yaml + raffstore1: + automatik: + struct: stateengine.general + 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 # Alle Aktionen mit Namen lamella sind betroffen + Daemmerung: + on_enter: + se_set_height: 100 + se_set_lamella: 20 + on_leave: + se_set_height: 100 + se_set_lamella: 15 + on_stay: + se_set_height: 100 + se_set_lamella: 20 + se_set_lamella: 20 + se_mindelta_lamella: 5 + +**minagedelta: ** + +.. code-block:: yaml + + se_minagedelta_: 300 (Sekunden)|30m (Minuten) + +Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird. +Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens +300 Sekunden zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird. +Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem +Betrieb gespeichert und überdauert einen Neustart nicht. **delay: ** @@ -167,8 +209,10 @@ Der Timer zur Ausführung der Aktion nach der angegebenen Verzögerung wird entfernt, wenn eine gleichartige Aktion ausgeführt werden soll (egal ob verzögert oder nicht). Wenn also die Verzögerung größer als der ``cycle`` ist, wird die Aktion -nie durchgeführt werden, es sei denn die Aktion soll nur -einmalig ausgeführt werden. +nicht mehr durchgeführt. +Außerdem ist es möglich, den Timer bewusst abzubrechen, ohne eine Aktion auszuführen, +indem der Delay auf -1 gesetzt wird. Dies macht insbesondere beim Verlassen von +Zuständen Sinn, um ungewünschte verzögerte Aktionen vom "alten" Zustand zu verhindern. **repeat: ** From e24844f0d32142ecc1010724fde723f4e64588d0 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Sep 2024 22:35:00 +0200 Subject: [PATCH 087/121] stateengine plugin: simplify log entry when having issues with value casting --- stateengine/StateEngineValue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index aeb4b22b4..0e14b1296 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -634,7 +634,7 @@ def __do_cast(self, value, item_id=None): _newvalue = element if element == 'novalue' else self.__cast_func(element) except Exception as ex: _newvalue = None - _issue = "Problem casting element '{0}' to {1}: {2}.".format(element, self.__cast_func, ex) + _issue = "Problem casting element '{0}': {1}.".format(element, ex) self._log_warning(_issue) valuelist.append(_newvalue) if element in self.__listorder: @@ -664,7 +664,7 @@ def __do_cast(self, value, item_id=None): _issue = "You most likely forgot to prefix your expression with 'eval:'" raise ValueError(_issue) else: - _issue = "Not possible to cast '{}' because {}".format(value, ex) + _issue = "{}".format(ex) raise ValueError(_issue) if value in self.__listorder: self.__listorder[self.__listorder.index(value)] = _newvalue From b07e9e722525b73806d3c53dc7cc07201aea0c8a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Sep 2024 22:37:23 +0200 Subject: [PATCH 088/121] stateengine plugin: highly improve and simplify issue finder and logger at startup --- stateengine/StateEngineAction.py | 164 ++++++++++++++---------------- stateengine/StateEngineActions.py | 39 +++++-- stateengine/StateEngineItem.py | 97 +++++++++++++----- stateengine/StateEngineState.py | 105 ++++++++++--------- 4 files changed, 224 insertions(+), 181 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index ccd6a29ef..04679281e 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -45,11 +45,13 @@ def action_status(self): # Cast function for delay # value: value to cast @staticmethod - def __cast_delay(value): + def __cast_seconds(value): if isinstance(value, str): delay = value.strip() if delay.endswith('m'): return int(delay.strip('m')) * 60 + elif delay.endswith('h'): + return int(delay.strip('h')) * 3600 else: return int(delay) elif isinstance(value, int): @@ -91,86 +93,80 @@ def __init__(self, abitem, name: str): self._state = None self._info_dict = {} - def update_delay(self, value): + def update_action_details(self, state, action_type): + if self._action_type is None: + self._action_type = action_type + if self._state is None: + self._state = state + self._log_develop("Updating state for action {} to {}, action type {}", self._name, state.id, action_type) + + def _update_value(self, value_type, value, attribute, cast=None): _issue_list = [] - _, _, _issue, _ = self.__delay.set(value) - if _issue: - _issue = {self._name: {'issue': _issue, 'attribute': 'delay', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - _issue_list.append(_issue) - _issue = self.__delay.set_cast(SeActionBase.__cast_delay) + if value_type is None: + return _issue_list + _, _, _issue, _ = value_type.set(value) if _issue: - _issue = {self._name: {'issue': _issue, 'attribute': 'delay', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': _issue, 'attribute': [attribute], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} _issue_list.append(_issue) + if cast == 'seconds': + _issue = value_type.set_cast(SeActionBase.__cast_seconds) + if _issue: + _issue = {self._name: {'issue': _issue, 'attribute': [attribute], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} + _issue_list.append(_issue) _issue_list = StateEngineTools.flatten_list(_issue_list) return _issue_list + def update_delay(self, value): + _issue = self._update_value(self.__delay, value, 'delay', 'seconds') + return _issue + def update_instanteval(self, value): - if self.__instanteval is None: - self.__instanteval = StateEngineValue.SeValue(self._abitem, "instanteval", False, "bool") - _, _, _issue, _ = self.__instanteval.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'instanteval', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__instanteval, value, 'instanteval') return _issue def update_mindelta(self, value): - self._log_warning("Mindelta is only relevant for set (force) actions - ignoring") - _issue = {self._name: {'issue': 'Mindelta not relevant for this action type', 'attribute': 'mindelta', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._log_warning("Mindelta is only relevant for set (force) actions - ignoring {}", value) + _issue = {self._name: {'issue': 'Mindelta not relevant for this action type', 'attribute': ['mindelta'], + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue def update_minagedelta(self, value): if self._minagedelta is None: self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta", False, "num") - _, _, _issue, _ = self._minagedelta.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'minagedelta', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self._minagedelta, value, 'minagedelta', 'seconds') return _issue def update_repeat(self, value): if self.__repeat is None: self.__repeat = StateEngineValue.SeValue(self._abitem, "repeat", False, "bool") - _, _, _issue, _ = self.__repeat.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'repeat', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__repeat, value, 'repeat') return _issue def update_order(self, value): - _, _, _issue, _ = self.__order.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'order', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.__order, value, 'order') return _issue def update_nextconditionset(self, value): - _, _, _issue, _ = self.nextconditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'nextconditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.nextconditionset, value, 'nextconditionset') return _issue def update_conditionset(self, value): - _, _, _issue, _ = self.conditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'conditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.conditionset, value, 'conditionset') return _issue def update_previousconditionset(self, value): - _, _, _issue, _ = self.previousconditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'previousconditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.previousconditionset, value, 'previousconditionset') return _issue def update_previousstate_conditionset(self, value): - _, _, _issue, _ = self.previousstate_conditionset.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'previousstate_conditionset', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self.previousstate_conditionset, value, 'previousstate_conditionset') return _issue def update_mode(self, value): - _value, _, _issue, _ = self.__mode.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'mode', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} - return _value[0], _issue + _issue = self._update_value(self.__mode, value, 'mode') + return _issue def get_order(self): order = self.__order.get(1) @@ -261,7 +257,7 @@ def set_source(self, current_condition, previous_condition, previousstate_condit # newly evaluated mindelta # Any issue that might have occured as a dict def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=None): - _issue = {self._name: {'issue': None, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': None, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} if isinstance(check_item, str): item = None #self._log_develop("Get item from eval on {} {}", self._function, check_item) @@ -276,21 +272,21 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No "plain eval expression without a preceeding eval. "\ "Please update your config of {}" _issue = { - self._name: {'issue': _text.format(check_item), 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._name: {'issue': _text.format(check_item), 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._log_warning(_text, check_item) _, _, item = item.partition(":") elif re.match(r'^.*:', item): _text = "se_eval/item attributes have to be plain eval expression. Please update your config of {}" _issue = { self._name: {'issue': _text.format(check_item), - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._log_warning(_text, check_item) _, _, item = item.partition(":") item = eval(item) if item is not None: check_item, _issue = self._abitem.return_item(item) _issue = { - self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + self._name: {'issue': _issue, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} if check_value: check_value.set_cast(check_item.cast) if check_mindelta: @@ -302,19 +298,19 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No else: self._log_develop("Got no item from eval on {} with initial item {}", self._function, item) except Exception as ex: - _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': ex, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} # raise Exception("Problem evaluating item '{}' from eval: {}".format(check_item, ex)) self._log_error("Problem evaluating item '{}' from eval: {}", check_item, ex) check_item = None if item is None: _issue = {self._name: {'issue': ['Item {} from eval not existing'.format(check_item)], - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} # raise Exception("Problem evaluating item '{}' from eval. It does not exist.".format(check_item)) self._log_error("Problem evaluating item '{}' from eval. It does not exist", check_item) check_item = None elif check_item is None: _issue = {self._name: {'issue': ['Item is None'], - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return check_item, check_value, check_mindelta, _issue def eval_minagedelta(self, actioninfo, state): @@ -609,7 +605,7 @@ def update(self, value): # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): + def complete(self, evals_items=None, use=None): raise NotImplementedError("Class {} doesn't implement complete()".format(self.__class__.__name__)) # Check if execution is possible @@ -683,7 +679,7 @@ def __init__(self, abitem, name: str): self._status = None self._delta = 0 self._value = StateEngineValue.SeValue(self._abitem, "value") - self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") + self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") def _getitem_fromeval(self): if self._item is None: @@ -699,7 +695,7 @@ def _getitem_fromeval(self): # value: Value of the set_(action_name) attribute def update(self, value): _, _, _issue, _ = self._value.set(value) - _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': _issue, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue def write_to_logger(self): @@ -731,14 +727,12 @@ def write_to_logger(self): # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self._log_develop('Completing action {}', self._name) + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) self._abitem.set_variable('current.action_name', self._name) - self._abitem.set_variable('current.state_name', state.name) - self._action_type = action_type - self._state = state + self._abitem.set_variable('current.state_name', self._state.name) self._item, self._status, self._mindelta, self._minagedelta, self._value, _issue = self.check_complete( - state, self._item, self._status, self._mindelta, self._minagedelta, self._value, "set/force", evals_items, use) + self._state, self._item, self._status, self._mindelta, self._minagedelta, self._value, "set/force", evals_items, use) self._action_status = _issue self._abitem.set_variable('current.action_name', '') @@ -824,9 +818,7 @@ def _force_set(self, actionname, item, value, source): def update_mindelta(self, value): if self._mindelta is None: self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") - _, _, _issue, _ = self._mindelta.set(value) - _issue = {self._name: {'issue': _issue, 'attribute': 'mindelta', - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = self._update_value(self._mindelta, value, 'mindelta') return _issue def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition, previous_condition, previousstate_condition, next_condition): @@ -916,20 +908,18 @@ def __repr__(self): def update(self, value): self.__byattr = value _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self._log_develop('Completing action {}', self._name) + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) self._abitem.set_variable('current.action_name', self._name) - self._abitem.set_variable('current.state_name', state.name) - self._action_type = action_type - self._state = state + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeByAttrDelayTimer".format(self.__byattr) _issue = {self._name: {'issue': None, 'attribute': self.__byattr, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._abitem.set_variable('current.action_name', '') self._abitem.set_variable('current.state_name', '') return _issue @@ -982,20 +972,18 @@ def update(self, value): value = None if value == "" else value _, _, _issue, _ = self.__value.set(value) _issue = {self._name: {'issue': _issue, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self._log_develop('Completing action {}', self._name) + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) self._abitem.set_variable('current.action_name', self._name) - self._abitem.set_variable('current.state_name', state.name) - self._action_type = action_type - self._state = state + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeLogicDelayTimer".format(self.__logic) _issue = {self._name: {'issue': None, 'logic': self.__logic, - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._abitem.set_variable('current.action_name', '') self._abitem.set_variable('current.state_name', '') return _issue @@ -1062,20 +1050,18 @@ def update(self, value): if func == "eval": self.__eval = value _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self._log_develop('Completing action {}', self._name) + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) self._abitem.set_variable('current.action_name', self._name) - self._abitem.set_variable('current.state_name', state.name) - self._action_type = action_type - self._state = state + self._abitem.set_variable('current.state_name', self._state.name) self._scheduler_name = "{}-SeRunDelayTimer".format(StateEngineTools.get_eval_name(self.__eval)) _issue = {self._name: {'issue': None, 'eval': StateEngineTools.get_eval_name(self.__eval), - 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._abitem.set_variable('current.action_name', '') self._abitem.set_variable('current.state_name', '') return _issue @@ -1174,23 +1160,21 @@ def update(self, value): else: raise ValueError("Action {0}: Unknown special value '{1}'!".format(self._name, special)) self.__special = special - _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': None, 'special': self.__value, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} return _issue # Complete action # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self._log_develop('Completing action {}', self._name) + def complete(self, evals_items=None, use=None): + self._log_develop('Completing action {}, action type {}, state {}', self._name, self._action_type, self._state) self._abitem.set_variable('current.action_name', self._name) - self._abitem.set_variable('current.state_name', state.name) - self._action_type = action_type - self._state = state + self._abitem.set_variable('current.state_name', self._state.name) if isinstance(self.__value, list): item = self.__value[0].property.path else: item = self.__value.property.path self._scheduler_name = "{}_{}-SeSpecialDelayTimer".format(self.__special, item) - _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': 'unknown', 'action': self._function}]}} + _issue = {self._name: {'issue': None, 'special': item, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} self._abitem.set_variable('current.action_name', '') self._abitem.set_variable('current.state_name', '') return _issue diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 02d00391b..92cf17fcb 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -60,6 +60,13 @@ def reset(self): def count(self): return len(self.__actions) + def update_action_details(self, state, action_type): + if self.__action_type is None: + self.__action_type = action_type + if self.__state is None: + self._log_develop("Updating state for actions: {}, action type: {}", state.id, action_type) + self.__state = state + # update action # attribute: name of attribute that defines action # value: value of the attribute @@ -77,6 +84,8 @@ def update(self, attribute, value): value = ":".join(map(str.strip, value.split(":"))) if value[:1] == '[' and value[-1:] == ']': value = StateEngineTools.convert_str_to_list(value, False) + if name in self.__actions: + self.__actions[name].update_action_details(self.__state, self.__action_type) if func == "se_delay": # set delay if name not in self.__actions: @@ -253,6 +262,7 @@ def __ensure_action_exists(self, func, name): # Check if action exists _issue = None if name in self.__actions: + self.__actions[name].update_action_details(self.__state, self.__action_type) return True, _issue # Create action depending on function @@ -279,6 +289,7 @@ def __ensure_action_exists(self, func, name): else: return False, _issue _issue_list = [] + action.update_action_details(self.__state, self.__action_type) if name in self.__unassigned_delays: _issue = action.update_delay(self.__unassigned_delays[name]) if _issue: @@ -416,6 +427,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['to']) elif parameter['function'] == "force": _action_exists, _issue = self.__ensure_action_exists("se_force", name) @@ -423,6 +435,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'to') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['to']) elif parameter['function'] == "run": _action_exists, _issue = self.__ensure_action_exists("se_run", name) @@ -430,6 +443,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'eval') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['eval']) elif parameter['function'] == "byattr": _action_exists, _issue = self.__ensure_action_exists("se_byattr", name) @@ -437,6 +451,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'attribute') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['attribute']) elif parameter['function'] == "trigger": _action_exists, _issue = self.__ensure_action_exists("se_trigger", name) @@ -444,6 +459,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'logic') + self.__actions[name].update_action_details(self.__state, self.__action_type) if 'value' in parameter and parameter['value'] is not None: self.__actions[name].update(parameter['logic'] + ':' + parameter['value']) else: @@ -454,6 +470,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "add": _action_exists, _issue = self.__ensure_action_exists("se_add", name) @@ -461,6 +478,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "remove": _action_exists, _issue = self.__ensure_action_exists("se_remove", name) @@ -468,6 +486,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removeall": _action_exists, _issue = self.__ensure_action_exists("se_removeall", name) @@ -475,6 +494,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removefirst": _action_exists, _issue = self.__ensure_action_exists("se_removefirst", name) @@ -482,6 +502,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) elif parameter['function'] == "removelast": _action_exists, _issue = self.__ensure_action_exists("se_removelast", name) @@ -489,6 +510,7 @@ def remove_action(e): _issue_list.append(_issue) if _action_exists: self.__raise_missing_parameter_error(parameter, 'value') + self.__actions[name].update_action_details(self.__state, self.__action_type) self.__actions[name].update(parameter['value']) except ValueError as ex: @@ -545,6 +567,7 @@ def remove_action(e): _issue_list.append(_issue) if _action: self.__actions[name] = _action + self._log_debug("Handle combined issuelist {}", _issue_list) return _issue_list # noinspection PyMethodMayBeStatic @@ -555,20 +578,16 @@ def __raise_missing_parameter_error(self, parameter, param_name): # Check the actions optimize and complete them # state: state (item) to read from - def complete(self, state, action_type, evals_items=None, use=None): - self.__action_type = action_type - self.__state = state - + def complete(self, evals_items=None, use=None): _status = {} - if use is None: - use = state.use.get() + if not self.__actions: + return _status for name in self.__actions: try: - _status.update(self.__actions[name].complete(state, action_type, evals_items, use)) + _status.update(self.__actions[name].complete(evals_items, use)) except ValueError as ex: - _status.update({name: {'issue': ex, 'issueorigin': {'state': state.id, 'action': 'unknown'}}}) - raise ValueError("Completing State '{0}', Action '{1}': {2}".format(state.id, name, ex)) - self._log_debug("Completing {} for state {} status {}", self.__actions, state, _status) + _status.update({name: {'issue': ex, 'issueorigin': {'state': self.__state.id, 'action': 'unknown'}}}) + raise ValueError("Completing State '{0}', Action '{1}': {2}".format(self.__state.id, name, ex)) return _status def set(self, value): diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 829f3a53a..40a9f32d2 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -331,7 +331,7 @@ def __init__(self, smarthome, item, se_plugin): self.__templates = {} self.__unused_attributes = {} self.__used_attributes = {} - self.__action_status = {} + self.__action_status = {"enter": {}, "enter_or_stay": {}, "stay": {}, "pass": {}, "leave": {}} self.__state_issues = {} self.__struct_issues = {} self.__webif_infos = OrderedDict() @@ -430,9 +430,21 @@ def show_issues_summary(self): if self.__unused_attributes: issues += 1 self.__log_issues('attributes') - if self.__action_status: + if self.__action_status['enter']: issues += 1 - self.__log_issues('actions') + self.__log_issues('actions_enter') + if self.__action_status['enter_or_stay']: + issues += 1 + self.__log_issues('actions_enter_or_stay') + if self.__action_status['stay']: + issues += 1 + self.__log_issues('actions_stay') + if self.__action_status['leave']: + issues += 1 + self.__log_issues('actions_leave') + if self.__action_status['pass']: + issues += 1 + self.__log_issues('actions_pass') if self.__state_issues: issues += 1 self.__log_issues('states') @@ -1071,19 +1083,31 @@ def _nested_test(dic, keys): def update_action_status(self, action_status): def combine_dicts(dict1, dict2): - combined = dict1.copy() - for key, value in dict2.items(): - if key in combined: - for k, v in combined.items(): - v['issueorigin'].extend( - [item for item in v['issueorigin'] if item not in combined[k]['issueorigin']]) - v['issue'].extend([item for item in v['issue'] if item not in combined[k]['issue']]) + combined = copy.deepcopy(dict1) + for action_type, action_dict in dict2.items(): + if action_type in combined: + # Merge the inner dictionary for this action_type + for key, value in action_dict.items(): + if key in combined[action_type]: + combined[action_type][key]['issueorigin'].extend( + [item for item in value['issueorigin'] if + item not in combined[action_type][key]['issueorigin']] + ) + combined[action_type][key]['issue'].extend( + [item for item in value['issue'] if item not in combined[action_type][key]['issue']] + ) + else: + # Add new key at the inner level if it doesn't exist + combined[action_type][key] = value else: - combined[key] = value + # Add the entire action_type dictionary if it's not in combined + combined[action_type] = action_dict + return combined - combined_dict = combine_dicts(action_status, self.__action_status) + combined_dict = combine_dicts(copy.deepcopy(action_status), copy.deepcopy(self.__action_status)) self.__action_status = combined_dict + del combined_dict def update_issues(self, issue_type, issues): def combine_dicts(dict1, dict2): @@ -1155,7 +1179,7 @@ def update_attributes(self, unused_attributes, used_attributes): self.__used_attributes = combined_dict def __log_issues(self, issue_type): - def print_readable_dict(data): + def print_readable_dict(attr, data): for key, value in data.items(): if isinstance(value, list): formatted_entries = [] @@ -1169,46 +1193,64 @@ def print_readable_dict(data): else: formatted_entries.append(item) if formatted_entries: - self.__logger.info("- {}: {}", key, ', '.join(formatted_entries)) + self.__logger.info("- {}{}: {}", attr, key, ', '.join(formatted_entries)) else: - self.__logger.info("- {}: {}", key, value) + self.__logger.info("- {}{}: {}", attr, key, value) def list_issues(v): _issuelist = StateEngineTools.flatten_list(v.get('issue')) + _attrlist = StateEngineTools.flatten_list(v.get('attribute')) if isinstance(_issuelist, list) and len(_issuelist) > 1: self.__logger.info("has the following issues:") self.__logger.increase_indent() - for e in _issuelist: + for i, e in enumerate(_issuelist): + _attr = "" if _attrlist is None or not _attrlist[i] else "attribute {}: ".format(_attrlist[i]) if isinstance(e, dict): - print_readable_dict(e) + print_readable_dict(_attr, e) else: - self.__logger.info("- {}", e) + self.__logger.info("- {}{}", _attr, e) self.__logger.decrease_indent() elif isinstance(_issuelist, list) and len(_issuelist) == 1: if isinstance(_issuelist[0], dict): self.__logger.info("has the following issues:") self.__logger.increase_indent() - print_readable_dict(_issuelist[0]) + _attr = "" if _attrlist is None or not _attrlist[0] else "attribute {}: ".format(_attrlist[0]) + print_readable_dict(_attr, _issuelist[0]) self.__logger.decrease_indent() else: - self.__logger.info("has the following issue: {}", _issuelist[0]) + _attr = "" if _attrlist is None or not _attrlist[0] else " for attribute {}".format(_attrlist[0]) + self.__logger.info("has the following issue{}: {}", _attr, _issuelist[0]) else: if isinstance(_issuelist, dict): self.__logger.info("has the following issues:") self.__logger.increase_indent() - print_readable_dict(_issuelist) + _attr = "" if not _attrlist else "attribute {}: ".format(_attrlist) + print_readable_dict(_attr, _issuelist) self.__logger.decrease_indent() else: - self.__logger.info("has the following issue: {}", _issuelist) + _attr = "" if not _attrlist else " for attribute {}".format(_attrlist) + self.__logger.info("has the following issue{}: {}", _attr, _issuelist) if "ignore" in v: self.__logger.info("It will be ignored") warn_unused = "" warn_issues = "" warn = "" - if issue_type == 'actions': - to_check = self.__action_status.items() - warn = ', '.join(key for key in self.__action_status.keys()) + if issue_type == 'actions_enter': + to_check = self.__action_status['enter'].items() + warn = ', '.join(key for key in self.__action_status['enter'].keys()) + elif issue_type == 'actions_enter_or_stay': + to_check = self.__action_status['enter_or_stay'].items() + warn = ', '.join(key for key in self.__action_status['enter_or_stay'].keys()) + elif issue_type == 'actions_stay': + to_check = self.__action_status['stay'].items() + warn = ', '.join(key for key in self.__action_status['stay'].keys()) + elif issue_type == 'actions_pass': + to_check = self.__action_status['pass'].items() + warn = ', '.join(key for key in self.__action_status['pass'].keys()) + elif issue_type == 'actions_leave': + to_check = self.__action_status['leave'].items() + warn = ', '.join(key for key in self.__action_status['leave'].keys()) elif issue_type == 'structs': to_check = self.__struct_issues.items() warn = ', '.join(key for key in self.__struct_issues.keys()) @@ -1267,9 +1309,8 @@ def list_issues(v): self.__logger.info("Definition {}{}", entry, additional) self.__logger.increase_indent() for origin in origin_list: - if issue_type == 'actions': - origin_text = 'state {}, action {}, on_{}'.format(origin.get('state'), origin.get('action'), - origin.get('type')) + if issue_type.startswith('actions_'): + origin_text = 'state {}, action {}'.format(origin.get('state'), origin.get('action')) elif issue_type == 'states': if origin.get('condition') == 'GeneralError' and len(origin_list) == 1: origin_text = 'there was a general error. The state' diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 4695ceaf3..905a969f8 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -28,7 +28,7 @@ from lib.item import Items from lib.item.item import Item -from copy import copy +from copy import copy, deepcopy # Class representing an object state, consisting of name, conditions to be met and configured actions for state @@ -180,7 +180,7 @@ def __init__(self, abitem, item_state): self.__name = '' self.__unused_attributes = {} self.__used_attributes = {} - self.__action_status = {} + self.__action_status = {"enter": {}, "enter_or_stay": {}, "stay": {}, "pass": {}, "leave": {}} self.__use_done = [] self.__use_list = [] self.__use_ignore_list = [] @@ -574,50 +574,47 @@ def update_unused(used_attributes, attrib_type, attrib_name): used_attributes[nested_entry].update(nested_dict) self.__used_attributes.update(used_attributes) - def update_action_status(action_status, actiontype): + def update_action_status(actn_type, action_status): + def filter_issues(input_dict): + return { + key: {sub_key: sub_value for sub_key, sub_value in value.items() if + sub_value.get('issue') not in (None, [], [None])} + for key, value in input_dict.items() + } + if action_status is None: return action_status = StateEngineTools.flatten_list(action_status) if isinstance(action_status, list): for e in action_status: - update_action_status(e, actiontype) + update_action_status(actn_type, e) return for itm, dct in action_status.items(): - if itm not in self.__action_status: - self.__action_status.update({itm: dct}) + if itm not in self.__action_status[actn_type]: + self.__action_status[actn_type].update({itm: dct}) for (itm, dct) in action_status.items(): issues = dct.get('issue') + attributes = dct.get('attribute') if issues: if isinstance(issues, list): - self.__action_status[itm]['issue'].extend( - [issue for issue in issues if issue not in self.__action_status[itm]['issue']]) - origin_list = self.__action_status[itm].get('issueorigin', []) - new_list = origin_list.copy() - for i, listitem in enumerate(origin_list): - entry_unknown = {'state': 'unknown', 'action': listitem.get('action')} - entry_unknown2 = {'state': 'unknown', 'action': 'unknown'} - entry_notype = {'state': self.id, 'action': listitem.get('action')} - entry_final = {'state': self.id, 'action': listitem.get('action'), 'type': actiontype} - - if listitem in (entry_unknown, entry_unknown2, entry_notype): - new_list[i] = entry_final - elif entry_final not in origin_list: - new_list.append(entry_final) - - self.__action_status[itm]['issueorigin'] = new_list - - filtered_dict = {} - for key, nested_dict in self.__action_status.items(): - filtered_dict.update({key: {}}) - filtered_dict[key].update({'used in': actiontype}) - filtered_dict[key].update(nested_dict) - #self._log_develop("Add {} to used {}", key, filtered_dict) - self.__used_attributes = copy(filtered_dict) - filtered_dict = {key: value for key, value in self.__action_status.items() - if value.get('issue') not in [[], [None], None]} - self.__action_status = filtered_dict - #self._log_develop("Updated action status: {}, updated used {}", self.__action_status, self.__used_attributes) + for i, issue in enumerate(issues): + if issue not in self.__action_status[actn_type][itm]['issue']: + self.__action_status[actn_type][itm]['issue'].append(issue) + self.__action_status[actn_type][itm]['attribute'].append(attributes[i]) + + flattened_dict = {} + for key, action_type_dict in self.__action_status.items(): + # Iterate through the inner dictionaries + for inner_key, nested_dict in action_type_dict.items(): + # Initialize the entry in the flattened dictionary + if inner_key not in flattened_dict: + flattened_dict[inner_key] = {} + # Add 'used in' and update with existing data + flattened_dict[inner_key]['used in'] = key + flattened_dict[inner_key].update(nested_dict) + self.__used_attributes = deepcopy(flattened_dict) + self.__action_status = filter_issues(self.__action_status) if isinstance(state, SeState): item_state = state.state_item @@ -686,21 +683,22 @@ def update_action_status(action_status, actiontype): child_name = StateEngineTools.get_last_part_of_item_id(child_item) try: action_mapping = { - "on_enter": ("enter", self.__actions_enter), - "on_stay": ("stay", self.__actions_stay), - "on_enter_or_stay": ("enter_or_stay", self.__actions_enter_or_stay), - "on_leave": ("leave", self.__actions_leave), - "on_pass": ("pass", self.__actions_pass) + "on_enter": ("enter", "actions_enter", self.__actions_enter), + "on_stay": ("stay", "actions_stay", self.__actions_stay), + "on_enter_or_stay": ("enter_or_stay", "actions_enter_or_stay", self.__actions_enter_or_stay), + "on_leave": ("leave", "actions_leave", self.__actions_leave), + "on_pass": ("pass", "actions_pass", self.__actions_pass) } if child_name in action_mapping: - action_name, action_method = action_mapping[child_name] + action_name, action_type, action_method = action_mapping[child_name] for attribute in child_item.conf: self._log_develop("Filling state with {} action named {} for state {} with config {}", child_name, attribute, state.id, child_item.conf) + action_method.update_action_details(self, action_type) _action_counts[action_name] += 1 _, _action_status = action_method.update(attribute, child_item.conf.get(attribute)) if _action_status: - update_action_status(_action_status, action_name) + update_action_status(action_name, _action_status) self._abitem.update_action_status(self.__action_status) update_unused(_used_attributes, 'action', child_name) @@ -710,11 +708,12 @@ def update_action_status(action_status, actiontype): self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) # Actions defined directly in the item go to "enter_or_stay" for attribute in item_state.conf: - _result = self.__actions_enter_or_stay.update(attribute, item_state.conf[attribute]) + self.__actions_enter_or_stay.update_action_details(self, "actions_enter_or_stay") + _result = self.__actions_enter_or_stay.update(attribute, item_state.conf.get(attribute)) _action_counts["enter_or_stay"] += _result[0] if _result else 0 _action_status = _result[1] if _action_status: - update_action_status(_action_status, 'enter_or_stay') + update_action_status("enter_or_stay", _action_status) self._abitem.update_action_status(self.__action_status) _total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] @@ -724,25 +723,25 @@ def update_action_status(action_status, actiontype): if recursion_depth == 0: self.__conditionsets.complete(self, use) - _action_status = self.__actions_enter.complete(self, 'actions_enter', self.__conditionsets.evals_items, use) + _action_status = self.__actions_enter.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'enter') + update_action_status("enter", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_stay.complete(self, 'actions_stay', self.__conditionsets.evals_items, use) + _action_status = self.__actions_stay.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'stay') + update_action_status("stay", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_enter_or_stay.complete(self, 'actions_enter_or_stay', self.__conditionsets.evals_items, use) + _action_status = self.__actions_enter_or_stay.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'enter_or_stay') + update_action_status("enter_or_stay", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_pass.complete(self, 'actions_pass', self.__conditionsets.evals_items, use) + _action_status = self.__actions_pass.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'pass') + update_action_status("pass", _action_status) self._abitem.update_action_status(self.__action_status) - _action_status = self.__actions_leave.complete(self, 'actions_leave', self.__conditionsets.evals_items, use) + _action_status = self.__actions_leave.complete(self.__conditionsets.evals_items, use) if _action_status: - update_action_status(_action_status, 'leave') + update_action_status("leave", _action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_action_status(self.__action_status) self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) From 7d65df66488861cf15e8d10bdbcb3b57cfa6a34c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Sep 2024 22:38:30 +0200 Subject: [PATCH 089/121] stateengine plugin: improve struct and add se_minagedelta to plugin.yaml --- stateengine/plugin.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index 0d1ea337a..a0505d4f6 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -947,15 +947,19 @@ item_structs: se_action_suspend: - 'function: set' - 'to: False' + - 'repeat: False' se_action_suspend_visu: - 'function: set' - 'to: False' + - 'repeat: False' se_action_suspend_end: - 'function: set' - 'to: ' + - 'repeat: False' se_action_suspend_start: - 'function: set' - 'to: ' + - 'repeat: False' on_leave: se_action_suspend: @@ -1276,15 +1280,19 @@ item_structs: se_action_suspend: - 'function: set' - 'to: False' + - 'repeat: False' se_action_suspend_visu: - 'function: set' - 'to: False' + - 'repeat: False' se_action_suspend_end: - 'function: set' - 'to: ' + - 'repeat: False' se_action_suspend_start: - 'function: set' - 'to: ' + - 'repeat: False' on_leave: se_action_suspendvariant: @@ -1469,6 +1477,12 @@ item_attribute_prefixes: de: 'Definiert um welchen Wert sich das Item mindestens geändert haben muss, um neu gesetzt zu werden' en: 'Definition of a threshold the item has to surpass to be newly set' + se_minagedelta_: + type: foo + description: + de: 'Definiert eine Pause zwischen mehreren Aktionsaufrufen.' + en: 'Definition of pause interval between multiple runs of actions' + se_min_: type: foo description: @@ -1482,6 +1496,7 @@ item_attribute_prefixes: en: 'Condition: The value of the item has to be lower than defined by se_max' se_value_: + type: foo description: de: 'Bedingung: Das Item muss exakt dem durch se_value angegebenem Wert entsprechen' en: 'Condition: The item has to have the exact same value as defined by se_value' @@ -1559,11 +1574,13 @@ item_attribute_prefixes: en: 'Special action like suspend or retrigger' se_set_: + type: foo description: de: 'Setzen eines Items auf einen bestimmten Wert (veraltet - Nutze stattdessen se_action)' en: 'Setting an item to a specific value (deprecated - use se_action instead)' se_force_: + type: foo description: de: 'Setzen eines Items auf einen bestimmten Wert, egal ob sich der Wert geändert hat oder nicht (veraltet - Nutze stattdessen se_action)' en: 'Setting an item to a specific value no matter whether the value has changed or not (deprecated - use se_action instead)' @@ -1593,17 +1610,19 @@ item_attribute_prefixes: en: 'Delaying an action (deprecated - use se_action instead)' se_repeat_: - type: bool + type: foo description: de: 'Definiert, ob eine Aktion beim erneuten Eintritt in den gleichen Status wiederholt werden soll oder nicht (veraltet - Nutze stattdessen se_action)' en: 'Definition wether an action should be repeated or not when reentering the same state (deprecated - use se_action instead)' se_order_: + type: foo description: de: 'Definiert die Reihenfolge einer Aktion als Integerzahl (veraltet - Nutze stattdessen se_action)' en: 'Definition of the running order of an action as integer (deprecated - use se_action instead)' se_manual_: + type: foo description: de: 'Diverse Funktion für den Suspendmodus wie include, exclude, invert, logitem.' en: 'Some functions relevant for the suspend mode like include, exclude, invert, logitem.' From 49963817a37b09beb3ccb07d4a0ef26e1a777e25 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 19 Sep 2024 22:38:50 +0200 Subject: [PATCH 090/121] stateengine plugin: bump version to 2.2.0 --- stateengine/__init__.py | 2 +- stateengine/plugin.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/__init__.py b/stateengine/__init__.py index bfa0c5c71..5b5296938 100755 --- a/stateengine/__init__.py +++ b/stateengine/__init__.py @@ -47,7 +47,7 @@ class StateEngine(SmartPlugin): - PLUGIN_VERSION = '2.1.0' + PLUGIN_VERSION = '2.2.0' # Constructor # noinspection PyUnusedLocal,PyMissingConstructor diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml index a0505d4f6..be3495046 100755 --- a/stateengine/plugin.yaml +++ b/stateengine/plugin.yaml @@ -39,7 +39,7 @@ plugin: state: ready support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support - version: '2.1.0' + version: '2.2.0' sh_minversion: '1.6' multi_instance: False classname: StateEngine From 8d1ff2fdf69a9d13ae285b9332c85125dae7bae9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 22 Sep 2024 20:33:51 +0200 Subject: [PATCH 091/121] stateengine plugin: fix previously introduced issue logging problem --- stateengine/StateEngineItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 40a9f32d2..38417aeda 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1204,7 +1204,7 @@ def list_issues(v): self.__logger.info("has the following issues:") self.__logger.increase_indent() for i, e in enumerate(_issuelist): - _attr = "" if _attrlist is None or not _attrlist[i] else "attribute {}: ".format(_attrlist[i]) + _attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[i] else "attribute {}: ".format(_attrlist[i]) if isinstance(e, dict): print_readable_dict(_attr, e) else: @@ -1214,11 +1214,11 @@ def list_issues(v): if isinstance(_issuelist[0], dict): self.__logger.info("has the following issues:") self.__logger.increase_indent() - _attr = "" if _attrlist is None or not _attrlist[0] else "attribute {}: ".format(_attrlist[0]) + _attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[0] else "attribute {}: ".format(_attrlist[0]) print_readable_dict(_attr, _issuelist[0]) self.__logger.decrease_indent() else: - _attr = "" if _attrlist is None or not _attrlist[0] else " for attribute {}".format(_attrlist[0]) + _attr = "" if _attrlist is None or not isinstance(_attrlist, list) or not _attrlist[0] else " for attribute {}".format(_attrlist[0]) self.__logger.info("has the following issue{}: {}", _attr, _issuelist[0]) else: if isinstance(_issuelist, dict): From 86a1d4b7ffa5cf071ace6d268df0a5367e84b721 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sun, 22 Sep 2024 20:44:29 +0200 Subject: [PATCH 092/121] stateengine plugin: fix pass_state webif handling on first run --- stateengine/StateEngineItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 38417aeda..23026c6ae 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -704,8 +704,8 @@ def update_current_to_empty(d): _leaveactions_run = True elif result is False and last_state != state and state.actions_pass.count() > 0: _pass_state = state - state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get()) - _key_pass = ['{}'.format(last_state.id), 'pass'] + _pass_state.run_pass(self.__pass_repeat.get(state, False), self.__repeat_actions.get()) + _key_pass = ['{}'.format(_pass_state.id), 'pass'] self.update_webif(_key_pass, True) self.__pass_repeat.update({state: True}) if result is True: From eeaf0d1c5f1686df88c0a5a7be68f0084a2d3c30 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 23 Sep 2024 06:39:57 +0200 Subject: [PATCH 093/121] stateengine plugin: minor improvements --- stateengine/StateEngineAction.py | 4 ++-- stateengine/StateEngineActions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 04679281e..2cd578b44 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -42,7 +42,7 @@ def function(self): def action_status(self): return self._action_status - # Cast function for delay + # Cast function for delay and other time based attributes # value: value to cast @staticmethod def __cast_seconds(value): @@ -59,7 +59,7 @@ def __cast_seconds(value): elif isinstance(value, float): return int(value) else: - raise ValueError("Can not cast delay value {0} to int!".format(value)) + raise ValueError("Can not cast value {0} to int!".format(value)) # Initialize the action # abitem: parent SeItem instance diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 92cf17fcb..05f2b340a 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -567,7 +567,7 @@ def remove_action(e): _issue_list.append(_issue) if _action: self.__actions[name] = _action - self._log_debug("Handle combined issuelist {}", _issue_list) + # self._log_develop("Handle combined issuelist {}", _issue_list) return _issue_list # noinspection PyMethodMayBeStatic From 61ffec0233e8c5aa5948641b474811f2ee7fa67c Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 23 Sep 2024 06:41:34 +0200 Subject: [PATCH 094/121] stateengine plugin: re-fix next conditionset when using released_by states --- stateengine/StateEngineItem.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 23026c6ae..e628ac0ec 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -761,11 +761,6 @@ def update_current_to_empty(d): "State is a copy and therefore just releasing {}. Skipping state actions, running leave actions " "of last state, then retriggering.", new_state.is_copy_for.id) if last_state is not None and self.__ab_alive: - _, self.__nextconditionset_name = self.__update_check_can_enter(last_state, _instant_leaveaction, False) - if self.__nextconditionset_name: - self.__nextconditionset_id = f"{state.state_item.property.path}.{self.__nextconditionset_name}" - else: - self.__nextconditionset_id = "" self.set_variable('next.conditionset_id', self.__nextconditionset_id) self.set_variable('next.conditionset_name', self.__nextconditionset_name) self.__logger.develop("Current variables: {}", self.__variables) From 4e3cb99a56268b25ec5629271007c31761019ac9 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Mon, 23 Sep 2024 06:43:03 +0200 Subject: [PATCH 095/121] stateengine plugin: improve logging (e.g. for handling released_by), introducing internal prefix for log messages --- stateengine/StateEngineItem.py | 8 ++++++-- stateengine/StateEngineLogger.py | 18 +++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index e628ac0ec..f0d5b9a3c 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -936,6 +936,7 @@ def update_can_release_list(): self.__logger.info("".ljust(80, "_")) self.__logger.info("Handling released_by attributes") + self.__logger.increase_indent("Handling released_by... ") can_release = {} all_released_by = {} skip_copy = True @@ -1037,7 +1038,7 @@ def update_can_release_list(): self.__release_info = {new_state.id: _can_release_list} _key_releasedby = ['{}'.format(new_state.id), 'releasedby'] self.update_webif(_key_releasedby, _can_release_list) - + self.__logger.increase_indent("") self.__logger.info("".ljust(80, "_")) return all_released_by @@ -1333,6 +1334,7 @@ def __reorder_states(self, init=True): _reordered_states = [] self.__logger.info("".ljust(80, "_")) self.__logger.info("Recalculating state order. Current order: {}", self.__states) + self.__logger.increase_indent() _copied_states = {} _add_order = 0 _changed_orders = [] @@ -1398,6 +1400,7 @@ def __reorder_states(self, init=True): else: _reorder_webif[state.id] = self.__webif_infos[state.id] self.__webif_infos = _reorder_webif + self.__logger.decrease_indent() self.__logger.info("Recalculated state order. New order: {}", self.__states) self.__logger.info("".ljust(80, "_")) @@ -1895,6 +1898,7 @@ def update_can_release_list(): self.__logger.info("".ljust(80, "_")) self.__logger.info("Initializing released_by attributes") + self.__logger.increase_indent() can_release = {} state_dict = {state.id: state for state in self.__states} for state in self.__states: @@ -1920,7 +1924,7 @@ def update_can_release_list(): self.__config_issues.update({state.id: {'issue': _issuelist, 'attribute': 'se_released_by'}}) state.update_releasedby_internal(_convertedlist) self.__update_can_release(can_release, state) - + self.__logger.decrease_indent() self.__logger.info("".ljust(80, "_")) # log item data diff --git a/stateengine/StateEngineLogger.py b/stateengine/StateEngineLogger.py index a398a6b15..645bf020a 100755 --- a/stateengine/StateEngineLogger.py +++ b/stateengine/StateEngineLogger.py @@ -131,6 +131,7 @@ def __init__(self, item, manual=False): self.__name = 'stateengine.{}'.format(item.property.path) self.__section = item.property.path.replace(".", "_").replace("/", "") self.__indentlevel = 0 + self.__indentprefix = "" if manual: self.__log_level_as_num = 2 else: @@ -151,7 +152,10 @@ def update_logfile(self): # Increase indentation level # by: number of levels to increase def increase_indent(self, by=1): - self.__indentlevel += by + if isinstance(by, int): + self.__indentlevel += by + else: + self.__indentprefix = by # Decrease indentation level # by: number of levels to decrease @@ -170,7 +174,7 @@ def log(self, level, text, *args): indent = "\t" * self.__indentlevel if args: text = text.format(*args) - logtext = "{0}{1} {2}\r\n".format(datetime.datetime.now(), indent, text) + logtext = "{0} {1}{2}{3}\r\n".format(datetime.datetime.now(), self.__indentprefix, indent, text) try: with open(self.__filename, mode="a", encoding="utf-8") as f: f.write(logtext) @@ -194,7 +198,7 @@ def header(self, text): def info(self, text, *args): self.log(1, text, *args) indent = "\t" * self.__indentlevel - text = '{}{}'.format(indent, text) + text = '{}{}{}'.format(self.__indentprefix, indent, text) if args: text = text.format(*args) self.logger.info(text) @@ -205,7 +209,7 @@ def info(self, text, *args): def debug(self, text, *args): self.log(2, text, *args) indent = "\t" * self.__indentlevel - text = '{}{}'.format(indent, text) + text = '{}{}{}'.format(self.__indentprefix, indent, text) if args: text = text.format(*args) self.logger.debug(text) @@ -216,7 +220,7 @@ def debug(self, text, *args): def develop(self, text, *args): self.log(3, "DEV: " + text, *args) indent = "\t" * self.__indentlevel - text = '{}{}'.format(indent, text) + text = '{}{}{}'.format(self.__indentprefix, indent, text) if args: text = text.format(*args) self.logger.log(StateEngineDefaults.VERBOSE, text) @@ -228,7 +232,7 @@ def develop(self, text, *args): def warning(self, text, *args): self.log(1, "WARNING: " + text, *args) indent = "\t" * self.__indentlevel - text = '{}{}'.format(indent, text) + text = '{}{}{}'.format(self.__indentprefix, indent, text) if args: text = text.format(*args) self.logger.warning(text) @@ -240,7 +244,7 @@ def warning(self, text, *args): def error(self, text, *args): self.log(1, "ERROR: " + text, *args) indent = "\t" * self.__indentlevel - text = '{}{}'.format(indent, text) + text = '{}{}{}'.format(self.__indentprefix, indent, text) if args: text = text.format(*args) self.logger.error(text) From 00a67378e54d633092817f9ad48f9e1bf4ce573d Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 01:03:34 +0200 Subject: [PATCH 096/121] stateengine plugin: fix and improve logging --- stateengine/StateEngineAction.py | 19 +++++++++++-------- stateengine/StateEngineItem.py | 18 +++++++++++------- stateengine/StateEngineValue.py | 19 +++++++++++-------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 2cd578b44..69250609d 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -429,6 +429,8 @@ def execute(self, is_repeat: bool, allow_item_repeat: bool, state): # check if any conditiontype is met or not # condition: type of condition 'conditionset'/'previousconditionset'/'previousstate_conditionset'/'nextconditionset' def _check_condition(condition: str): + self._log_debug("Checking {}", condition) + self._log_increase_indent() _conditions_met_count = 0 _conditions_necessary_count = 0 _condition_to_meet = None @@ -451,23 +453,24 @@ def _check_condition(condition: str): _updated_current_condition = self._abitem.get_variable("next.conditionset_id") if _current_condition == '' else _current_condition _condition_to_meet = _condition_to_meet if isinstance(_condition_to_meet, list) else [_condition_to_meet] _condition_met = [] + self._log_decrease_indent() for cond in _condition_to_meet: - if cond is not None: + if cond is not None and condition not in _conditions_met_type: _conditions_necessary_count += 1 _orig_cond = cond try: - cond = re.compile(cond) _matching = cond.fullmatch(_updated_current_condition) if _matching: - self._log_debug("Given {} {} matches current one: {}", condition, _orig_cond, _updated_current_condition) + self._log_debug("Given {} '{}' matches current one: '{}'", condition, _orig_cond.pattern, _updated_current_condition) _condition_met.append(_updated_current_condition) _conditions_met_count += 1 + _conditions_met_type.append(condition) else: - self._log_debug("Given {} {} not matching current one: {}", condition, _orig_cond, _updated_current_condition) - self.update_webif_actionstatus(state, self._name, 'False', None, f"({condition} {_orig_cond} not met)") + self._log_debug("Given {} '{}' not matching current one: '{}'", condition, _orig_cond.pattern, _updated_current_condition) + self.update_webif_actionstatus(state, self._name, 'False', None, f"({condition} {_orig_cond.pattern} not met)") except Exception as ex: if cond is not None: - self._log_warning("Given {} {} is not a valid regex: {}", condition, _orig_cond, ex) + self._log_warning("Given {} '{}' is not a valid regex: {}", condition, _orig_cond.pattern, ex) return _condition_met, _conditions_met_count, _conditions_necessary_count # update web interface with delay info @@ -499,11 +502,9 @@ def _update_repeat_webif(value: bool): self._log_increase_indent() try: self._getitem_fromeval() - self._log_decrease_indent() _validitem = True except Exception: _validitem = False - self._log_decrease_indent() if not self._can_execute(state): self._log_decrease_indent() return @@ -513,6 +514,7 @@ def _update_repeat_webif(value: bool): previous_condition_met = None previousstate_condition_met = None next_condition_met = None + _conditions_met_type = [] if not self.conditionset.is_empty(): current_condition_met, cur_conditions_met, cur_condition_necessary = _check_condition('conditionset') conditions_met += cur_conditions_met @@ -529,6 +531,7 @@ def _update_repeat_webif(value: bool): next_condition_met, next_conditions_met, next_conditionset_necessary = _check_condition('nextconditionset') conditions_met += next_conditions_met condition_necessary += min(1, next_conditionset_necessary) + self._log_decrease_indent() self._log_develop("Action '{0}': conditions met: {1}, necessary {2}.", self._name, conditions_met, condition_necessary) if conditions_met < condition_necessary: self._log_info("Action '{0}': Skipping because not all conditions are met.", self._name) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index f0d5b9a3c..407dba038 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -223,18 +223,17 @@ def __init__(self, smarthome, item, se_plugin): self.__name = str(self.__item) self.__itemClass = Item # initialize logging - + self.__logger.log_level_as_num = 2 self.__log_level = StateEngineValue.SeValue(self, "Log Level", False, "num") _default_log_level = self.__logger.default_log_level.get() _returnvalue, _returntype, _using_default, _issue, _ = self.__log_level.set_from_attr(self.__item, "se_log_level", - _default_log_level) + default_value=_default_log_level) self.__using_default_log_level = _using_default _returnvalue = self.__log_level.get() if isinstance(_returnvalue, list) and len(_returnvalue) == 1: _returnvalue = _returnvalue[0] - self.__logger.log_level_as_num = 2 _startup_log_level = self.__logger.startup_log_level.get() @@ -266,13 +265,13 @@ def __init__(self, smarthome, item, se_plugin): # get startup delay self.__startup_delay = StateEngineValue.SeValue(self, "Startup Delay", False, "num") - self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", StateEngineDefaults.startup_delay) + self.__startup_delay.set_from_attr(self.__item, "se_startup_delay", default_value=StateEngineDefaults.startup_delay) self.__startup_delay_over = False # Init suspend settings self.__default_suspend_time = StateEngineDefaults.suspend_time.get() self.__suspend_time = StateEngineValue.SeValue(self, "Suspension time on manual changes", False, "num") - self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", self.__default_suspend_time) + self.__suspend_time.set_from_attr(self.__item, "se_suspend_time", default_value=self.__default_suspend_time) # Init laststate and previousstate items/values self.__config_issues = {} @@ -459,7 +458,7 @@ def update_leave_action(self, default_instant_leaveaction): self.__default_instant_leaveaction = default_instant_leaveaction _returnvalue_leave, _returntype_leave, _using_default_leave, _issue, _ = self.__instant_leaveaction.set_from_attr( - self.__item, "se_instant_leaveaction", default_instant_leaveaction) + self.__item, "se_instant_leaveaction", default_value=default_instant_leaveaction) if len(_returnvalue_leave) > 1: self.__logger.warning("se_instant_leaveaction for item {} can not be defined as a list" @@ -2111,7 +2110,12 @@ def get_update_original_source(self): # return value of variable def get_variable(self, varname): - return self.__variables[varname] if varname in self.__variables else "(Unknown variable '{0}'!)".format(varname) + if varname not in self.__variables: + returnvalue = "(Unknown variable '{0}'!)".format(varname) + self.__logger.warning("Issue when getting variable {}".format(returnvalue)) + else: + returnvalue = self.__variables[varname] + return returnvalue # set value of variable def set_variable(self, varname, value): diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 0e14b1296..b4ff3a3bb 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -115,7 +115,7 @@ def set_from_attr(self, item, attribute_name, default_value=None, reset=True, at value = default_value _using_default = True self._log_develop("Processing value from attribute name {0}, reset {1}, type {2}: using default value {3}", - attribute_name, reset, value, attr_type) + attribute_name, reset, attr_type, value) value_list = [] if value is not None and isinstance(value, list) and attr_type is not None: for i, entry in enumerate(value): @@ -475,6 +475,7 @@ def get_type(self): # Write condition to logger def write_to_logger(self): + returnvalues = [] if self.__template is not None: self._log_info("{0}: Using template(s) {1}", self.__name, self.__template) if self.__value is not None: @@ -484,7 +485,7 @@ def write_to_logger(self): self._log_debug("{0}: {1} ({2})", self.__name, i, type(i)) else: self._log_debug("{0}: {1} ({2})", self.__name, self.__value, type(self.__value)) - return self.__value + returnvalues.append(self.__value) if self.__regex is not None: if isinstance(self.__regex, list): for i in self.__regex: @@ -492,7 +493,7 @@ def write_to_logger(self): self._log_debug("{0} from regex: {1}", self.__name, i) else: self._log_debug("{0} from regex: {1}", self.__name, self.__regex) - return f"regex:{self.__regex}" + returnvalues.append(f"regex:{self.__regex}") if self.__struct is not None: if isinstance(self.__struct, list): for i in self.__struct: @@ -501,7 +502,7 @@ def write_to_logger(self): else: self._log_debug("{0} from struct: {1}", self.__name, self.__struct.property.path) - return self.__struct + returnvalues.append(self.__struct) if self.__item is not None: _original_listorder = self.__listorder.copy() items = [] @@ -517,14 +518,14 @@ def write_to_logger(self): items = self.__get_from_item() self._log_debug("Currently item results in {}", items) self.__listorder = _original_listorder - return items + returnvalues.append(items) if self.__eval is not None: self._log_debug("{0} from eval: {1}", self.__name, self.__eval) _original_listorder = self.__listorder.copy() eval_result = self.__get_eval() self._log_debug("Currently eval results in {}. ", eval_result) self.__listorder = _original_listorder - return eval_result + returnvalues.append(eval_result) if self.__varname is not None: if isinstance(self.__varname, list): for i in self.__varname: @@ -535,8 +536,10 @@ def write_to_logger(self): _original_listorder = self.__listorder.copy() var_result = self.__get_from_variable() self.__listorder = _original_listorder - return var_result - return None + returnvalues.append(var_result) + returnvalues = StateEngineTools.flatten_list(returnvalues) + returnvalues = returnvalues[0] if len(returnvalues) == 1 else None if len(returnvalues) == 0 else returnvalues + return returnvalues # Get Text (similar to logger text) # prefix: Prefix for text From 98c146134fc93f5701a9ff10c6cd1a275ad62586 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 01:05:36 +0200 Subject: [PATCH 097/121] stateengine plugin: introduce regex casting, used for conditionset comparison for actions --- stateengine/StateEngineAction.py | 8 ++++---- stateengine/StateEngineValue.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 69250609d..70099d79d 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -74,10 +74,10 @@ def __init__(self, abitem, name: str): self.__delay = StateEngineValue.SeValue(self._abitem, "delay") self.__repeat = None self.__instanteval = None - self.nextconditionset = StateEngineValue.SeValue(self._abitem, "nextconditionset", True, "str") - self.conditionset = StateEngineValue.SeValue(self._abitem, "conditionset", True, "str") - self.previousconditionset = StateEngineValue.SeValue(self._abitem, "previousconditionset", True, "str") - self.previousstate_conditionset = StateEngineValue.SeValue(self._abitem, "previousstate_conditionset", True, "str") + self.nextconditionset = StateEngineValue.SeValue(self._abitem, "nextconditionset", True, "regex") + self.conditionset = StateEngineValue.SeValue(self._abitem, "conditionset", True, "regex") + self.previousconditionset = StateEngineValue.SeValue(self._abitem, "previousconditionset", True, "regex") + self.previousstate_conditionset = StateEngineValue.SeValue(self._abitem, "previousstate_conditionset", True, "regex") self.__mode = StateEngineValue.SeValue(self._abitem, "mode", True, "str") self.__order = StateEngineValue.SeValue(self._abitem, "order", False, "num") self._minagedelta = StateEngineValue.SeValue(self._abitem, "minagedelta") diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index b4ff3a3bb..244689933 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -54,7 +54,7 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__varname = None self.__template = None self.__issues = [] - self.__get_issues = {'cast_item': [], 'eval': [], 'regex': [], 'struct': [], 'var': [], 'item': []} + self.__get_issues = {'cast_item': [], 'cast_regex': [], 'eval': [], 'regex': [], 'struct': [], 'var': [], 'item': []} self._additional_sources = [] self.itemsApi = Items.get_instance() self.__itemClass = Item @@ -64,6 +64,8 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__valid_valuetypes = ["value", "regex", "eval", "var", "item", "template", "struct"] if value_type == "str": self.__cast_func = StateEngineTools.cast_str + elif value_type == "regex": + self.__cast_func = self.cast_regex elif value_type == "num": self.__cast_func = StateEngineTools.cast_num elif value_type == "item": @@ -564,6 +566,31 @@ def get_text(self, prefix=None, suffix=None): value = value if suffix is None else value + suffix return value + # cast a value as regex. Throws ValueError if cast is not possible + # value: value to cast + # returns: value as regex + def cast_regex(self, value): + try: + _issue_dict = {} + _returnvalue = value + if isinstance(value, str): + try: + _returnvalue = re.compile(value, re.IGNORECASE) + except Exception as ex: + _issue = "Issue converting {} to regex: {}".format(value, ex) + _issue_dict = {str(value): _issue} + self._log_error(_issue) + if _issue_dict and _issue_dict not in self.__get_issues['cast_regex']: + self.__get_issues['cast_regex'].append(_issue_dict) + return _returnvalue + except Exception as ex: + _issue = "Can't cast {0} to regex! {1}".format(value, ex) + _issue_dict = {str(value): _issue} + if _issue_dict not in self.__get_issues['cast_regex']: + self.__get_issues['cast_regex'].append(_issue_dict) + self._log_error(_issue) + return value + # cast a value as item. Throws ValueError if cast is not possible # value: value to cast # returns: value as item or struct From 1d52c52aa08ab69fca2063f4487f58cc5e200d2b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 01:06:23 +0200 Subject: [PATCH 098/121] stateengine plugin: fix/improve conversion of lists in items --- stateengine/StateEngineTools.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index a29968b0a..2a892ceaa 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -323,8 +323,16 @@ def convert_str_to_list(value, force=True): try: elements = re.findall(r"'([^']+)'|([^,]+)", value) flattened_elements = [element[0] if element[0] else element[1] for element in elements] - formatted_str = "[" + ", ".join( - ["'" + element.strip(" '\"") + "'" for element in flattened_elements]) + "]" + formatted_elements = [] + for element in flattened_elements: + element = element.strip(" '\"") + if "'" in element: + formatted_elements.append(f'"{element}"') + elif '"' in element: + formatted_elements.append(f"'{element}'") + else: + formatted_elements.append(f"'{element}'") + formatted_str = "[" + ", ".join(formatted_elements) + "]" return literal_eval(formatted_str) except Exception as ex: raise ValueError("Problem converting string to list: {}".format(ex)) From c49f322aec0d361a65dce125c52628ac0fdc075a Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 01:07:09 +0200 Subject: [PATCH 099/121] stateengine plugin: correctly parse values in item, you can now also define regex, eval, etc. in an item(value)! --- stateengine/StateEngineValue.py | 338 +++++++++++++++++++------------- 1 file changed, 204 insertions(+), 134 deletions(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 244689933..4cb873448 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -164,11 +164,13 @@ def __resetvalue(self): # Set value # value: string indicating value or source of value # name: name of object ("time" is being handled differently) - def set(self, value, name="", reset=True, copyvalue=True): + def set(self, value, name="", reset=True, copyvalue=True, returnvalue=False): + if copyvalue is True: value = copy.copy(value) if reset: self.__resetvalue() + returnvalues = [] if isinstance(value, list): source = [] field_value = [] @@ -184,7 +186,8 @@ def set(self, value, name="", reset=True, copyvalue=True): f = val source.append(s) field_value.append(f) - self.__listorder.append("{}:{}".format(s, f)) + if not returnvalue: + self.__listorder.append("{}:{}".format(s, f)) if field_value[i] == "": source[i] = "value" field_value[i] = value[i] @@ -199,10 +202,11 @@ def set(self, value, name="", reset=True, copyvalue=True): "will be handled the same as the item type, e.g. string, bool, etc.", _issue, self.__valid_valuetypes, field_value[i]) source[i] = "value" - self.__type_listorder.append(source[i]) - self.__orig_listorder.append(val) - if source[i] == "value": - self.__listorder[i] = value[i] + if not returnvalue: + self.__type_listorder.append(source[i]) + self.__orig_listorder.append(val) + if source[i] == "value": + self.__listorder[i] = value[i] if source[i] == "template": if self.__template is None: self.__template = [] @@ -211,11 +215,14 @@ def set(self, value, name="", reset=True, copyvalue=True): if _template is not None: try: source[i], field_value[i] = StateEngineTools.partition_strip(_template, ":") - if val in self.__listorder and field_value[i] in self._abitem.templates: + if not returnvalue and val in self.__listorder and field_value[i] in self._abitem.templates: self.__listorder[self.__listorder.index(val)] = self._abitem.templates.get(field_value[i]) + elif returnvalue and val in returnvalues and field_value[i] in self._abitem.templates: + returnvalues[returnvalues.index(val)] = self._abitem.templates.get(field_value[i]) except Exception as ex: self._abitem.updatetemplates(field_value[i], None) - self.__listorder = [i for i in self.__listorder if i != val] + if not returnvalue: + self.__listorder = [i for i in self.__listorder if i != val] self._log_warning("Removing template {}: {}", field_value[i], ex) val, field_value[i], source[i] = None, None, None else: @@ -223,7 +230,8 @@ def set(self, value, name="", reset=True, copyvalue=True): if _issue not in self.__issues: self.__issues.append(_issue) self._log_warning(_issue) - self.__listorder = [i for i in self.__listorder if i != val] + if not returnvalue: + self.__listorder = [i for i in self.__listorder if i != val] source[i], field_value[i], val = None, None, None try: if isinstance(self.__template, list) and len(self.__template) == 1: @@ -233,17 +241,21 @@ def set(self, value, name="", reset=True, copyvalue=True): elif isinstance(value, str): source, field_value = StateEngineTools.partition_strip(value, ":") - self.__listorder.append("{}{}{}".format(source, ":" if field_value else "", field_value)) + if not returnvalue: + self.__listorder.append("{}{}{}".format(source, ":" if field_value else "", field_value)) if source == "template": self.__template = field_value _template = self._abitem.templates.get(self.__template) if _template is not None: try: source, field_value = StateEngineTools.partition_strip(_template, ":") - if value in self.__listorder and field_value in self._abitem.templates: + if not returnvalue and value in self.__listorder and field_value in self._abitem.templates: self.__listorder[self.__listorder.index(value)] = self._abitem.templates[self.__template] + elif returnvalue and value in returnvalues and field_value in self._abitem.templates: + returnvalues[returnvalues.index(value)] = self._abitem.templates[self.__template] except Exception as ex: - self.__listorder = [i for i in self.__listorder if i != value] + if not returnvalue: + self.__listorder = [i for i in self.__listorder if i != value] source, field_value, value = None, None, None self._abitem.updatetemplates(self.__template, None) self._log_warning("Removing template {}: {}", self.__template, ex) @@ -252,7 +264,8 @@ def set(self, value, name="", reset=True, copyvalue=True): if _issue not in self.__issues: self.__issues.append(_issue) self._log_warning(_issue) - self.__listorder = [i for i in self.__listorder if i != value] + if not returnvalue: + self.__listorder = [i for i in self.__listorder if i != value] source, field_value, value = None, None, None try: cond1 = source.lstrip('-').replace('.', '', 1).isdigit() @@ -274,10 +287,11 @@ def set(self, value, name="", reset=True, copyvalue=True): "will be handled the same as the item type, e.g. string, bool, etc.", _issue, self.__valid_valuetypes, field_value) source = "value" - if source == "value": - self.__listorder = [field_value] - self.__type_listorder.append(source) - self.__orig_listorder.append(value) + if not returnvalue: + if source == "value": + self.__listorder = [field_value] + self.__type_listorder.append(source) + self.__orig_listorder.append(value) else: source = "value" field_value = value @@ -316,8 +330,9 @@ def set(self, value, name="", reset=True, copyvalue=True): elif field_value[i] == "": field_value[i] = s s = "value" - self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, - list) else self.__value + if not returnvalue: + self.__value = [] if self.__value is None else [self.__value] if not isinstance(self.__value, + list) else self.__value if s == "value": cond3 = isinstance(field_value[i], str) and field_value[i].lstrip('-').replace('.', '', 1).isdigit() if cond3: @@ -330,10 +345,14 @@ def set(self, value, name="", reset=True, copyvalue=True): _value, _issue = self.__do_cast(field_value[i]) if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) - self.__value.append(_value) + if not returnvalue: + self.__value.append(_value) + else: + returnvalues.append(_value) else: self.__value.append(None) - self.__item = [] if self.__item is None else [self.__item] if not isinstance(self.__item, list) else self.__item + if not returnvalue: + self.__item = [] if self.__item is None else [self.__item] if not isinstance(self.__item, list) else self.__item if s == "item": _item, _issue = self._abitem.return_item(field_value[i]) if _issue not in [[], None, [None], self.__issues]: @@ -341,35 +360,47 @@ def set(self, value, name="", reset=True, copyvalue=True): if _issue_dict not in self.__get_issues['item']: self.__get_issues['item'].append(_issue_dict) self.__issues.append(_issue) - self.__item.append(None if s != "item" else self.__absolute_item(_item, field_value[i])) - self.__eval = [] if self.__eval is None else [self.__eval] if not isinstance(self.__eval, list) else self.__eval - self.__eval.append(None if s != "eval" else field_value[i]) - self.__regex = [] if self.__regex is None else [self.__regex] if not isinstance(self.__regex, list) else self.__regex - self.__regex.append(None if s != "regex" else field_value[i]) - self.__struct = [] if self.__struct is None else [self.__struct] if not isinstance(self.__struct, list) else self.__struct - self.__struct.append(None if s != "struct" else StateEngineStructs.create(self._abitem, field_value[i])) - self.__varname = [] if self.__varname is None else [self.__varname] if not isinstance(self.__varname, list) else self.__varname - self.__varname.append(None if s != "var" else field_value[i]) - - if self.__item: - self.__item = [i for i in self.__item if i is not None] - self.__item = self.__item[0] if len(self.__item) == 1 else None if len(self.__item) == 0 else self.__item - if self.__eval: - self.__eval = [i for i in self.__eval if i is not None] - self.__eval = self.__eval[0] if len(self.__eval) == 1 else None if len(self.__eval) == 0 else self.__eval - if self.__regex: - self.__regex = [i for i in self.__regex if i is not None] - self.__regex = self.__regex[0] if len(self.__regex) == 1 else None if len(self.__regex) == 0 else self.__regex - if self.__struct: - self.__struct = [i for i in self.__struct if i is not None] - self.__struct = None if len(self.__struct) == 0 else self.__struct - if self.__varname: - self.__varname = [i for i in self.__varname if i is not None] - self.__varname = self.__varname[0] if len(self.__varname) == 1 else None if len(self.__varname) == 0 else self.__varname - if self.__value: - self.__value = [i for i in self.__value if i is not None] - self.__value = self.__value[0] if len(self.__value) == 1 else None if len(self.__value) == 0 else self.__value - + returnvalues.append(_item) + if not returnvalue: + self.__item.append(None if s != "item" else self.__absolute_item(_item, field_value[i])) + self.__eval = [] if self.__eval is None else [self.__eval] if not isinstance(self.__eval, list) else self.__eval + self.__eval.append(None if s != "eval" else field_value[i]) + self.__regex = [] if self.__regex is None else [self.__regex] if not isinstance(self.__regex, list) else self.__regex + self.__regex.append(None if s != "regex" else field_value[i]) + self.__struct = [] if self.__struct is None else [self.__struct] if not isinstance(self.__struct, list) else self.__struct + self.__struct.append(None if s != "struct" else StateEngineStructs.create(self._abitem, field_value[i])) + self.__varname = [] if self.__varname is None else [self.__varname] if not isinstance(self.__varname, list) else self.__varname + self.__varname.append(None if s != "var" else field_value[i]) + else: + if s == "item": + returnvalues.append(self.__get_from_item(field_value[i])) + elif s == "struct": + returnvalues.append(self.__get_from_struct(field_value[i])) + elif s == "eval": + returnvalues.append(self.__get_eval(field_value[i])) + elif s == "regex": + returnvalues.append(self.__get_from_regex(field_value[i])) + elif s == "var": + returnvalues.append(self.__get_from_variable(field_value[i])) + if not returnvalue: + if self.__item: + self.__item = [i for i in self.__item if i is not None] + self.__item = self.__item[0] if len(self.__item) == 1 else None if len(self.__item) == 0 else self.__item + if self.__eval: + self.__eval = [i for i in self.__eval if i is not None] + self.__eval = self.__eval[0] if len(self.__eval) == 1 else None if len(self.__eval) == 0 else self.__eval + if self.__regex: + self.__regex = [i for i in self.__regex if i is not None] + self.__regex = self.__regex[0] if len(self.__regex) == 1 else None if len(self.__regex) == 0 else self.__regex + if self.__struct: + self.__struct = [i for i in self.__struct if i is not None] + self.__struct = None if len(self.__struct) == 0 else self.__struct + if self.__varname: + self.__varname = [i for i in self.__varname if i is not None] + self.__varname = self.__varname[0] if len(self.__varname) == 1 else None if len(self.__varname) == 0 else self.__varname + if self.__value: + self.__value = [i for i in self.__value if i is not None] + self.__value = self.__value[0] if len(self.__value) == 1 else None if len(self.__value) == 0 else self.__value else: if source == "item": _item, _issue = self._abitem.return_item(field_value) @@ -378,11 +409,24 @@ def set(self, value, name="", reset=True, copyvalue=True): if _issue_dict not in self.__get_issues['item']: self.__get_issues['item'].append(_issue_dict) self.__issues.append(_issue) - self.__item = None if source != "item" else self.__absolute_item(_item, field_value) - self.__eval = None if source != "eval" else field_value - self.__regex = None if source != "regex" else field_value - self.__struct = None if source != "struct" else StateEngineStructs.create(self._abitem, field_value) - self.__varname = None if source != "var" else field_value + returnvalues.append(_item) + if not returnvalue: + self.__item = None if source != "item" else self.__absolute_item(_item, field_value) + self.__eval = None if source != "eval" else field_value + self.__regex = None if source != "regex" else field_value + self.__struct = None if source != "struct" else StateEngineStructs.create(self._abitem, field_value) + self.__varname = None if source != "var" else field_value + else: + if source == "item": + returnvalues.append(self.__get_from_item(field_value)) + elif source == "struct": + returnvalues.append(self.__get_from_struct(field_value)) + elif source == "eval": + returnvalues.append(self.__get_eval(field_value)) + elif source == "regex": + returnvalues.append(self.__get_from_regex(field_value)) + elif source == "var": + returnvalues.append(self.__get_from_variable(field_value)) if source == "value": if isinstance(field_value, list) and not self.__allow_value_list: raise ValueError("{0}: value_in is not allowed, problem with {1}. Allowed = {2}".format( @@ -394,16 +438,24 @@ def set(self, value, name="", reset=True, copyvalue=True): field_value = True elif isinstance(field_value, str) and field_value.lower() in ['false', 'no']: field_value = False - self.__value, _issue = self.__do_cast(field_value) + if not returnvalue: + self.__value, _issue = self.__do_cast(field_value) + else: + val, _issue = self.__do_cast(field_value) + returnvalues.append(val) if _issue not in [[], None, [None], self.__issues]: self.__issues.append(_issue) else: self.__value = None self.__issues = StateEngineTools.flatten_list(self.__issues) + del value + if returnvalue: + returnvalues = StateEngineTools.flatten_list(returnvalues) + returnvalues = returnvalues[0] if len(returnvalues) == 1 else None if len(returnvalues) == 0 else returnvalues + return returnvalues, self.__issues self.__listorder = StateEngineTools.flatten_list(self.__listorder) self.__type_listorder = StateEngineTools.flatten_list(self.__type_listorder) self.__orig_listorder = StateEngineTools.flatten_list(self.__orig_listorder) - del value return self.__listorder, self.__type_listorder, self.__issues, self.__orig_listorder # Set cast function @@ -718,10 +770,12 @@ def __do_cast(self, value, item_id=None): return value, _issue # Determine value by using a struct - def __get_from_struct(self): + def __get_from_struct(self, struct_get=None): values = [] - if isinstance(self.__struct, list): - for val in self.__struct: + if struct_get is None: + struct_get = self.__struct + if isinstance(struct_get, list): + for val in struct_get: if val is not None: _newvalue, _issue = self.__do_cast(val) _issue_dict = {val: _issue} @@ -731,28 +785,28 @@ def __get_from_struct(self): if 'struct:{}'.format(val.property.path) in self.__listorder: self.__listorder[self.__listorder.index('struct:{}'.format(val.property.path))] = _newvalue else: - if self.__struct is not None: - _newvalue, _issue = self.__do_cast(self.__struct) - _issue_dict = {self.__struct: _issue} + if struct_get is not None: + _newvalue, _issue = self.__do_cast(struct_get) + _issue_dict = {struct_get: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['struct']: self.__get_issues['struct'].append(_issue_dict) - if 'struct:{}'.format(self.__regex) in self.__listorder: - self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue + if 'struct:{}'.format(struct_get) in self.__listorder: + self.__listorder[self.__listorder.index('struct:{}'.format(struct_get))] = _newvalue values = _newvalue if values: return values try: - _newvalue, _issue = self.__do_cast(self.__struct) + _newvalue, _issue = self.__do_cast(struct_get) _issue_dict = {_newvalue: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['struct']: self.__get_issues['struct'].append(_issue_dict) - if 'struct:{}'.format(self.__struct) in self.__listorder: - self.__listorder[self.__listorder.index('struct:{}'.format(self.__struct))] = _newvalue + if 'struct:{}'.format(struct_get) in self.__listorder: + self.__listorder[self.__listorder.index('struct:{}'.format(struct_get))] = _newvalue values = _newvalue except Exception as ex: - values = self.__struct + values = struct_get _issue = "Problem while getting from struct '{0}': {1}.".format(values, ex) _issue_dict = {values: _issue} if _issue_dict not in self.__get_issues['struct']: @@ -761,28 +815,30 @@ def __get_from_struct(self): return values # Determine value by regular expression - def __get_from_regex(self): - if isinstance(self.__regex, list): + def __get_from_regex(self, regex_get=None): + if regex_get is None: + regex_get = self.__regex + if isinstance(regex_get, list): values = [] - for val in self.__regex: + for val in regex_get: _newvalue = re.compile(val, re.IGNORECASE) values.append(_newvalue) if 'regex:{}'.format(val) in self.__listorder: self.__listorder[self.__listorder.index('regex:{}'.format(val))] = _newvalue else: - _newvalue = re.compile(self.__regex, re.IGNORECASE) - if 'regex:{}'.format(self.__regex) in self.__listorder: - self.__listorder[self.__listorder.index('regex:{}'.format(self.__regex))] = _newvalue + _newvalue = re.compile(regex_get, re.IGNORECASE) + if 'regex:{}'.format(regex_get) in self.__listorder: + self.__listorder[self.__listorder.index('regex:{}'.format(regex_get))] = _newvalue values = _newvalue if values is not None: return values try: - _newvalue = re.compile(self.__regex, re.IGNORECASE) - if 'regex:{}'.format(self.__regex) in self.__listorder: - self.__listorder[self.__listorder.index('regex:{}'.format(self.__regex))] = _newvalue + _newvalue = re.compile(regex_get, re.IGNORECASE) + if 'regex:{}'.format(regex_get) in self.__listorder: + self.__listorder[self.__listorder.index('regex:{}'.format(regex_get))] = _newvalue values = _newvalue except Exception as ex: - values = self.__regex + values = regex_get _issue = "Problem while creating regex '{0}': {1}.".format(values, ex) _issue_dict = {values: _issue} if _issue_dict not in self.__get_issues['regex']: @@ -791,7 +847,7 @@ def __get_from_regex(self): return values # Determine value by executing eval-function - def __get_eval(self): + def __get_eval(self, eval_get=None): # noinspection PyUnusedLocal sh = self._sh # noinspection PyUnusedLocal @@ -802,37 +858,44 @@ def __get_eval(self): "get_variable('next.", 'get_variable("next.' ] - if isinstance(self.__eval, str): - self.__eval = StateEngineTools.parse_relative(self.__eval, 'sh.', ['()', '.property.']) - if "stateengine_eval" in self.__eval or "se_eval" in self.__eval: + set_eval = False + if eval_get is None: + eval_get = self.__eval + set_eval = True + if isinstance(eval_get, str): + if set_eval: + self.__eval = StateEngineTools.parse_relative(eval_get, 'sh.', ['()', '.property.']) + else: + eval_get = StateEngineTools.parse_relative(eval_get, 'sh.', ['()', '.property.']) + if "stateengine_eval" in eval_get or "se_eval" in eval_get: # noinspection PyUnusedLocal stateengine_eval = se_eval = StateEngineEval.SeEval(self._abitem) - self._log_debug("Checking eval: {0}", self.__eval) - if self.__eval in self._abitem.cache: + self._log_debug("Checking eval: {0}", eval_get) + if eval_get in self._abitem.cache: self._log_increase_indent() - result = self._abitem.cache.get(self.__eval) + result = self._abitem.cache.get(eval_get) self._log_debug("Loading eval from cache: {}", result) self._log_decrease_indent() - if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + if 'eval:{}'.format(eval_get) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(eval_get))] = [result] return result self._log_increase_indent() try: - _newvalue, _issue = self.__do_cast(eval(self.__eval)) - _issue_dict = {StateEngineTools.get_eval_name(self.__eval): _issue} + _newvalue, _issue = self.__do_cast(eval(eval_get)) + _issue_dict = {StateEngineTools.get_eval_name(eval_get): _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: self.__get_issues['eval'].append(_issue_dict) - if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] + if 'eval:{}'.format(eval_get) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(eval_get))] = [_newvalue] values = _newvalue self._log_decrease_indent() self._log_debug("Eval result: {0} ({1}).", values, type(values)) - if not any(pattern in self.__eval for pattern in patterns): - self._abitem.cache = {self.__eval: values} + if not any(pattern in eval_get for pattern in patterns): + self._abitem.cache = {eval_get: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - _name = StateEngineTools.get_eval_name(self.__eval) + _name = StateEngineTools.get_eval_name(eval_get) _issue = "Problem evaluating '{0}': {1}.".format(_name, ex) _issue_dict = {_name: _issue} if _issue_dict not in self.__get_issues['eval']: @@ -843,14 +906,14 @@ def __get_eval(self): finally: self._log_decrease_indent() else: - if isinstance(self.__eval, list): + if isinstance(eval_get, list): values = [] - for val in self.__eval: + for val in eval_get: try: val = val.replace("\n", "") except Exception: pass - self._log_debug("Checking eval {0} from list {1}.", val, self.__eval) + self._log_debug("Checking eval {0} from list {1}.", val, eval_get) self._log_increase_indent() if val in self._abitem.cache: result = self._abitem.cache.get(val) @@ -909,31 +972,31 @@ def __get_eval(self): self._abitem.cache = {val: value} self._log_decrease_indent() else: - self._log_debug("Checking eval (no str, no list): {0}.", self.__eval) - if self.__eval in self._abitem.cache: + self._log_debug("Checking eval (no str, no list): {0}.", eval_get) + if eval_get in self._abitem.cache: self._log_increase_indent() - result = self._abitem.cache.get(self.__eval) + result = self._abitem.cache.get(eval_get) self._log_debug("Loading eval (no str, no list) from cache: {}", result) self._log_decrease_indent() - if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [result] + if 'eval:{}'.format(eval_get) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(eval_get))] = [result] return result try: self._log_increase_indent() - _newvalue, _issue = self.__do_cast(self.__eval()) + _newvalue, _issue = self.__do_cast(eval_get()) _issue_dict = {_newvalue: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['eval']: self.__get_issues['eval'].append(_issue_dict) - if 'eval:{}'.format(self.__eval) in self.__listorder: - self.__listorder[self.__listorder.index('eval:{}'.format(self.__eval))] = [_newvalue] + if 'eval:{}'.format(eval_get) in self.__listorder: + self.__listorder[self.__listorder.index('eval:{}'.format(eval_get))] = [_newvalue] values = _newvalue self._log_decrease_indent() self._log_debug("Eval result (no str, no list): {0}.", values) - self._abitem.cache = {self.__eval: values} + self._abitem.cache = {eval_get: values} self._log_increase_indent() except Exception as ex: self._log_decrease_indent() - _name = StateEngineTools.get_eval_name(self.__eval) + _name = StateEngineTools.get_eval_name(eval_get) _issue = "Problem evaluating '{0}': {1}.".format(_name, ex) self._log_warning(_issue) self._log_increase_indent() @@ -944,10 +1007,12 @@ def __get_eval(self): return values # Determine value from item - def __get_from_item(self): - if isinstance(self.__item, list): + def __get_from_item(self, get_item=None): + if get_item is None: + get_item = self.__item + if isinstance(get_item, list): values = [] - for val in self.__item: + for val in get_item: _new_values = [] if val is None: _newvalue = None @@ -960,7 +1025,9 @@ def __get_from_item(self): checked_entry = checked_entry if isinstance(checked_entry, list) else [checked_entry] for entry in checked_entry: - _newvalue, _issue = self.__do_cast(entry) + # _newvalue, _issue = self.__do_cast(entry) + _newvalue, _issue = self.set(entry, reset=False, returnvalue=True) + self._log_develop("Return from set from item list: {}, issue {}", _newvalue, _issue) _issue_dict = {entry: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: self.__get_issues['item'].append(_issue_dict) @@ -981,78 +1048,81 @@ def __get_from_item(self): if values is not None: return values else: - if self.__item is None: + if get_item is None: return None try: - checked_entry = StateEngineTools.convert_str_to_list(self.__item.property.value) + checked_entry = StateEngineTools.convert_str_to_list(get_item.property.value) except Exception as ex: self._log_warning("While getting from item: {}", ex) checked_entry = [] checked_entry = checked_entry if isinstance(checked_entry, list) else [checked_entry] _new_values = [] for entry in checked_entry: - _newvalue, _issue = self.__do_cast(entry) + #_newvalue, _issue = self.__do_cast(entry) + _newvalue, _issue = self.set(entry, reset=False, returnvalue=True) + self._log_develop("Return from set from item: {}, issue {}, listorder {}", _newvalue, _issue, self.__listorder) _issue_dict = {entry: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: self.__get_issues['item'].append(_issue_dict) if _newvalue is not None: _new_values.append(_newvalue) _new_values = _new_values[0] if len(_new_values) == 1 else None if len(_new_values) == 0 else [_new_values] - search_item = 'item:{}'.format(self.__item) + search_item = 'item:{}'.format(get_item) if search_item in self.__listorder: index = self.__listorder.index(search_item) self.__listorder[index] = _new_values - if self.__item in self.__listorder: - index = self.__listorder.index(self.__item) + if get_item in self.__listorder: + index = self.__listorder.index(get_item) self.__listorder[index] = _new_values values = _new_values if values is not None: return values try: - _newvalue = self.__item.property.path - search_item = 'item:{}'.format(self.__item) + _newvalue = get_item.property.path + search_item = 'item:{}'.format(get_item) if search_item in self.__listorder: index = self.__listorder.index(search_item) self.__listorder[index] = _newvalue values = _newvalue except Exception as ex: - values = self.__item + values = get_item _issue = "Problem while reading item path '{0}': {1}.".format(values, ex) self._log_info(_issue) - _newvalue, _issue = self.__do_cast(values) + #_newvalue, _issue = self.__do_cast(values) + _newvalue, _issue = self.set(values, reset=False, returnvalue=True) + self._log_develop("Return from set from item end: {}, issue {}", _newvalue, _issue) _issue_dict = {_newvalue: _issue} if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['item']: self.__get_issues['item'].append(_issue_dict) return _newvalue # Determine value from variable - def __get_from_variable(self): + def __get_from_variable(self, var_get=None): def update_value(varname): value = self._abitem.get_variable(varname) new_value, _issue = self.__do_cast(value) new_value = 'var:{}'.format(varname) if new_value == '' else new_value - if isinstance(new_value, str) and 'Unknown variable' in new_value: + if isinstance(new_value, str) and '(Unknown variable' in new_value: _issue = "There is a problem with your variable {}".format(new_value) - self._log_warning(_issue) new_value = '' _issue_dict = {varname: _issue} - if _issue not in [[], None, [None]] and _issue_dict not in self.__get_issues['var']: + if _issue_dict not in self.__get_issues['var']: self.__get_issues['var'].append(_issue_dict) - self._log_debug("Checking variable '{0}', value {1} from list {2}", - varname, new_value, self.__listorder) if 'var:{}'.format(varname) in self.__listorder: self.__listorder[self.__listorder.index('var:{}'.format(varname))] = new_value return new_value - + _issue = "" values = [] - - if isinstance(self.__varname, list): - for var in self.__varname: + if var_get is None: + var_get = self.__varname + if isinstance(var_get, list): + for var in var_get: self._log_debug("Checking variable in loop '{0}', value {1} from list {2}", var, values[-1], self.__listorder) values.append(update_value(var)) else: - values = update_value(self.__varname) - self._log_debug("Variable result: {0}", values) + values = update_value(var_get) + + self._log_debug("Variable result: '{}'.", values) return values From f4814b7fce27a99e01e64077addfb517a51ed3a1 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 10:47:52 +0200 Subject: [PATCH 100/121] stateengine plugin: fix string to list conversion --- stateengine/StateEngineTools.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 2a892ceaa..8be8bb0b2 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -317,21 +317,25 @@ def partition_strip(value, splitchar): # value: list as string # returns: list or original value def convert_str_to_list(value, force=True): - if isinstance(value, str) and (value[:1] == '[' and value[-1:] == ']'): - value = value.strip("[]") + if isinstance(value, str): + value = value.strip() + if value.startswith('[') and value.endswith(']'): + value = value[1:-1].strip() if isinstance(value, str) and "," in value: try: - elements = re.findall(r"'([^']+)'|([^,]+)", value) - flattened_elements = [element[0] if element[0] else element[1] for element in elements] + elements = re.findall(r"'([^']*)'|\"([^\"]*)\"|([^,]+)", value) + flattened_elements = [element[0] if element[0] else (element[1] if element[1] else element[2].strip()) for + element in elements] + flattened_elements = [element.strip() for element in flattened_elements] formatted_elements = [] for element in flattened_elements: element = element.strip(" '\"") if "'" in element: - formatted_elements.append(f'"{element}"') + formatted_elements.append(f'"{element}"') # If element contains single quote, wrap in double quotes elif '"' in element: - formatted_elements.append(f"'{element}'") + formatted_elements.append(f"'{element}'") # If element contains double quote, wrap in single quotes else: - formatted_elements.append(f"'{element}'") + formatted_elements.append(f"'{element}'") # Default case, wrap in single quotes formatted_str = "[" + ", ".join(formatted_elements) + "]" return literal_eval(formatted_str) except Exception as ex: From d27ef45d91315bac4f9d23eb9eae0af3a8aa7dc6 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 10:48:18 +0200 Subject: [PATCH 101/121] stateengine plugin: reset issues list when setting a value (internal use only) --- stateengine/StateEngineValue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 4cb873448..344db0d01 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -165,7 +165,7 @@ def __resetvalue(self): # value: string indicating value or source of value # name: name of object ("time" is being handled differently) def set(self, value, name="", reset=True, copyvalue=True, returnvalue=False): - + self.__issues = [] if copyvalue is True: value = copy.copy(value) if reset: From 8d3e1bbf1d9a85524f6272a87120fc128aaaf22f Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 21:02:30 +0200 Subject: [PATCH 102/121] stateengine plugin: fix logging of action count --- stateengine/StateEngineState.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 905a969f8..7d0147125 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -695,8 +695,9 @@ def filter_issues(input_dict): for attribute in child_item.conf: self._log_develop("Filling state with {} action named {} for state {} with config {}", child_name, attribute, state.id, child_item.conf) action_method.update_action_details(self, action_type) - _action_counts[action_name] += 1 - _, _action_status = action_method.update(attribute, child_item.conf.get(attribute)) + _result = action_method.update(attribute, child_item.conf.get(attribute)) + _action_counts[action_name] += _result[0] if _result else 0 + _action_status = _result[1] if _action_status: update_action_status(action_name, _action_status) self._abitem.update_action_status(self.__action_status) @@ -716,7 +717,7 @@ def filter_issues(input_dict): update_action_status("enter_or_stay", _action_status) self._abitem.update_action_status(self.__action_status) - _total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] + _total_actioncount = _action_counts["enter"] + _action_counts["stay"] + _action_counts["enter_or_stay"] + _action_counts["leave"] + _action_counts["pass"] self.update_name(item_state, recursion_depth) # Complete condition sets and actions at the end From b1f977ff5b9c0cfc14f6b3aa673ccc05dff41148 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 21:02:56 +0200 Subject: [PATCH 103/121] stateengine plugin: fix list actions --- stateengine/StateEngineAction.py | 129 ++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 46 deletions(-) diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 70099d79d..d6653982a 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -287,10 +287,7 @@ def check_getitem_fromeval(self, check_item, check_value=None, check_mindelta=No check_item, _issue = self._abitem.return_item(item) _issue = { self._name: {'issue': _issue, 'issueorigin': [{'state': self._state.id, 'action': self._function}]}} - if check_value: - check_value.set_cast(check_item.cast) - if check_mindelta: - check_mindelta.set_cast(check_item.cast) + check_item, check_mindelta = self._cast_stuff(check_item, check_mindelta, check_value) self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) if self._abitem.id == check_item.property.path: self._caller += '_self' @@ -343,6 +340,12 @@ def eval_minagedelta(self, actioninfo, state): else: return False + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + return check_item, check_mindelta + def check_complete(self, state, check_item, check_status, check_mindelta, check_minagedelta, check_value, action_type, evals_items=None, use=None): _issue = {self._name: {'issue': None, 'issueorigin': [{'state': state.id, 'action': self._function}]}} @@ -380,45 +383,12 @@ def check_complete(self, state, check_item, check_status, check_mindelta, check_ if check_item is None and _issue[self._name].get('issue') is None: _issue = {self._name: {'issue': ['Item not defined in rules section'], 'issueorigin': [{'state': state.id, 'action': self._function}]}} - # missing status in action: Try to find it. - if check_status is None: - status = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self._name, 0, use) - if status is not None: - check_status, _issue = self._abitem.return_item(status) - _issue = {self._name: {'issue': _issue, - 'issueorigin': [{'state': state.id, 'action': self._function}]}} - - if check_mindelta.is_empty(): - mindelta = StateEngineTools.find_attribute(self._sh, state, "se_mindelta_" + self._name, 0, use) - if mindelta is not None: - check_mindelta.set(mindelta) if check_minagedelta.is_empty(): minagedelta = StateEngineTools.find_attribute(self._sh, state, "se_minagedelta_" + self._name, 0, use) if minagedelta is not None: check_minagedelta.set(minagedelta) - - if check_status is not None: - check_value.set_cast(check_status.cast) - check_mindelta.set_cast(check_status.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(check_status.property.path) - if self._abitem.id == check_status.property.path: - self._caller += '_self' - elif check_status is None: - if isinstance(check_item, str): - pass - elif check_item is not None: - check_value.set_cast(check_item.cast) - check_mindelta.set_cast(check_item.cast) - self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) - if self._abitem.id == check_item.property.path: - self._caller += '_self' - if _issue[self._name].get('issue') not in [[], [None], None]: - self._log_develop("Issue with {} action {}", action_type, _issue) - else: - _issue = {self._name: {'issue': None, - 'issueorigin': [{'state': state.id, 'action': self._function}]}} - + check_item, check_status, check_mindelta, check_value, _issue = self._get_status(check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue) return check_item, check_status, check_mindelta, check_minagedelta, check_value, _issue # Execute action (considering delay, etc) @@ -568,7 +538,9 @@ def _update_repeat_webif(value: bool): self._log_increase_indent() if _validitem: delay = 0 if self.__delay.is_empty() else self.__delay.get() - plan_next = self._se_plugin.scheduler_return_next(self._scheduler_name) + plan_next = None + if self._scheduler_name: + plan_next = self._se_plugin.scheduler_return_next(self._scheduler_name) if plan_next is not None and plan_next > self.shtime.now() or delay == -1: self._log_info("Action '{0}: Removing previous delay timer '{1}'.", self._name, self._scheduler_name) self._se_plugin.scheduler_remove(self._scheduler_name) @@ -684,6 +656,51 @@ def __init__(self, abitem, name: str): self._value = StateEngineValue.SeValue(self._abitem, "value") self._mindelta = StateEngineValue.SeValue(self._abitem, "mindelta", False, "num") + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + # missing status in action: Try to find it. + if check_status is None: + status = StateEngineTools.find_attribute(self._sh, state, "se_status_" + self._name, 0, use) + if status is not None: + check_status, _issue = self._abitem.return_item(status) + _issue = {self._name: {'issue': _issue, + 'issueorigin': [{'state': state.id, 'action': self._function}]}} + + if check_mindelta.is_empty(): + mindelta = StateEngineTools.find_attribute(self._sh, state, "se_mindelta_" + self._name, 0, use) + if mindelta is not None: + check_mindelta.set(mindelta) + + if check_status is not None: + self._log_develop("Casting value {} to status {}", check_value, check_status) + check_value.set_cast(check_status.cast) + check_mindelta.set_cast(check_status.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_status.property.path) + if self._abitem.id == check_status.property.path: + self._caller += '_self' + elif check_status is None: + if isinstance(check_item, str): + pass + elif check_item is not None: + self._log_develop("Casting value {} to item {}", check_value, check_item) + check_value.set_cast(check_item.cast) + check_mindelta.set_cast(check_item.cast) + self._scheduler_name = "{}-SeItemDelayTimer".format(check_item.property.path) + if self._abitem.id == check_item.property.path: + self._caller += '_self' + if _issue[self._name].get('issue') not in [[], [None], None]: + self._log_develop("Issue with {} action {}", action_type, _issue) + else: + _issue = {self._name: {'issue': None, + 'issueorigin': [{'state': state.id, 'action': self._function}]}} + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + if check_value: + check_value.set_cast(check_item.cast) + if check_mindelta: + check_mindelta.set_cast(check_item.cast) + return check_item, check_mindelta + def _getitem_fromeval(self): if self._item is None: return @@ -1329,12 +1346,17 @@ def __repr__(self): return "SeAction Add {}".format(self._name) def write_to_logger(self): - SeActionBase.write_to_logger(self) SeActionSetItem.write_to_logger(self) + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + return check_item, check_mindelta + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): - value = value if isinstance(value, list) else [value] self._log_debug("{0}: Add '{1}' to '{2}'.{3}", actionname, value, item.property.path, repeat_text) + value = value if isinstance(value, list) else [value] value = item.property.value + value self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable @@ -1354,9 +1376,14 @@ def __repr__(self): return "SeAction RemoveFirst {}".format(self._name) def write_to_logger(self): - SeActionBase.write_to_logger(self) SeActionSetItem.write_to_logger(self) + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + return check_item, check_mindelta + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] @@ -1385,9 +1412,14 @@ def __repr__(self): return "SeAction RemoveLast {}".format(self._name) def write_to_logger(self): - SeActionBase.write_to_logger(self) SeActionSetItem.write_to_logger(self) + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + return check_item, check_mindelta + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] @@ -1418,17 +1450,22 @@ def __repr__(self): return "SeAction RemoveAll {}".format(self._name) def write_to_logger(self): - SeActionBase.write_to_logger(self) SeActionSetItem.write_to_logger(self) + def _get_status(self, check_item, check_status, check_mindelta, check_value, state, use, action_type, _issue): + return check_item, check_status, check_mindelta, check_value, _issue + + def _cast_stuff(self, check_item, check_mindelta, check_value): + return check_item, check_mindelta + def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, value, source, current_condition=None, previous_condition=None, previousstate_condition=None, next_condition=None): currentvalue = item.property.value value = value if isinstance(value, list) else [value] for v in value: try: currentvalue = [i for i in currentvalue if i != v] - self._log_debug("{0}: Remove all '{1}' from '{2}'.{3}", - actionname, v, item.property.path, repeat_text) + self._log_debug("{0}: Remove all '{1}' from '{2}', value is now {3}.{4}", + actionname, v, item.property.path, currentvalue, repeat_text) except Exception as ex: self._log_warning("{0}: Remove all '{1}' from '{2}' failed: {3}", actionname, value, item.property.path, ex) From f78a5b17ddac733dd9597cedc44b47ae0e46e771 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Thu, 26 Sep 2024 22:10:36 +0200 Subject: [PATCH 104/121] stateengine plugin: fix issue tracking for action definitions, minor updates --- stateengine/StateEngineItem.py | 2 +- stateengine/StateEngineState.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineItem.py b/stateengine/StateEngineItem.py index 407dba038..b171a8823 100755 --- a/stateengine/StateEngineItem.py +++ b/stateengine/StateEngineItem.py @@ -1325,7 +1325,7 @@ def list_issues(v): self.__logger.info("") for entry, value in to_check: if 'issue' not in value: - text = "Definition {} not used in any action or condition.".format(entry) + text = "Definition {} not used in any action or condition.".format(value.get('attribute', entry)) self.__logger.info("{}", text) self.__logger.decrease_indent() diff --git a/stateengine/StateEngineState.py b/stateengine/StateEngineState.py index 7d0147125..b2ab0221a 100755 --- a/stateengine/StateEngineState.py +++ b/stateengine/StateEngineState.py @@ -242,7 +242,7 @@ def write_to_log(self): 'actions_stay': {}, 'actions_leave': {}, 'actions_pass': {}, - 'leave': False, 'enter': False, 'stay': False, + 'leave': False, 'enter': False, 'stay': False, 'pass': False, 'is_copy_for': None, 'releasedby': None}) self._log_decrease_indent() self._log_info("Finished Web Interface Update") @@ -573,6 +573,7 @@ def update_unused(used_attributes, attrib_type, attrib_name): used_attributes[nested_entry].update({attrib_type: attrib_name}) used_attributes[nested_entry].update(nested_dict) self.__used_attributes.update(used_attributes) + self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) def update_action_status(actn_type, action_status): def filter_issues(input_dict): @@ -613,8 +614,10 @@ def filter_issues(input_dict): # Add 'used in' and update with existing data flattened_dict[inner_key]['used in'] = key flattened_dict[inner_key].update(nested_dict) + self.__used_attributes = deepcopy(flattened_dict) self.__action_status = filter_issues(self.__action_status) + self._abitem.update_attributes(self.__unused_attributes, self.__used_attributes) if isinstance(state, SeState): item_state = state.state_item From 2f18d59a56293e414180fa7f91f08da49a70cca8 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Fri, 27 Sep 2024 20:54:52 +0200 Subject: [PATCH 105/121] stateengine: re-fix time handling --- stateengine/StateEngineValue.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 344db0d01..1bfc943d3 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -64,20 +64,28 @@ def __init__(self, abitem, name, allow_value_list=False, value_type=None): self.__valid_valuetypes = ["value", "regex", "eval", "var", "item", "template", "struct"] if value_type == "str": self.__cast_func = StateEngineTools.cast_str + self.__cast = "str" elif value_type == "regex": self.__cast_func = self.cast_regex + self.__cast = "regex" elif value_type == "num": self.__cast_func = StateEngineTools.cast_num + self.__cast = "num" elif value_type == "item": self.__cast_func = self.cast_item + self.__cast = "item" elif value_type == "bool": self.__cast_func = StateEngineTools.cast_bool + self.__cast = "bool" elif value_type == "time": self.__cast_func = StateEngineTools.cast_time + self.__cast = "time" elif value_type == "list": self.__cast_func = StateEngineTools.cast_list + self.__cast = "list" else: self.__cast_func = None + self.__cast = None def __repr__(self): return "{}".format(self.get()) @@ -156,6 +164,7 @@ def __resetvalue(self): self.__struct = None self.__varname = None self.__template = None + self.__cast = None self._additional_sources = [] self.__listorder = [] self.__type_listorder = [] @@ -170,6 +179,8 @@ def set(self, value, name="", reset=True, copyvalue=True, returnvalue=False): value = copy.copy(value) if reset: self.__resetvalue() + if name: + self.__cast = name returnvalues = [] if isinstance(value, list): source = [] @@ -273,7 +284,7 @@ def set(self, value, name="", reset=True, copyvalue=True, returnvalue=False): except Exception: cond1 = False cond2 = False - if name == "time" and cond1 and cond2: + if (name == "time" or self.__cast == "time") and cond1 and cond2: field_value = value source = "value" elif field_value == "": @@ -324,7 +335,7 @@ def set(self, value, name="", reset=True, copyvalue=True, returnvalue=False): except Exception: cond1 = False cond2 = False - if name == "time" and cond1 and cond2: + if (name == "time" or self.__cast == "time") and cond1 and cond2: field_value[i] = '{}:{}'.format(source[i], field_value[i]) s = "value" elif field_value[i] == "": From b8c4ee469a1356d4ba40fefe1ce7998851effae4 Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 28 Sep 2024 16:28:53 +0200 Subject: [PATCH 106/121] stateengine plugin: make it possible to set value of list item by se_set_..: [foo, bar] --- stateengine/StateEngineValue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stateengine/StateEngineValue.py b/stateengine/StateEngineValue.py index 1bfc943d3..3c0678e6a 100755 --- a/stateengine/StateEngineValue.py +++ b/stateengine/StateEngineValue.py @@ -726,7 +726,7 @@ def __do_cast(self, value, item_id=None): try: _newvalue = element if element == 'novalue' else self.__cast_func(element) except Exception as ex: - _newvalue = None + _newvalue = element _issue = "Problem casting element '{0}': {1}.".format(element, ex) self._log_warning(_issue) valuelist.append(_newvalue) @@ -777,7 +777,7 @@ def __do_cast(self, value, item_id=None): self._log_debug("Original casting of {} to {} failed. New cast is now: {}.", value, self.__cast_func, type(value)) return value, _issue - return None, _issue + return value, _issue return value, _issue # Determine value by using a struct From cb431df32e7334227b70b834d7350850f1d68a5b Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Sat, 28 Sep 2024 16:30:33 +0200 Subject: [PATCH 107/121] stateengine plugin: minor internal code update --- stateengine/StateEngineActions.py | 6 ++---- stateengine/StateEngineTools.py | 4 ++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/stateengine/StateEngineActions.py b/stateengine/StateEngineActions.py index 05f2b340a..75a64de47 100755 --- a/stateengine/StateEngineActions.py +++ b/stateengine/StateEngineActions.py @@ -82,8 +82,7 @@ def update(self, attribute, value): return _count, _issue elif isinstance(value, str): value = ":".join(map(str.strip, value.split(":"))) - if value[:1] == '[' and value[-1:] == ']': - value = StateEngineTools.convert_str_to_list(value, False) + value = StateEngineTools.convert_str_to_list(value, False) if name in self.__actions: self.__actions[name].update_action_details(self.__state, self.__action_type) if func == "se_delay": @@ -388,8 +387,7 @@ def remove_action(e): entry = list("{!s}:{!s}".format(k, v) for (k, v) in entry.items())[0] key, val = StateEngineTools.partition_strip(entry, ":") val = ":".join(map(str.strip, val.split(":"))) - if val[:1] == '[' and val[-1:] == ']': - val = StateEngineTools.convert_str_to_list(val, False) + val = StateEngineTools.convert_str_to_list(val, False) if key == "function": parameter[key] = StateEngineTools.cast_str(val) elif key == "force": diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index 8be8bb0b2..b423c9c6a 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -318,9 +318,13 @@ def partition_strip(value, splitchar): # returns: list or original value def convert_str_to_list(value, force=True): if isinstance(value, str): + orig_value = value value = value.strip() if value.startswith('[') and value.endswith(']'): value = value[1:-1].strip() + else: + return orig_value + if isinstance(value, str) and "," in value: try: elements = re.findall(r"'([^']*)'|\"([^\"]*)\"|([^,]+)", value) From 1a25e405387c82b8f1140696d1c3d6c2031490d0 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:16:32 +0200 Subject: [PATCH 108/121] database: add orphan reassignment --- database/__init__.py | 19 +++++++++++++++ database/webif/__init__.py | 5 ++-- database/webif/templates/base_database.html | 4 ++- database/webif/templates/index.html | 27 ++++++++++++++++++++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/database/__init__.py b/database/__init__.py index 65eb46b76..086251a90 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -967,6 +967,25 @@ def _count_orphanlogentries(self): return + def reassign_orphaned_id(self, orphan_id, to): + """ + Reassign values from orphaned item ID to given item ID + + :param orphan_id: item id of the orphaned item + :param to: item id of the target item + :type orphan_id: int + :type to: int + """ + self.logger.error(f'reassign called: {orphan_id} -> {to}') + cur = self._db_maint.cursor() + self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid;"), {'newid': to, 'orphanid': orphan_id}, cur=cur) + self._execute(self._prepare("DELETE FROM {item} WHERE id = :orphanid LIMIT 1;"), {'orphanid': orphan_id}, cur=cur) + self.logger.info(f'reassigned orphaned id {orphan_id} to new id {to}') + cur.close() + self._db_maint.commit() + self.logger.debug('rebuilding orphan list') + self.build_orphanlist() + def _delete_orphan(self, item_path): """ Delete orphan item or logentries it diff --git a/database/webif/__init__.py b/database/webif/__init__.py index 9c9178391..d31a4b270 100755 --- a/database/webif/__init__.py +++ b/database/webif/__init__.py @@ -63,7 +63,7 @@ def __init__(self, webif_dir, plugin): @cherrypy.expose def index(self, reload=None, action=None, item_id=None, item_path=None, time_end=None, day=None, month=None, year=None, - time_orig=None, changed_orig=None): + time_orig=None, changed_orig=None, orphanID=None, newID=None): """ Build index.html for cherrypy @@ -76,6 +76,8 @@ def index(self, reload=None, action=None, item_id=None, item_path=None, time_end if item_path is not None: item = self.plugin.items.return_item(item_path) delete_triggered = False + if orphanID is not None and newID is not None and orphanID != newID: + self.plugin.reassign_orphaned_id(orphanID, to=newID) if action is not None: if action == "delete_log" and item_id is not None: if time_orig is not None and changed_orig is not None: @@ -271,7 +273,6 @@ def db_sqldump(self): return - @cherrypy.expose def cleanup(self): self.plugin.cleanup() diff --git a/database/webif/templates/base_database.html b/database/webif/templates/base_database.html index 8a305091a..0bbea1793 100755 --- a/database/webif/templates/base_database.html +++ b/database/webif/templates/base_database.html @@ -31,7 +31,8 @@ { className: "time", targets: 2 }, { className: "type", targets: 3 }, { className: "id", targets: 4, render: $.fn.dataTable.render.number('.', ',', 0, '') }, - { className: "logcount", targets: 5, render: $.fn.dataTable.render.number('.', ',', 0, '') }, + { className: "reassign", targets: 5 }, + { className: "logcount", targets: 6, render: $.fn.dataTable.render.number('.', ',', 0, '') }, ].concat($.fn.dataTable.defaults.columnDefs)}); {% else %} orphantable = $('#orphantable').DataTable( { @@ -42,6 +43,7 @@ { className: "time", targets: 2 }, { className: "type", targets: 3 }, { className: "id", targets: 4, render: $.fn.dataTable.render.number('.', ',', 0, '') }, + { className: "reassign", targets: 5 }, ].concat($.fn.dataTable.defaults.columnDefs)}); {% endif %} diff --git a/database/webif/templates/index.html b/database/webif/templates/index.html index 6a52ed64b..9939883f1 100755 --- a/database/webif/templates/index.html +++ b/database/webif/templates/index.html @@ -158,6 +158,17 @@ {% set tab3title = _('Verwaiste Items') %} {% block bodytab3 %} + + +
    {% if p.remove_orphan or len(p.orphanlist) == 0 %} @@ -174,6 +185,7 @@ {{ _('Letzte Änderung') }} {{ _('Typ') }} {{ _('DB-ID') }} + {{ _('Neuzuweisung') }} {% if p.count_logentries %} {{ _('Anzahl Einträge') }} {% endif %} @@ -181,6 +193,7 @@ {% for item in p.orphanlist %} + {% set itemid = p.id(item, create=False) %} {{ item }} @@ -199,7 +212,19 @@ {% endif %} {% endif %} {{ _(p.db_itemtype(item)) }} - {{ p.id(item, create=False) }} + {{ itemid }} + + + + {% if p.count_logentries %} {{ p._orphan_logcount[p.id(item, create=False)] }} {% endif %} From 9fd2dd6f9cd004fba8530ef39f3f7e9b485e27d0 Mon Sep 17 00:00:00 2001 From: psilo909 Date: Sun, 13 Oct 2024 19:04:39 +0200 Subject: [PATCH 109/121] MVG_LIVE: - fixed an issue in station_id setting when not using async io with pypi lib --- mvg_live/__init__.py | 15 ++++++++++----- mvg_live/plugin.yaml | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/mvg_live/__init__.py b/mvg_live/__init__.py index 6ac5c092c..c78560788 100755 --- a/mvg_live/__init__.py +++ b/mvg_live/__init__.py @@ -27,7 +27,7 @@ class MVG_Live(SmartPlugin): ALLOW_MULTIINSTANCE = False - PLUGIN_VERSION = "1.6.0" + PLUGIN_VERSION = "1.6.1" def __init__(self, sh, *args, **kwargs): """ @@ -42,14 +42,19 @@ def stop(self): self.alive = False def get_station(self, station): - mvg_station = MvgApi.station(station) - if mvg_station: - return mvg_station + try: + mvg_station = MvgApi.station(station) + if mvg_station: + return mvg_station + except MvgApiError as e: + self.logger.error("MVGLive: Could not find %s: %s" % (ort, e)) def get_station_departures(self, station): mvg_station = self.get_station(station) + self.logger.error(mvg_station) if mvg_station: mvgapi = MvgApi(mvg_station['id']) + mvgapi.station_id = mvg_station['id'] return mvgapi.departures() else: - logger.error("Station %s does not exist."%station) \ No newline at end of file + self.logger.error("Station %s does not exist."%station) \ No newline at end of file diff --git a/mvg_live/plugin.yaml b/mvg_live/plugin.yaml index 65b810d52..260a57f28 100755 --- a/mvg_live/plugin.yaml +++ b/mvg_live/plugin.yaml @@ -12,8 +12,8 @@ plugin: documentation: http://smarthomeng.de/user/plugins_doc/config/mvg_live.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1108867-neues-plugin-mvg_live - version: 1.6.0 # Plugin version - sh_minversion: '1.5' # minimum shNG version to use this plugin + version: 1.6.1 # Plugin version + sh_minversion: '1.5' # 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 restartable: unknown From db181cf89cb9e4df3206d66e2a175a8bae79cb86 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 14 Oct 2024 07:34:24 +0200 Subject: [PATCH 110/121] yamahayxc: minor fixes --- yamahayxc/__init__.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/yamahayxc/__init__.py b/yamahayxc/__init__.py index a2ec97899..3591d27f9 100755 --- a/yamahayxc/__init__.py +++ b/yamahayxc/__init__.py @@ -29,7 +29,6 @@ # - parse zone().range_step -> read range/step for vol / eq -import logging import requests import socket import json @@ -53,20 +52,20 @@ class YamahaYXC(SmartPlugin): """ This is the main plugin class YamahaYXC to control YXC-compatible devices. """ - PLUGIN_VERSION = "1.0.6" + PLUGIN_VERSION = "1.0.7" ALLOW_MULTIINSTANCE = False # # public functions # - def __init__(self, smarthome): + def __init__(self, **kwargs): """ Default init function """ - self.logger = logging.getLogger(__name__) + super().__init__(**kwargs) + self.logger.info("Init YamahaYXC") - self._sh = smarthome # valid commands for use in item configuration 'yamahayxc_cmd = ...' self._yamaha_cmds = ['state', 'power', 'input', 'playback', 'preset', @@ -79,7 +78,7 @@ def __init__(self, smarthome): self._yamaha_ignore_cmds_upd = ['state', 'preset', 'alarm_on', 'alarm_time', 'alarm_beep'] # store items in 2D-array: - # _yamaha_dev [host] [cmd] = item + # _yamaha_dev [host] [cmd] = item # also see parse_item()... self._yamaha_dev = {} # store host addresses of devices @@ -106,13 +105,13 @@ def run(self): data, addr = self.sock.recvfrom(self.srv_buffer) try: host, port = addr - except: + except Exception as e: self.logger.warn( - "Error receiving data - host/port not readable") + f"Error receiving data - host/port not readable: {e}") return if host not in list(self._yamaha_dev.keys()): self.logger.debug( - "Received notify from unknown host {}".format(host)) + f"Received notify from unknown host {host}") else: # connected device sends updates every second for # about 10 minutes without further interaction @@ -128,11 +127,11 @@ def run(self): data_flat = {} try: data_flat.update(data['main']) - except: + except Exception: pass try: data_flat.update(data['netusb']) - except: + except Exception: pass # try all known command words... @@ -145,7 +144,7 @@ def run(self): notify_val = self._convert_value_yxc_to_plugin(data_flat[cmd], cmd, host) item = self._yamaha_dev[host][cmd] item(notify_val, "YamahaYXC") - except: + except Exception: pass # device told us new info is available? @@ -171,7 +170,7 @@ def stop(self): self.alive = False try: self.sock.shutdown(socket.SHUT_RDWR) - except: + except Exception: pass def parse_item(self, item): @@ -352,7 +351,7 @@ def _update_alarm_state(self, yamaha_host, update_items=True): alarm = state["alarm"] except KeyError: return - + alarm_on = alarm["alarm_on"] alarm_time = alarm["oneday"]["time"] alarm_beep = alarm["oneday"]["beep"] From 6d7e38dba65de6325cc499f163ec32422835cf46 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:27:22 +0200 Subject: [PATCH 111/121] database: change selection to modal dialogue --- database/__init__.py | 21 ++++++---- database/webif/__init__.py | 8 ++-- database/webif/static/style.css | 37 +++++++++++++++++ database/webif/templates/index.html | 64 +++++++++++++++++++++-------- 4 files changed, 102 insertions(+), 28 deletions(-) create mode 100644 database/webif/static/style.css diff --git a/database/__init__.py b/database/__init__.py index 086251a90..4128c5dc1 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -976,15 +976,18 @@ def reassign_orphaned_id(self, orphan_id, to): :type orphan_id: int :type to: int """ - self.logger.error(f'reassign called: {orphan_id} -> {to}') - cur = self._db_maint.cursor() - self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid;"), {'newid': to, 'orphanid': orphan_id}, cur=cur) - self._execute(self._prepare("DELETE FROM {item} WHERE id = :orphanid LIMIT 1;"), {'orphanid': orphan_id}, cur=cur) - self.logger.info(f'reassigned orphaned id {orphan_id} to new id {to}') - cur.close() - self._db_maint.commit() - self.logger.debug('rebuilding orphan list') - self.build_orphanlist() + try: + self.logger.warning(f'reassigning orphaned data from (old) id {orphan_id} to (new) id {to}') + cur = self._db_maint.cursor() + self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid;"), {'newid': to, 'orphanid': orphan_id}, cur=cur) + self._execute(self._prepare("DELETE FROM {item} WHERE id = :orphanid LIMIT 1;"), {'orphanid': orphan_id}, cur=cur) + self.logger.warning(f'reassigned orphaned id {orphan_id} to new id {to}') + cur.close() + self._db_maint.commit() + self.logger.warning('rebuilding orphan list') + self.build_orphanlist() + except Exception as e: + self.logger.error(f'error on reassigning id {orphan_id} to {to}: {e}') def _delete_orphan(self, item_path): """ diff --git a/database/webif/__init__.py b/database/webif/__init__.py index d31a4b270..79b10c6dd 100755 --- a/database/webif/__init__.py +++ b/database/webif/__init__.py @@ -63,7 +63,7 @@ def __init__(self, webif_dir, plugin): @cherrypy.expose def index(self, reload=None, action=None, item_id=None, item_path=None, time_end=None, day=None, month=None, year=None, - time_orig=None, changed_orig=None, orphanID=None, newID=None): + time_orig=None, changed_orig=None, orphan_id=None, new_id=None): """ Build index.html for cherrypy @@ -76,8 +76,10 @@ def index(self, reload=None, action=None, item_id=None, item_path=None, time_end if item_path is not None: item = self.plugin.items.return_item(item_path) delete_triggered = False - if orphanID is not None and newID is not None and orphanID != newID: - self.plugin.reassign_orphaned_id(orphanID, to=newID) + self.logger.error(f'index called with oid {orphan_id} and nid {new_id}') + if orphan_id is not None and new_id is not None and orphan_id != new_id: + self.logger.error(f'calling reassign for {orphan_id} and {new_id}') + self.plugin.reassign_orphaned_id(orphan_id, to=new_id) if action is not None: if action == "delete_log" and item_id is not None: if time_orig is not None and changed_orig is not None: diff --git a/database/webif/static/style.css b/database/webif/static/style.css new file mode 100644 index 000000000..94a8b7bf9 --- /dev/null +++ b/database/webif/static/style.css @@ -0,0 +1,37 @@ +/* The Modal (background) */ +.or-modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 999; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content/Box */ +.or-modal-content { + background-color: #fefefe; + margin: 15% auto; /* 15% from the top and centered */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ +} + +/* The Close Button */ +.or-close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.or-close:hover, +.or-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} \ No newline at end of file diff --git a/database/webif/templates/index.html b/database/webif/templates/index.html index 9939883f1..c65e8a91a 100755 --- a/database/webif/templates/index.html +++ b/database/webif/templates/index.html @@ -4,6 +4,10 @@ {% set dataSet = 'overview' %} {% set tab1title = _('Database Items') %} +{% block pluginstyles %} + +{% endblock pluginstyles %} + {%- block pluginscripts %} {{ super() }} +
    +
    + × +
    + Neuzuweisen von Itemreihe (ID ) +
    +
    + Bitte wählen Sie die Itemreihe aus, der die verwaisten Daten zugewiesen werden sollen: + + + +
    +
    +
    +
    {% if p.remove_orphan or len(p.orphanlist) == 0 %} @@ -214,16 +255,7 @@ {{ _(p.db_itemtype(item)) }} {{ itemid }} - - + {% if p.count_logentries %} {{ p._orphan_logcount[p.id(item, create=False)] }} From 8fac7513eee18cbd2ff3f92fd136f761ce8578e4 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:17:53 +0200 Subject: [PATCH 112/121] database: remove debug code --- database/__init__.py | 6 +++--- database/webif/__init__.py | 2 -- database/webif/templates/index.html | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/database/__init__.py b/database/__init__.py index 4128c5dc1..357714091 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -977,14 +977,14 @@ def reassign_orphaned_id(self, orphan_id, to): :type to: int """ try: - self.logger.warning(f'reassigning orphaned data from (old) id {orphan_id} to (new) id {to}') + self.logger.debug(f'reassigning orphaned data from (old) id {orphan_id} to (new) id {to}') cur = self._db_maint.cursor() self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid;"), {'newid': to, 'orphanid': orphan_id}, cur=cur) self._execute(self._prepare("DELETE FROM {item} WHERE id = :orphanid LIMIT 1;"), {'orphanid': orphan_id}, cur=cur) - self.logger.warning(f'reassigned orphaned id {orphan_id} to new id {to}') + self.logger.info(f'reassigned orphaned id {orphan_id} to new id {to}') cur.close() self._db_maint.commit() - self.logger.warning('rebuilding orphan list') + self.logger.debug('rebuilding orphan list') self.build_orphanlist() except Exception as e: self.logger.error(f'error on reassigning id {orphan_id} to {to}: {e}') diff --git a/database/webif/__init__.py b/database/webif/__init__.py index 79b10c6dd..e5425b6a8 100755 --- a/database/webif/__init__.py +++ b/database/webif/__init__.py @@ -76,9 +76,7 @@ def index(self, reload=None, action=None, item_id=None, item_path=None, time_end if item_path is not None: item = self.plugin.items.return_item(item_path) delete_triggered = False - self.logger.error(f'index called with oid {orphan_id} and nid {new_id}') if orphan_id is not None and new_id is not None and orphan_id != new_id: - self.logger.error(f'calling reassign for {orphan_id} and {new_id}') self.plugin.reassign_orphaned_id(orphan_id, to=new_id) if action is not None: if action == "delete_log" and item_id is not None: diff --git a/database/webif/templates/index.html b/database/webif/templates/index.html index c65e8a91a..c7c7a3605 100755 --- a/database/webif/templates/index.html +++ b/database/webif/templates/index.html @@ -177,7 +177,7 @@ document.getElementById('orphanSelect').selectedIndex = 0; // debug: show call parameters - alert("shngPost('', {orphan_id: " + orphanID + ", new_id: " + newID + "});"); + // alert("shngPost('', {orphan_id: " + orphanID + ", new_id: " + newID + "});"); // call index page with arguments shngPost('', {orphan_id: orphanID, new_id: newID}); From 8df822856180368005ceab1f8384bbd721b651e3 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:59:27 +0200 Subject: [PATCH 113/121] database: move to REST communication --- database/__init__.py | 1 + database/webif/__init__.py | 26 +++++++++++++++++++++++--- database/webif/templates/index.html | 22 +++++++++++++++++----- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/database/__init__.py b/database/__init__.py index 357714091..b66ae81b1 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -988,6 +988,7 @@ def reassign_orphaned_id(self, orphan_id, to): self.build_orphanlist() except Exception as e: self.logger.error(f'error on reassigning id {orphan_id} to {to}: {e}') + return e def _delete_orphan(self, item_path): """ diff --git a/database/webif/__init__.py b/database/webif/__init__.py index e5425b6a8..1fef30513 100755 --- a/database/webif/__init__.py +++ b/database/webif/__init__.py @@ -63,7 +63,7 @@ def __init__(self, webif_dir, plugin): @cherrypy.expose def index(self, reload=None, action=None, item_id=None, item_path=None, time_end=None, day=None, month=None, year=None, - time_orig=None, changed_orig=None, orphan_id=None, new_id=None): + time_orig=None, changed_orig=None): """ Build index.html for cherrypy @@ -76,8 +76,6 @@ def index(self, reload=None, action=None, item_id=None, item_path=None, time_end if item_path is not None: item = self.plugin.items.return_item(item_path) delete_triggered = False - if orphan_id is not None and new_id is not None and orphan_id != new_id: - self.plugin.reassign_orphaned_id(orphan_id, to=new_id) if action is not None: if action == "delete_log" and item_id is not None: if time_orig is not None and changed_orig is not None: @@ -132,6 +130,28 @@ def index(self, reload=None, action=None, item_id=None, item_path=None, time_end tabcount=2, action=action, item_id=item_id, delete_triggered=delete_triggered, language=self.plugin.get_sh().get_defaultlanguage()) + @cherrypy.expose + def reassign(self): + cl = cherrypy.request.headers['Content-Length'] + if not cl: + return + try: + rawbody = cherrypy.request.body.read(int(cl)) + data = json.loads(rawbody) + except Exception: + return + orphan_id = data.get("orphan_id") + new_id = data.get("new_id") + result = {"operation": "request", "result": "success"} + if orphan_id is not None and new_id is not None and orphan_id != new_id: + self.logger.info(f'reassigning orphaned id {orphan_id} to new id {new_id}') + err = self.plugin.reassign_orphaned_id(orphan_id, to=new_id) + if err: + return + return json.dumps(result) + else: + self.logger.warning(f'reassigning orphaned id {orphan_id} to new id {new_id} failed') + @cherrypy.expose def get_data_html(self, dataSet=None, params=None): """ diff --git a/database/webif/templates/index.html b/database/webif/templates/index.html index c7c7a3605..d19ecb524 100755 --- a/database/webif/templates/index.html +++ b/database/webif/templates/index.html @@ -179,11 +179,23 @@ // debug: show call parameters // alert("shngPost('', {orphan_id: " + orphanID + ", new_id: " + newID + "});"); - // call index page with arguments - shngPost('', {orphan_id: orphanID, new_id: newID}); - - // reload page to reflect recalculated orphans - setTimeout(window.location.reload(), 3000); + var mydata = {"orphan_id": orphanID, "new_id": newID}; + $.ajax({ + type: "POST", + url: "reassign", + data: JSON.stringify(mydata), + contentType: 'application/json', + dataType: 'json', + error: function() { + alert("Fehler beim Übermitteln der Daten. Bitte shng-Log prüfen!"); + document.getElementById('orphanModal').style.display = 'none'; + }, + success: function() { + document.getElementById('orphanModal').style.display = 'none'; + // reload page to reflect recalculated orphans + setTimeout(window.location.reload(), 3000); + } + }) } From 3da5851a919db17823e0d77fb68898cf864196e6 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:10:44 +0200 Subject: [PATCH 114/121] database: remove debug info from index.html --- database/webif/templates/index.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/database/webif/templates/index.html b/database/webif/templates/index.html index d19ecb524..173072106 100755 --- a/database/webif/templates/index.html +++ b/database/webif/templates/index.html @@ -176,9 +176,6 @@ document.getElementById('orphan-item-name').textContent = ""; document.getElementById('orphanSelect').selectedIndex = 0; - // debug: show call parameters - // alert("shngPost('', {orphan_id: " + orphanID + ", new_id: " + newID + "});"); - var mydata = {"orphan_id": orphanID, "new_id": newID}; $.ajax({ type: "POST", From 368b372db240bab98466b948d29bb7212bc350ca Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:11:23 +0200 Subject: [PATCH 115/121] database: raw string for regex --- database/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/__init__.py b/database/__init__.py index b66ae81b1..6b06f3223 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -1207,7 +1207,7 @@ def _expression(self, func): expression['finalizer'] = func[:func.index(":")] func = func[func.index(":") + 1:] if func == 'count' or func.startswith('count'): - parts = re.match('(count)((<>|!=|<|=|>)(\d+))?', func) + parts = re.match(r'(count)((<>|!=|<|=|>)(\d+))?', func) func = 'count' if parts and parts.group(3) is not None: expression['params']['op'] = parts.group(3) From ca3c8e81d7c296d14a2abdd8666fda9974423394 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:59:41 +0200 Subject: [PATCH 116/121] database: add max_reassign_logentries parameter --- database/__init__.py | 22 ++++++++++++++++------ database/plugin.yaml | 10 +++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/database/__init__.py b/database/__init__.py index b66ae81b1..e2d2ef0c3 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -51,7 +51,7 @@ class Database(SmartPlugin): """ ALLOW_MULTIINSTANCE = True - PLUGIN_VERSION = '1.6.12' + PLUGIN_VERSION = '1.6.13' # SQL queries: {item} = item table name, {log} = log table name # time, item_id, val_str, val_num, val_bool, changed @@ -104,6 +104,7 @@ def __init__(self, sh, *args, **kwargs): self._precision = self.get_parameter_value('precision') self.count_logentries = self.get_parameter_value('count_logentries') self.max_delete_logentries = self.get_parameter_value('max_delete_logentries') + self.max_reassign_logentries = self.get_parameter_value('max_reassign_logentries') self._default_maxage = float(self.get_parameter_value('default_maxage')) self._copy_database = self.get_parameter_value('copy_database') @@ -976,15 +977,24 @@ def reassign_orphaned_id(self, orphan_id, to): :type orphan_id: int :type to: int """ + log_info = self.logger.warning # info + log_debug = self.logger.error # debug try: - self.logger.debug(f'reassigning orphaned data from (old) id {orphan_id} to (new) id {to}') + log_info(f'reassigning orphaned data from (old) id {orphan_id} to (new) id {to}') cur = self._db_maint.cursor() - self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid;"), {'newid': to, 'orphanid': orphan_id}, cur=cur) + count = self.readLogCount(orphan_id, cur=cur) + log_debug(f'found {count} entries to reassign, reassigning {self.max_reassign_logentries} at once') + + while count > 0: + log_debug(f'reassigning {min(count, self.max_reassign_logentries)} log entries') + self._execute(self._prepare("UPDATE {log} SET item_id = :newid WHERE item_id = :orphanid LIMIT :limit;"), {'newid': to, 'orphanid': orphan_id, 'limit': self.max_reassign_logentries}, cur=cur) + count -= self.max_reassign_logentries + self._execute(self._prepare("DELETE FROM {item} WHERE id = :orphanid LIMIT 1;"), {'orphanid': orphan_id}, cur=cur) - self.logger.info(f'reassigned orphaned id {orphan_id} to new id {to}') + log_info(f'reassigned orphaned id {orphan_id} to new id {to}') cur.close() self._db_maint.commit() - self.logger.debug('rebuilding orphan list') + log_debug('rebuilding orphan list') self.build_orphanlist() except Exception as e: self.logger.error(f'error on reassigning id {orphan_id} to {to}: {e}') @@ -1207,7 +1217,7 @@ def _expression(self, func): expression['finalizer'] = func[:func.index(":")] func = func[func.index(":") + 1:] if func == 'count' or func.startswith('count'): - parts = re.match('(count)((<>|!=|<|=|>)(\d+))?', func) + parts = re.match(r'(count)((<>|!=|<|=|>)(\d+))?', func) func = 'count' if parts and parts.group(3) is not None: expression['params']['op'] = parts.group(3) diff --git a/database/plugin.yaml b/database/plugin.yaml index 3f4cbbafe..50f3e59fb 100755 --- a/database/plugin.yaml +++ b/database/plugin.yaml @@ -11,7 +11,7 @@ plugin: keywords: database support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1021844-neues-database-plugin - version: 1.6.12 # Plugin version + version: 1.6.13 # Plugin version sh_minversion: '1.9.3.2' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: True # plugin supports multi instance @@ -72,6 +72,14 @@ parameters: de: "Maximal auf einmal zu löschende Anzahl an Log Einträgen mit dem database_maxage Attribut, reduziert die Belastung der Datenbank bei alten Datenbeständen" en: "Maximum number of Logentries to delete at once with database_maxage attribute, reduces load on database with old datasets" + max_reassign_logentries: + type: int + default: 20 # 000 + valid_min: 10 # 00 + description: + de: "Maximal auf einmal neu zuzuweisende Anzahl an Log Einträgen, reduziert die Belastung der Datenbank bei großen Datenbeständen" + en: "Maximum number of Logentries to reassign at once, reduces load on database with large datasets" + default_maxage: type: int default: 0 From 3c3bec525393bc3b1e9c9715fe49804d2ed04eb6 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:05:21 +0200 Subject: [PATCH 117/121] yamahayxc: fix startup --- yamahayxc/__init__.py | 4 ++-- yamahayxc/plugin.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/yamahayxc/__init__.py b/yamahayxc/__init__.py index 3591d27f9..35975be9f 100755 --- a/yamahayxc/__init__.py +++ b/yamahayxc/__init__.py @@ -59,12 +59,12 @@ class YamahaYXC(SmartPlugin): # public functions # - def __init__(self, **kwargs): + def __init__(self, smarthome, **kwargs): """ Default init function """ super().__init__(**kwargs) - + self._sh = smarthome self.logger.info("Init YamahaYXC") # valid commands for use in item configuration 'yamahayxc_cmd = ...' diff --git a/yamahayxc/plugin.yaml b/yamahayxc/plugin.yaml index 9da7b64bf..57eff09f8 100755 --- a/yamahayxc/plugin.yaml +++ b/yamahayxc/plugin.yaml @@ -64,7 +64,7 @@ plugin: # documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1174064-plugin-yamaha-musiccast-geräte-neuere-generation - version: 1.0.6 # Plugin version + version: 1.0.7 # Plugin version sh_minversion: '1.3' # 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 From fdb96f8e8897914aee50e4f34e03de354d6e4b48 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sat, 19 Oct 2024 22:12:09 +0200 Subject: [PATCH 118/121] database: fix sql query --- database/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/__init__.py b/database/__init__.py index 65eb46b76..4593d1e50 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -988,7 +988,7 @@ def _delete_orphan(self, item_path): return True cur = self._db_maint.cursor() - self._execute(self._prepare("DELETE FROM {log} WHERE item_id = :id ORDER BY time ASC LIMIT :maxrecords;"), {'id': item_id, 'maxrecords': self.delete_orphan_chunk_size}, cur=cur) + self._execute(self._prepare("DELETE FROM {log} WHERE item_id = :id LIMIT :maxrecords;"), {'id': item_id, 'maxrecords': self.delete_orphan_chunk_size}, cur=cur) delete_orphan_chunk_size_str = f"{self.delete_orphan_chunk_size:,}".replace(',', '.') self.logger.info(f"_delete_orphan: Deleted (up to) {delete_orphan_chunk_size_str} log entries for Item {item_path}") cur.close() From 6a6f9ecb9ca13c15e1bc8e7c36de26e3bd30af7b Mon Sep 17 00:00:00 2001 From: psilo909 Date: Mon, 28 Oct 2024 18:07:05 +0100 Subject: [PATCH 119/121] Withings Health: Updated Doku --- withings_health/user_doc.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/withings_health/user_doc.rst b/withings_health/user_doc.rst index 5cada8c16..98395b516 100644 --- a/withings_health/user_doc.rst +++ b/withings_health/user_doc.rst @@ -23,7 +23,8 @@ Vorbereitung Dieses Plugin benötigt die withings-api. Sie müssen sich unter `Withings Account `_ registrieren und im Dashboard -eine Applikation anlegen. Der Name ist frei wählbar, die (lokale) Callback-URL wird über die Weboberfläche des Plugins angezeigt: http://:/plugin/withings_health. +eine Applikation anlegen. Der Name ist frei wählbar, die (lokale) Callback-URL wird über die Weboberfläche des Plugins +angezeigt: http://:/plugin/withings_health (Die IP sollte die von SmartHomeNG sein!). Wenn Sie sich bei der `Withings App `_ einloggen, kann die achtstellige Zahl in der URL ausgelesen und in der Pluginkonfiguration als user_id angegeben werden. From 63112a349117dc153e745ef795e5593cf4a9815e Mon Sep 17 00:00:00 2001 From: Onkel Andy Date: Wed, 30 Oct 2024 21:32:51 +0100 Subject: [PATCH 120/121] oppo plugin: fix reply for current subtitle --- oppo/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oppo/commands.py b/oppo/commands.py index 38a9cadb3..c11c7029d 100755 --- a/oppo/commands.py +++ b/oppo/commands.py @@ -29,7 +29,7 @@ 'language': {'read': True, 'write': False, 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': [r'@QAT OK (?:[A-Z]*) (?:\d{1,2})/(?:\d{1,2}) ([A-Za-z]*)', r'@UAT (?:[A-Z]{2}) (?:\d{2})/(?:\d{2}) ([A-Z]{3}) (?:[0-7.]*)']}, }, 'subtitle': { - 'current': {'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'@UST (\d{2})/(?:\d{2}) ([A-Z]{3})'}, + 'current': {'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'@UST (\d{2})/(?:\d{2}) (?:[A-Z]{3})'}, 'available': {'read': True, 'write': False, 'item_type': 'num', 'dev_datatype': 'raw', 'reply_pattern': r'@UST (?:\d{2})/(\d{2}) (?:[A-Z]{3})'}, 'language': {'read': True, 'write': False, 'item_type': 'str', 'dev_datatype': 'raw', 'reply_pattern': r'@UST (?:\d{2})/(?:\d{2}) ([A-Z]{3})'}, }, From e5fda4bf6c72b43bb82d022a3a18547c9935fff0 Mon Sep 17 00:00:00 2001 From: Morg42 <43153739+Morg42@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:14:57 +0100 Subject: [PATCH 121/121] plugins: adjust gitignore for symlinked priv_* dirs --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6a46b1f3c..c664aba43 100755 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,8 @@ nosetests.xml ehthumbs.db Thumbs.db -# don't upload private plugins -/priv_*/ +# don't upload private plugins or symlinked dirs +/priv_* # don't upload plugins loaded from develop to a release installation /*_dev/