diff --git a/hue2/user_doc.rst b/hue2/user_doc.rst index 9f8c9bba5..ea978cdba 100755 --- a/hue2/user_doc.rst +++ b/hue2/user_doc.rst @@ -25,7 +25,7 @@ Neue Features Das Plugin bietet im Vergleich zum **hue** Plugin zusätzlich folgende Features: -- Die Authorisierung an der Hue Bride ist in das Plugin integriert und erfolgt über das Webinferface des Plugins. +- Die Authorisierung an der Hue Bridge ist in das Plugin integriert und erfolgt über das Webinferface des Plugins. - Das Plugin hat eine Funktion um aktive Hue Bridges im lokalen Netzwerk zu finden. - Das Plugin unterstützt je Instanz im Gegensatz zum alten Plugin 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. diff --git a/hue_apiv2/__init__.py b/hue_apiv2/__init__.py new file mode 100755 index 000000000..f0a56bbf4 --- /dev/null +++ b/hue_apiv2/__init__.py @@ -0,0 +1,1125 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- 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 +# +# hue_apiv2 plugin to run with SmartHomeNG +# +# 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 qhue +import requests +import xmltodict + +# new for asyncio --> +import threading +import asyncio +from concurrent.futures import CancelledError +import time + +from aiohue import HueBridgeV2 +# <-- new for asyncio + +# for hostname retrieval for registering with the bridge +from socket import getfqdn + +from lib.model.smartplugin import * +from lib.item import Items + +from .webif import WebInterface + +from .discover_bridges import discover_bridges + +# 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 + +mapping_delimiter = '|' + + +class HueApiV2(SmartPlugin): + """ + Main class of the Plugin. Does all plugin specific stuff and provides + the update functions for the items + """ + + PLUGIN_VERSION = '0.2.0' # (must match the version specified in plugin.yaml) + + hue_sensor_state_values = ['daylight', 'temperature', 'presence', 'lightlevel', 'status'] + hue_sensor_config_values = ['reachable', 'battery', 'on', 'sunriseoffset', 'sunsetoffset'] + + br = None # Bridge object for communication with the bridge + bridge_config = {} + bridge_scenes = {} + bridge_sensors = {} + + v2bridge = None + devices = {} # devices connected to the hue bridge + + + def __init__(self, sh): + """ + Initalizes the plugin. + + If you need the sh object at all, use the method self.get_sh() to get it. There should be almost no need for + a reference to the sh object any more. + + Plugins have to use the new way of getting parameter values: + use the SmartPlugin method get_parameter_value(parameter_name). Anywhere within the Plugin you can get + the configured (and checked) value for a parameter by calling self.get_parameter_value(parameter_name). It + returns the value in the datatype that is defined in the metadata. + """ + + # Call init code of parent class (SmartPlugin) + super().__init__() + + # get the parameters for the plugin (as defined in metadata plugin.yaml): + #self.bridge_type = self.get_parameter_value('bridge_type') + self.bridge_serial = self.get_parameter_value('bridge_serial') + self.bridge_ip = self.get_parameter_value('bridge_ip') + self.bridge_port = self.get_parameter_value('bridge_port') + self.bridge_user = self.get_parameter_value('bridge_user') + + # polled for value changes by adding a scheduler entry in the run method of this plugin + self.sensor_items_configured = False # If no sensor items are configured, the sensor-scheduler is not started + self._default_transition_time = int(float(self.get_parameter_value('default_transitionTime'))*1000) + + self.discovered_bridges = [] + self.bridge = self.get_bridge_desciption(self.bridge_ip, self.bridge_port) + if False and self.bridge == {}: + # discover hue bridges on the network + self.discovered_bridges = self.discover_bridges() + + # self.bridge = self.get_parameter_value('bridge') + # self.get_bridgeinfo() + # self.logger.warning("Configured Bridge={}, type={}".format(self.bridge, type(self.bridge))) + + if self.bridge_serial == '': + self.bridge = {} + else: + # if a bridge is configured + # find bridge using its serial number + self.bridge = self.get_data_from_discovered_bridges(self.bridge_serial) + if self.bridge.get('serialNumber', '') == '': + self.logger.warning("Configured bridge {} is not in the list of discovered bridges, starting second discovery") + self.discovered_bridges = self.discover_bridges() + + if self.bridge.get('serialNumber', '') == '': + # if not discovered, use stored ip address + self.bridge['ip'] = self.bridge_ip + self.bridge['port'] = self.bridge_port + self.bridge['serialNumber'] = self.bridge_serial + self.logger.warning("Configured bridge {} is still not in the list of discovered bridges, trying with stored ip address {}:{}".format(self.bridge_serial, self.bridge_ip, self.bridge_port)) + + api_config = self.get_api_config_of_bridge('http://'+self.bridge['ip']+':'+str(self.bridge['port'])+'/') + self.bridge['datastoreversion'] = api_config.get('datastoreversion', '') + self.bridge['apiversion'] = api_config.get('apiversion', '') + self.bridge['swversion'] = api_config.get('swversion', '') + self.bridge['modelid'] = api_config.get('modelid', '') + + + self.bridge['username'] = self.bridge_user + if self.bridge.get('ip', '') != self.bridge_ip: + # if ip address of bridge has changed, store new ip address in configuration data + self.update_plugin_config() + + # dict to store information about items handled by this plugin + self.plugin_items = {} + + self.init_webinterface(WebInterface) + + return + + + # ---------------------------------------------------------------------------------- + + def run(self): + """ + Run method for the plugin + """ + self.logger.debug("Run method called") + + # Start the asyncio eventloop in it's own thread + # and set self.alive to True when the eventloop is running + self.start_asyncio(self.plugin_coro()) + + # self.alive = True # if using asyncio, do not set self.alive here. Set it in the session coroutine + +# while not self.alive: +# pass +# self.run_asyncio_coro(self.list_asyncio_tasks()) + + return + + + def stop(self): + """ + Stop method for the plugin + """ + self.logger.debug("Stop method called") + + # self.alive = False # if using asyncio, do not set self.alive here. Set it in the session coroutine + + # Stop the asyncio eventloop and it's thread + self.stop_asyncio() + return + + + # ---------------------------------------------------------------------------------- + + async def plugin_coro(self): + """ + Coroutine for the session that communicates with the hue bridge + + This coroutine opens the session to the hue bridge and + only terminate, when the plugin ois stopped + """ + self.logger.notice("plugin_coro started") + + self.logger.debug("plugin_coro: Opening session") + + host = '10.0.0.190' + appkey = 'uJLI9mXMgsPoV5g6FqKbKQwkKNdgjJmTcAv4SXYA' + #self.v2bridge = HueBridgeV2(host, appkey) + self.v2bridge = HueBridgeV2(self.bridge_ip, self.bridge_user) + + self.alive = True + self.logger.info("plugin_coro: Plugin is running (self.alive=True)") + + async with self.v2bridge: + self.logger.info(f"plugin_coro: Connected to bridge: {self.v2bridge.bridge_id}") + self.logger.info(f" - device id: {self.v2bridge.config.bridge_device.id}") + self.logger.info(f" - name : {self.v2bridge.config.bridge_device.metadata.name}") + + self.unsubscribe_function = self.v2bridge.subscribe(self.handle_event) + + try: + self.initialize_items_from_bridge() + except Exception as ex: + # catch exception to prevent plugin_coro from unwanted termination + self.logger.exception(f"Exception in initialize_items_from_bridge(): {ex}") + + # block: wait until a stop command is received by the queue + queue_item = await self.run_queue.get() + + self.alive = False + self.logger.info("plugin_coro: Plugin is stopped (self.alive=False)") + + self.logger.debug("plugin_coro: Closing session") + # husky2: await self.apiSession.close() + #self.unsubscribe_function() + + self.logger.notice("plugin_coro finished") + return + + + def handle_event(self, event_type, event_item, initialize=False): + """ + Callback function for bridge.subscribe() + """ + if isinstance(event_type, str): + e_type = event_type + else: + e_type = str(event_type.value) + if e_type == 'update': + if event_item.type.value == 'light': + self.update_light_items_from_event(event_item) + elif event_item.type.value == 'grouped_light': + self.update_group_items_from_event(event_item) + elif event_item.type.value == 'zigbee_connectivity': + lights = self.v2bridge.devices.get_lights(event_item.owner.rid) + if len(lights) > 0: + for light in lights: + mapping_root = light.id + mapping_delimiter + 'light' + mapping_delimiter + self.update_items_with_mapping(light, mapping_root, 'reachable', str(event_item.status.value) == 'connected', initialize) + self.update_items_with_mapping(light, mapping_root, 'connectivity', event_item.status.value, initialize) + mapping_root = event_item.id + mapping_delimiter + 'sensor' + mapping_delimiter + self.update_items_with_mapping(event_item, mapping_root, 'connectivity', event_item.status.value, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'reachable', str(event_item.status.value) == 'connected', initialize) + else: + mapping_root = event_item.id + mapping_delimiter + 'sensor' + mapping_delimiter + self.update_items_with_mapping(event_item, mapping_root, 'connectivity', event_item.status.value, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'reachable', str(event_item.status.value) == 'connected', initialize) + + device_name = self._get_device_name(event_item.owner.rid) + status = event_item.status.value + self.logger.notice(f"handle_event: '{event_item.type.value}' is unhandled - device='{device_name}', {status=} - event={event_item}") + device = self._get_device(event_item.owner.rid) + self.logger.notice(f" - {device=}") + sensors = self.v2bridge.devices.get_sensors(event_item.owner.rid) + self.logger.notice(f" - {sensors=}") + + elif event_item.type.value == 'button': + #device_name = self._get_device_name(event_item.owner.rid) + #control_id = event_item.metadata.control_id + #last_event = event_item.button.last_event.value + #sensor_id = event_item.id + #self.logger.notice(f"handle_event: '{event_item.type.value}' is handled - device='{device_name}', id={control_id}, last_event={last_event}, sensor={sensor_id} - event={event_item}") + self.update_button_items_from_event(event_item, initialize=initialize) + elif event_item.type.value == 'device_power': + self.update_devicepower_items_from_event(event_item, initialize=initialize) + elif event_item.type.value == 'geofence_client': + pass + else: + self.logger.notice(f"handle_event: Eventtype '{event_item.type.value}' is unhandled - event={event_item}") + else: + self.logger.notice(f"handle_event: Eventtype {event_type.value} is unhandled") + return + + def _get_device(self, device_id): + device = None + for d in self.v2bridge.devices: + if device_id in d.id: + device = d + break + return device + + def _get_device_name(self, device_id): + device = self._get_device(device_id) + if device is None: + return '-' + return device.metadata.name + + def _get_light_name(self, light_id): + name = '-' + for d in self.v2bridge.devices: + if light_id in d.lights: + name = d.metadata.name + return name + + def log_event(self, event_type, event_item): + + if event_item.type.value == 'geofence_client': + pass + elif event_item.type.value == 'light': + mapping = event_item.id + mapping_delimiter + event_item.type.value + mapping_delimiter + 'y' + self.logger.debug(f"handle_event: {event_type.value} {event_item.type.value}: '{self._get_light_name(event_item.id)}' {event_item.id_v1} {mapping=} {event_item.id} - {event_item=}") + elif event_item.type.value == 'grouped_light': + self.logger.notice(f"handle_event: {event_type.value} {event_item.type.value}: {event_item.id} {event_item.id_v1} - {event_item=}") + else: + self.logger.notice(f"handle_event: {event_type.value} {event_item.type.value}: {event_item.id} - {event_item=}") + return + + + def update_light_items_from_event(self, event_item, initialize=False): + + mapping_root = event_item.id + mapping_delimiter + event_item.type.value + mapping_delimiter + + if self.get_items_for_mapping(mapping_root + 'on') != []: + self.logger.notice(f"update_light_items_from_event: '{self._get_light_name(event_item.id)}' - {event_item}") + + if initialize: + self.update_items_with_mapping(event_item, mapping_root, 'name', self._get_light_name(event_item.id), initialize) + self.update_items_with_mapping(event_item, mapping_root, 'dict', {}, initialize) + + self.update_items_with_mapping(event_item, mapping_root, 'on', event_item.on.on, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'bri', event_item.dimming.brightness, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'xy', [event_item.color.xy.x, event_item.color.xy.y], initialize) + try: + mirek = event_item.color_temperature.mirek + except: + mirek = 0 + self.update_items_with_mapping(event_item, mapping_root, 'ct', mirek, initialize) + + return + + + def update_group_items_from_event(self, event_item, initialize=False): + if event_item.type.value == 'grouped_light': + mapping_root = event_item.id + mapping_delimiter + 'group' + mapping_delimiter + + if self.get_items_for_mapping(mapping_root + 'on') != []: + room = self.v2bridge.groups.grouped_light.get_zone(event_item.id) + name = room.metadata.name + if event_item.id_v1 == '/groups/0': + name = '(All lights)' + self.logger.notice(f"update_group_items_from_event: '{name}' - {event_item}") + + if initialize: + self.update_items_with_mapping(event_item, mapping_root, 'name', self._get_light_name(event_item.id), initialize) + self.update_items_with_mapping(event_item, mapping_root, 'dict', {}, initialize) + + self.update_items_with_mapping(event_item, mapping_root, 'on', event_item.on.on, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'bri', event_item.dimming.brightness, initialize) + + return + + + def update_button_items_from_event(self, event_item, initialize=False): + + mapping_root = event_item.id + mapping_delimiter + event_item.type.value + mapping_delimiter + + if initialize: + self.update_items_with_mapping(event_item, mapping_root, 'name', self._get_device_name(event_item.owner.rid) ) + + last_event = event_item.button.last_event.value + + #if mapping_root.startswith('2463dfc8-ee7f-4484-8901-3f5bbb319e4d'): + # self.logger.notice(f"update_button_items_from_event: Button1: {last_event}") + + self.update_items_with_mapping(event_item, mapping_root, 'event', last_event, initialize) + if last_event == 'initial_press': + self.update_items_with_mapping(event_item, mapping_root, 'initial_press', True, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'repeat', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'short_release', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'long_release', False, initialize) + if last_event == 'repeat': + self.update_items_with_mapping(event_item, mapping_root, 'initial_press', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'repeat', True, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'short_release', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'long_release', False, initialize) + if last_event == 'short_release': + self.update_items_with_mapping(event_item, mapping_root, 'initial_press', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'repeat', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'short_release', True, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'long_release', False, initialize) + if last_event == 'long_release': + self.update_items_with_mapping(event_item, mapping_root, 'initial_press', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'repeat', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'short_release', False, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'long_release', True, initialize) + + return + + + def update_devicepower_items_from_event(self, event_item, initialize=False): + + mapping_root = event_item.id + mapping_delimiter + event_item.type.value + mapping_delimiter + + if initialize: + self.update_items_with_mapping(event_item, mapping_root, 'name', self._get_device_name(event_item.owner.rid) ) + + self.update_items_with_mapping(event_item, mapping_root, 'power_status', event_item.power_state.battery_state.value, initialize) + self.update_items_with_mapping(event_item, mapping_root, 'battery_level', event_item.power_state.battery_level, initialize) + + return + + + def update_items_with_mapping(self, event_item, mapping_root, function, value, initialize=False): + + update_items = self.get_items_for_mapping(mapping_root + function) + + for item in update_items: + #if initialize: + # # set v2 id in config data + # config_data = self.get_item_config(item) + # self.logger.debug(f"update_items_with_mapping: setting config_data for id_v1={config_data['id_v1']} -> Setting id to {event_item.id}") + # config_data['id'] = event_item.id + item(value, self.get_fullname()) + + + def initialize_items_from_bridge(self): + """ + Initializing the item values with data from the hue bridge after connecting to in + """ + self.logger.debug('initialize_items_from_bridge: Start') + self.logger.notice(f"initialize_items_from_bridge: v2bridge={dir(self.v2bridge)}") + #self.v2bridge.lights.initialize(None) + for event_item in self.v2bridge.lights: + self.update_light_items_from_event(event_item, initialize=True) + for event_item in self.v2bridge.groups: + self.update_group_items_from_event(event_item, initialize=True) + for event_item in self.v2bridge.sensors: + #self.update_button_items_from_event(event_item, initialize=True) + self.handle_event('update', event_item, initialize=True) + + self.logger.debug('initialize_items_from_bridge: End') + return + + + +# ---------------------------------------------------------------------------------- + + 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. + """ + resource = self.get_iattr_value(item.conf, 'hue_apiv2_resource') + function = self.get_iattr_value(item.conf, 'hue_apiv2_function') + if self.has_iattr(item.conf, 'hue_apiv2_id') and self.has_iattr(item.conf, 'hue_apiv2_function') or \ + resource == 'scene' and function == 'activate_scene': + config_data = {} + id = self.get_iattr_value(item.conf, 'hue_apiv2_id') + if id is None: + id = 'None' + config_data['id'] = id + #config_data['id_v1'] = id + config_data['resource'] = self.get_iattr_value(item.conf, 'hue_apiv2_resource') + config_data['function'] = self.get_iattr_value(item.conf, 'hue_apiv2_function') + config_data['transition_time'] = self.get_iattr_value(item.conf, 'hue_apiv2_transition_time') + + config_data['name'] = '' # to be filled during initialization of v2bridge + #if self.has_iattr(item.conf, 'hue_apiv2_reference_light_id'): + # if config_data['resource'] == "group": + # config_data['hue_apiv2_reference_light_id'] = self.get_iattr_value(item.conf, 'hue_apiv2_reference_light_id') + + config_data['item'] = item + +# mapping = config_data['id_v1'] + mapping_delimiter + config_data['resource'] + mapping_delimiter + config_data['function'] + mapping = config_data['id'] + mapping_delimiter + config_data['resource'] + mapping_delimiter + config_data['function'] + + # updating=True, if not read only + if not config_data['function'] in ['reachable', 'battery'] and \ + not config_data['function'] in self.hue_sensor_state_values: + pass +# self.add_item(item, mapping=mapping, config_data_dict=config_data, updating=True) +# return self.update_item + self.add_item(item, mapping=mapping, config_data_dict=config_data) + + # alt: + self.logger.debug("parse item: {}".format(item)) + conf_data = {} + conf_data['id'] = self.get_iattr_value(item.conf, 'hue_apiv2_id') + conf_data['resource'] = self.get_iattr_value(item.conf, 'hue_apiv2_resource') + conf_data['function'] = self.get_iattr_value(item.conf, 'hue_apiv2_function') + if self.has_iattr(item.conf, 'hue_apiv2_reference_light_id'): + if conf_data['resource'] == "group": + conf_data['hue_apiv2_reference_light_id'] = self.get_iattr_value(item.conf, 'hue_apiv2_reference_light_id') + + conf_data['item'] = item + # store config in plugin_items + self.plugin_items[item.property.path] = conf_data + # set flags to schedule updates for sensors, lights and groups + if conf_data['resource'] == 'sensor': + # ensure that the scheduler for sensors will be started if items use sensor data + self.sensor_items_configured = True + + if conf_data['resource'] == 'group': + # bridge updates are allways scheduled + self.logger.debug("parse_item: configured group item = {}".format(conf_data)) + + # updating=True, if not read only + if not conf_data['function'] in ['reachable', 'battery'] and \ + not conf_data['function'] in self.hue_sensor_state_values: + self.add_item(item, mapping=mapping, config_data_dict=config_data, updating=True) + return self.update_item + #self.add_item(item, mapping=mapping, config_data_dict=config_data) + return + + if 'hue_apiv2_dpt3_dim' in item.conf: + return self.dimDPT3 + + + def parse_logic(self, logic): + """ + Default plugin parse_logic method + """ + if 'xxx' in logic.conf: + # self.function(logic['name']) + pass + + + def dimDPT3(self, item, caller=None, source=None, dest=None): + # Evaluation of the list values for the KNX data + # [1] for dimming + # [0] for direction + parent = item.return_parent() + + if item()[1] == 1: + # dimmen + if item()[0] == 1: + # up + parent(254, self.get_shortname()+"dpt3") + else: + # down + parent(-254, self.get_shortname()+"dpt3") + else: + parent(0, self.get_shortname()+"dpt3") + + + 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 + """ + 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.info(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 bridge -> {config_data=}") + + if config_data['resource'] == 'light': + self.update_light_from_item(config_data, item) + elif config_data['resource'] == 'group': + self.update_group_from_item(config_data, item) + elif config_data['resource'] == 'scene': + self.update_scene_from_item(config_data, item) + elif config_data['resource'] == 'sensor': + self.update_sensor_from_item(config_data, item) + elif config_data['resource'] == 'button': + pass + # self.update_button_from_item(config_data, item) + else: + self.logger.error(f"Resource '{config_data['resource']}' is not implemented") + + return + + + def update_light_from_item(self, config_data, item): + value = item() + self.logger.debug(f"update_light_from_item: config_data = {config_data}") + hue_transition_time = self._default_transition_time + if config_data['transition_time'] is not None: + hue_transition_time = int(float(config_data['transition_time']) * 1000) + + #self.logger.notice(f"update_light_from_item: function={config_data['function']}, hue_transition_time={hue_transition_time}, id={config_data['id']}") + if config_data['function'] == 'on': + if value: + self.run_asyncio_coro(self.v2bridge.lights.turn_on(config_data['id'], hue_transition_time)) + else: + self.run_asyncio_coro(self.v2bridge.lights.turn_off(config_data['id'], hue_transition_time)) + elif config_data['function'] == 'bri': + self.run_asyncio_coro(self.v2bridge.lights.set_brightness(config_data['id'], float(value), hue_transition_time)) + elif config_data['function'] == 'xy' and isinstance(value, list) and len(value) == 2: + self.run_asyncio_coro(self.v2bridge.lights.set_color(config_data['id'], value[0], value[1], hue_transition_time)) + elif config_data['function'] == 'ct': + self.run_asyncio_coro(self.v2bridge.lights.set_color_temperature(config_data['id'], value, hue_transition_time)) + elif config_data['function'] == 'dict': + if value != {}: + on = value.get('on', None) + bri = value.get('bri', None) + xy = value.get('xy', None) + if xy is not None: + xy = (xy[0], xy[1]) + ct = value.get('ct', None) + if bri or xy or ct: + on = True + transition_time = value.get('transition_time', None) + if transition_time is None: + transition_time = hue_transition_time + else: + transition_time = int(float(transition_time)*1000) + self.run_asyncio_coro(self.v2bridge.lights.set_state(config_data['id'], on, bri, xy, ct, transition_time=transition_time)) + elif config_data['function'] == 'bri_inc': + self.logger.warning(f"Lights: {config_data['function']} not implemented") + elif config_data['function'] == 'alert': + self.logger.warning(f"Lights: {config_data['function']} not implemented") + elif config_data['function'] == 'effect': + self.logger.warning(f"Lights: {config_data['function']} not implemented") + else: + # The following functions from the api v1 are not supported by the api v2: + # - hue, sat, ct + # - name (for display, reading is done from the device-name) + self.logger.notice(f"update_light_from_item: The function {config_data['function']} is not supported/implemented") + return + + + def update_scene_from_item(self, config_data, item): + + value = item() + self.logger.debug(f"update_scene_from_item: config_data = {config_data}") + hue_transition_time = self._default_transition_time + if config_data['transition_time'] is not None: + hue_transition_time = int(float(config_data['transition_time']) * 1000) + + if config_data['function'] == 'activate': + self.run_asyncio_coro(self.v2bridge.scenes.recall(id=config_data['id'])) + elif config_data['function'] == 'activate_scene': + #self.v2bridge.scenes.recall(id=value, dynamic=False, duration=hue_transition_time, brightness=float(bri)) + self.run_asyncio_coro(self.v2bridge.scenes.recall(id=value)) + elif config_data['function'] == 'name': + self.logger.warning(f"Scenes: {config_data['function']} not implemented") + return + + + def update_group_from_item(self, config_data, item): + value = item() + self.logger.debug(f"update_group_from_item: config_data = {config_data} -> value = {value}") + + hue_transition_time = self._default_transition_time + if config_data['transition_time'] is not None: + hue_transition_time = int(float(config_data['transition_time']) * 1000) + + #self.logger.notice(f"update_group_from_item: function={config_data['function']}, hue_transition_time={hue_transition_time}, id={config_data['id']}") + if config_data['function'] == 'on': + self.run_asyncio_coro(self.v2bridge.groups.grouped_light.set_state(config_data['id'], on=value, transition_time=hue_transition_time)) + elif config_data['function'] == 'bri': + self.run_asyncio_coro(self.v2bridge.groups.grouped_light.set_state(config_data['id'], on=True, brightness=float(value), transition_time=hue_transition_time)) + elif config_data['function'] == 'xy' and isinstance(value, list) and len(value) == 2: + self.run_asyncio_coro(self.v2bridge.groups.grouped_light.set_state(config_data['id'], on=True, color_xy=value, transition_time=hue_transition_time)) + elif config_data['function'] == 'ct': + self.run_asyncio_coro(self.v2bridge.groups.grouped_light.set_state(config_data['id'], on=True, color_temp=value, transition_time=hue_transition_time)) + elif config_data['function'] == 'dict': + if value != {}: + on = value.get('on', None) + bri = value.get('bri', None) + xy_in = value.get('xy', None) + xy = None + if xy_in is not None: + xy = (xy_in[0], xy_in[1]) + self.logger.notice(f"update_group_from_item: {xy_in=}, {xy=}, {type(xy)=}") + ct = value.get('ct', None) + if bri or xy or ct: + on = True + transition_time = value.get('transition_time', None) + if transition_time is None: + transition_time = hue_transition_time + else: + transition_time = int(float(transition_time)*1000) + self.run_asyncio_coro(self.v2bridge.groups.grouped_light.set_state(config_data['id'], on, bri, xy, ct, transition_time=transition_time)) + elif config_data['function'] == 'bri_inc': + self.logger.warning(f"Groups: {config_data['function']} not implemented") + elif config_data['function'] == 'alert': + self.logger.warning(f"Groups: {config_data['function']} not implemented") + elif config_data['function'] == 'effect': + self.logger.warning(f"Groups: {config_data['function']} not implemented") + else: + # The following functions from the api v1 are not supported by the api v2: + # - hue, sat, ct, name + self.logger.notice(f"update_group_from_item: The function {config_data['function']} is not supported/implemented") +### + + return + + try: + if plugin_item['function'] == 'activate_scene': + self.br.groups(plugin_item['id'], 'action', scene=value, transitiontime=hue_transition_time) + elif plugin_item['function'] == 'modify_scene': + self.br.groups(plugin_item['id'], 'scenes', value['scene_id'], 'lights', value['light_id'], 'state', **(value['state'])) + + except qhue.qhue.QhueException as e: + msg = f"{e}" + msg = f"update_light_from_item: item {plugin_item['item'].id()} - function={plugin_item['function']} - '{msg}'" + if msg.find(' 201 ') >= 0: + self.logger.info(msg) + else: + self.logger.error(msg) + + return + + + def update_sensor_from_item(self, config_data, value): + + self.logger.debug(f"update_sensor_from_item: config_data = {config_data}") + if config_data['function'] == 'name': + self.logger.warning(f"Sensors: {config_data['function']} not implemented") + return + + + def get_api_config_of_bridge(self, urlbase): + + url = urlbase + 'api/config' + api_config = {} + try: + r = requests.get(url) + if r.status_code == 200: + api_config = r.json() + except Exception as e: + self.logger.error(f"get_api_config_of_bridge: url='{url}' - Exception {e}") + return api_config + + + def get_data_from_discovered_bridges(self, serialno): + """ + Get data from discovered bridges for a given serial number + + :param serialno: serial number of the bridge to look for + :return: bridge info + """ + result = {} + for db in self.discovered_bridges: + if db['serialNumber'] == serialno: + result = db + break + if result == {}: + # if bridge is not in list of discovered bridges, rediscover bridges and try again + self.discovered_bridges = self.discover_bridges() + for db in self.discovered_bridges: + if db['serialNumber'] == serialno: + result = db + break + + if result != {}: + api_config = self.get_api_config_of_bridge(result.get('URLBase','')) + result['datastoreversion'] = api_config.get('datastoreversion', '') + result['apiversion'] = api_config.get('apiversion', '') + result['swversion'] = api_config.get('swversion', '') + result['modelid'] = api_config.get('modelid', '') + + return result + + + def poll_bridge(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. + """ + # # get the value from the device + # device_value = ... + #self.get_lights_info() + if self.bridge.get('serialNumber','') == '': + self.bridge_config = {} + self.bridge_scenes = {} + self.bridge_sensors = {} + return + else: + if self.br is not None: + try: + if not self.sensor_items_configured: + self.bridge_sensors = self.br.sensors() + except Exception as e: + self.logger.error(f"poll_bridge: Exception {e}") + + try: + self.bridge_config = self.br.config() + except Exception as e: + self.logger.info(f"poll_bridge: Bridge-config not supported - Exception {e}") + + try: + self.bridge_scenes = self.br.scenes() + except Exception as e: + self.logger.info(f"poll_bridge: Scenes not supported - Exception {e}") + + # update items with polled data + src = self.get_instance_name() + if src == '': + src = None + for pi in self.plugin_items: + plugin_item = self.plugin_items[pi] + if plugin_item['resource'] == 'scene': + value = self._get_scene_item_value(plugin_item['id'], plugin_item['function'], plugin_item['item'].id()) + if value is not None: + plugin_item['item'](value, self.get_shortname(), src) + if plugin_item['resource'] == 'group': + if not "hue_apiv2_reference_light_id" in plugin_item: + if plugin_item['function'] != 'dict' and plugin_item['function'] != 'modify_scene': + if plugin_item['function'] == 'on': + value = self._get_group_item_value(plugin_item['id'], 'any_on', plugin_item['item'].id()) + else: + value = self._get_group_item_value(plugin_item['id'], plugin_item['function'], plugin_item['item'].id()) + if value is not None: + plugin_item['item'](value, self.get_shortname(), src) + return + + + def poll_bridge_sensors(self): + """ + Polls for updates of sensors 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. + """ + # get the value from the device: poll data from bridge + if self.bridge.get('serialNumber','') == '': + self.bridge_sensors = {} + return + else: + if self.br is not None: + try: + self.bridge_sensors = self.br.sensors() + except Exception as e: + self.logger.error(f"poll_bridge_sensors: Exception {e}") + + # update items with polled data + src = self.get_instance_name() + if src == '': + src = None + for pi in self.plugin_items: + plugin_item = self.plugin_items[pi] + if plugin_item['resource'] == 'sensor': + value = self._get_sensor_item_value(plugin_item['id'], plugin_item['function'], plugin_item['item'].id()) + if value is not None: + plugin_item['item'](value, self.get_shortname(), src) + return + + + def _get_group_item_value(self, group_id, function, item_path): + """ + Update item that has hue_resource == 'group' + :param id: + :param function: + :return: + """ + result = '' + + return result + + + def _get_scene_item_value(self, scene_id, function, item_path): + """ + Update item that has hue_resource == 'scene' + :param id: + :param function: + :return: + """ + result = '' + try: + scene = self.bridge_scenes[scene_id] + except KeyError: + self.logger.error(f"poll_bridge: Scene '{scene_id}' not defined on bridge (item '{item_path}')") + return None + + if function == 'name': + result = scene['name'] + return result + + + def _get_sensor_item_value(self, sensor_id, function, item_path): + """ + Update item that has hue_resource == 'sensor' + :param id: + :param function: + :return: + """ + result = '' + try: + sensor = self.bridge_sensors[sensor_id] + except KeyError: + self.logger.error(f"poll_bridge_sensors: Sensor '{sensor_id}' not defined on bridge (item '{item_path}')") + return None + except Exception as e : + self.logger.exception(f"poll_bridge_sensors: Sensor '{sensor_id}' on bridge (item '{item_path}') - exception: {e}") + return None + if function in self.hue_sensor_state_values: + try: + result = sensor['state'][function] + except KeyError: + self.logger.warning( + f"poll_bridge_sensors: Function {function} not supported by sensor '{sensor_id}' (item '{item_path}')") + result = '' + elif function in self.hue_sensor_config_values: + try: + result = sensor['config'][function] + except KeyError: + self.logger.warning( + f"poll_bridge_sensors: Function {function} not supported by sensor '{sensor_id}' (item '{item_path}')") + result = '' + elif function == 'name': + result = sensor['name'] + return result + + + def update_plugin_config(self): + """ + Update the plugin configuration of this plugin in ../etc/plugin.yaml + + Fill a dict with all the parameters that should be changed in the config file + and call the Method update_config_section() + """ + conf_dict = {} + # conf_dict['bridge'] = self.bridge + conf_dict['bridge_serial'] = self.bridge.get('serialNumber','') + conf_dict['bridge_user'] = self.bridge.get('username','') + conf_dict['bridge_ip'] = self.bridge.get('ip','') + conf_dict['bridge_port'] = self.bridge.get('port','') + self.update_config_section(conf_dict) + return + + # ============================================================================================ + + def get_bridgeinfo(self): + if self.bridge.get('serialNumber','') == '': + self.br = None + self.bridge_config = {} + self.bridge_scenes = {} + self.bridge_sensors = {} + return + self.logger.info("get_bridgeinfo: self.bridge = {}".format(self.bridge)) + self.br = qhue.Bridge(self.bridge['ip']+':'+str(self.bridge['port']), self.bridge['username']) + try: + self.bridge_config = self.br.config() + self.bridge_scenes = self.br.scenes() + self.bridge_sensors = self.br.sensors() + except Exception as e: + self.logger.error(f"Bridge '{self.bridge.get('serialNumber','')}' returned exception {e}") + self.br = None + self.bridge_config = {} + self.bridge_scenes = {} + self.bridge_sensors = {} + return False + + return True + + + def get_bridge_desciption(self, ip, port): + """ + Get description of bridge + + :param ip: + :param port: + :return: + """ + br_info = {} + + protocol = 'http' + if str(port) == '443': + protocol = 'https' + + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + r = requests.get(protocol + '://' + ip + ':' + str(port) + '/description.xml', verify=False) + if r.status_code == 200: + xmldict = xmltodict.parse(r.text) + br_info['ip'] = ip + br_info['port'] = str(port) + br_info['friendlyName'] = str(xmldict['root']['device']['friendlyName']) + br_info['manufacturer'] = str(xmldict['root']['device']['manufacturer']) + br_info['manufacturerURL'] = str(xmldict['root']['device']['manufacturerURL']) + br_info['modelDescription'] = str(xmldict['root']['device']['modelDescription']) + br_info['modelName'] = str(xmldict['root']['device']['modelName']) + br_info['modelURL'] = str(xmldict['root']['device']['modelURL']) + br_info['modelNumber'] = str(xmldict['root']['device']['modelNumber']) + br_info['serialNumber'] = str(xmldict['root']['device']['serialNumber']) + br_info['UDN'] = str(xmldict['root']['device']['UDN']) + br_info['gatewayName'] = str(xmldict['root']['device'].get('gatewayName', '')) + + br_info['URLBase'] = str(xmldict['root']['URLBase']) + if br_info['modelName'] == 'Philips hue bridge 2012': + br_info['version'] = 'v1' + elif br_info['modelName'] == 'Philips hue bridge 2015': + br_info['version'] = 'v2' + else: + br_info['version'] = 'unknown' + + # get API information + api_config = self.get_api_config_of_bridge(br_info['URLBase']) + br_info['datastoreversion'] = api_config.get('datastoreversion', '') + br_info['apiversion'] = api_config.get('apiversion', '') + br_info['swversion'] = api_config.get('swversion', '') + br_info['modelid'] = api_config.get('modelid', '') + + return br_info + + + def discover_bridges(self): + bridges = [] + try: + #discovered_bridges = discover_bridges(mdns=True, upnp=True, httponly=True) + discovered_bridges = discover_bridges(upnp=True, httponly=True) + + except Exception as e: + self.logger.error("discover_bridges: Exception in discover_bridges(): {}".format(e)) + discovered_bridges = {} + + for br in discovered_bridges: + ip = discovered_bridges[br].split('/')[2].split(':')[0] + port = discovered_bridges[br].split('/')[2].split(':')[1] + br_info = self.get_bridge_desciption(ip, port) + + bridges.append(br_info) + + for bridge in bridges: + self.logger.info("Discoverd bridge = {}".format(bridge)) + + return bridges + + # -------------------------------------------------------------------------------------------- + + def create_new_username(self, ip, port, devicetype=None, timeout=5): + """ + Helper function to generate a new anonymous username on a hue bridge + + This method is a copy from the queue package without keyboard input + + :param ip: ip address of the bridge + :param devicetype: (optional) devicetype to register with the bridge. If unprovided, generates a device + type based on the local hostname. + :param timeout: (optional, default=5) request timeout in seconds + + :return: username/application key + + Raises: + QhueException if something went wrong with username generation (for + example, if the bridge button wasn't pressed). + """ + api_url = "http://{}/api".format(ip+':'+port) + try: + # for qhue versions v2.0.0 and up + session = requests.Session() + res = qhue.qhue.Resource(api_url, session, timeout) + except: + # for qhue versions prior to v2.0.0 + res = qhue.qhue.Resource(api_url, timeout) + res = qhue.qhue.Resource(api_url, timeout) + + if devicetype is None: + devicetype = "SmartHomeNG#{}".format(getfqdn()) + + # raises QhueException if something went wrong + try: + response = res(devicetype=devicetype, http_method="post") + except Exception as e: + self.logger.warning("create_new_username: Exception {}".format(e)) + return '' + else: + self.logger.info("create_new_username: Generated username = {}".format(response[0]["success"]["username"])) + return response[0]["success"]["username"] + + + def remove_username(self, ip, port, username, timeout=5): + """ + Remove the username/application key from the bridge + + This function works only up to api version 1.3.0 of the bridge. Afterwards Philips/Signify disbled + the removal of users through the api. It is now only possible through the portal (cloud serivce). + + :param ip: ip address of the bridge + :param username: + :param timeout: (optional, default=5) request timeout in seconds + :return: + + Raises: + QhueException if something went wrong with username deletion + """ + api_url = "http://{}/api/{}".format(ip+':'+port, username) + url = api_url + "/config/whitelist/{}".format(username) + self.logger.info("remove_username: url = {}".format(url)) + res = qhue.qhue.Resource(url, timeout) + + devicetype = "SmartHomeNG#{}".format(getfqdn()) + + # raises QhueException if something went wrong + try: + response = res(devicetype=devicetype, http_method="delete") + except Exception as e: + self.logger.error("remove_username: res-delete exception {}".format(e)) + response = [{'error': str(e)}] + + if not('success' in response[0]): + self.logger.warning("remove_username: Error removing username/application key {} - {}".format(username, response[0])) + else: + self.logger.info("remove_username: username/application key {} removed".format(username)) + + diff --git a/hue_apiv2/assets/webif_connect_1.jpg b/hue_apiv2/assets/webif_connect_1.jpg new file mode 100755 index 000000000..efd91fb48 Binary files /dev/null and b/hue_apiv2/assets/webif_connect_1.jpg differ diff --git a/hue_apiv2/assets/webif_connect_2.jpg b/hue_apiv2/assets/webif_connect_2.jpg new file mode 100755 index 000000000..f7233481e Binary files /dev/null and b/hue_apiv2/assets/webif_connect_2.jpg differ diff --git a/hue_apiv2/assets/webif_tab1.jpg b/hue_apiv2/assets/webif_tab1.jpg new file mode 100644 index 000000000..ce0e1d307 Binary files /dev/null and b/hue_apiv2/assets/webif_tab1.jpg differ diff --git a/hue_apiv2/assets/webif_tab2.jpg b/hue_apiv2/assets/webif_tab2.jpg new file mode 100644 index 000000000..d9b389b96 Binary files /dev/null and b/hue_apiv2/assets/webif_tab2.jpg differ diff --git a/hue_apiv2/assets/webif_tab3.jpg b/hue_apiv2/assets/webif_tab3.jpg new file mode 100644 index 000000000..4b0ba9483 Binary files /dev/null and b/hue_apiv2/assets/webif_tab3.jpg differ diff --git a/hue_apiv2/assets/webif_tab4.jpg b/hue_apiv2/assets/webif_tab4.jpg new file mode 100644 index 000000000..0d7816bc1 Binary files /dev/null and b/hue_apiv2/assets/webif_tab4.jpg differ diff --git a/hue_apiv2/assets/webif_tab5.jpg b/hue_apiv2/assets/webif_tab5.jpg new file mode 100644 index 000000000..8f42ae211 Binary files /dev/null and b/hue_apiv2/assets/webif_tab5.jpg differ diff --git a/hue_apiv2/assets/webif_tab6.jpg b/hue_apiv2/assets/webif_tab6.jpg new file mode 100644 index 000000000..78c55f6ef Binary files /dev/null and b/hue_apiv2/assets/webif_tab6.jpg differ diff --git a/hue_apiv2/assets/webif_tab7.jpg b/hue_apiv2/assets/webif_tab7.jpg new file mode 100644 index 000000000..93ef638aa Binary files /dev/null and b/hue_apiv2/assets/webif_tab7.jpg differ diff --git a/hue_apiv2/discover_bridges.py b/hue_apiv2/discover_bridges.py new file mode 100755 index 000000000..d41e62bf8 --- /dev/null +++ b/hue_apiv2/discover_bridges.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2021- Martin Sinn m.sinn@gmx.de +######################################################################### +# This file is part of the hue plugin for SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# 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 socket +import requests +import xmltodict +import logging +logger = logging.getLogger('discover_bridges') + +from datetime import datetime +import time + + +def get_bridge_desciptrion(ip, port): + """ + Get description of bridge + + :param ip: + :param port: + :return: + """ + br_info = {} + + protocol = 'http' + if str(port) == '443': + protocol = 'https' + + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + r = requests.get(protocol + '://' + ip + ':' + str(port) + '/description.xml', verify=False) + if r.status_code == 200: + xmldict = xmltodict.parse(r.text) + br_info['ip'] = ip + br_info['port'] = str(port) + br_info['friendlyName'] = str(xmldict['root']['device']['friendlyName']) + br_info['manufacturer'] = str(xmldict['root']['device']['manufacturer']) + br_info['manufacturerURL'] = str(xmldict['root']['device']['manufacturerURL']) + br_info['modelDescription'] = str(xmldict['root']['device']['modelDescription']) + br_info['modelName'] = str(xmldict['root']['device']['modelName']) + br_info['modelURL'] = str(xmldict['root']['device']['modelURL']) + br_info['modelNumber'] = str(xmldict['root']['device']['modelNumber']) + br_info['serialNumber'] = str(xmldict['root']['device']['serialNumber']) + br_info['UDN'] = str(xmldict['root']['device']['UDN']) + br_info['gatewayName'] = str(xmldict['root']['device'].get('gatewayName', '')) + + br_info['URLBase'] = str(xmldict['root']['URLBase']) + if br_info['modelName'] == 'Philips hue bridge 2012': + br_info['version'] = 'v1' + elif br_info['modelName'] == 'Philips hue bridge 2015': + br_info['version'] = 'v2' + else: + br_info['version'] = 'unknown' + + return br_info + + +discovered_bridges = {} # key: bridge_id, value: {, bridge_id, url_base} + +def add_discovered_bridge(ip, port): + + if port == 443: + protocol = 'https' + else: + protocol = 'http' + url_base = protocol + '://' + ip + ':' + str(port) + + bridge_id = '?' + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + r = requests.get(url_base + '/description.xml', verify=False) + if r.status_code == 200: + xmldict = xmltodict.parse(r.text) + bridge_id = xmldict['root']['device']['serialNumber'] + + if not bridge_id in discovered_bridges.keys(): + discovered_bridges[bridge_id] = url_base + + return + + +# ====================================================================================== +# Discover via mDNS +# + +from zeroconf import ServiceBrowser, Zeroconf + +class MyListener: + + services = {} + + def remove_service(self, zeroconf, type, name): + pass + + def update_service(self, zeroconf, type, name, info): + pass + + def add_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + self.services[name] = info + + +def discover_via_mdns(): + + zeroconf = Zeroconf() + listener = MyListener() + t1 = datetime.now() + browser = ServiceBrowser(zeroconf, "_hue._tcp.local.", listener) + + time.sleep(2) + + zeroconf.close() + t2 = datetime.now() + + for sv in listener.services: + service = listener.services[sv] + + ip = socket.gethostbyname(service.server) + if service.port == 443: + protocol = 'https' + else: + protocol = 'http' + bridge_id = service.properties[b'bridgeid'].decode() + + add_discovered_bridge(ip, service.port) + + +# ====================================================================================== +# Discover via UPnP +# + +try: + from .ssdp import discover as ssdp_discover +except: + from ssdp import discover as ssdp_discover + +def discover_via_upnp(): + + t1 = datetime.now() + ssdp_list = ssdp_discover("ssdp:all", timeout=10) + t2 = datetime.now() + + devices = [u for u in ssdp_list if (u.server is not None and 'IpBridge' in u.server)] + for d in devices: + ip_port = d.location.split('/')[2] + ip = ip_port.split(':')[0] + port = ip_port.split(':')[1] + + add_discovered_bridge(ip, port) + + +# ====================================================================================== +# Discover via Signify broker server +# + +def discover_via_broker(): + + import json + + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + try: + r = requests.get('https://discovery.meethue.com', verify=False) + if r.status_code == 200: + bridge_list = json.loads(r.text) + for br in bridge_list: + ip = br['internalipaddress'] + port = 443 + add_discovered_bridge(ip, port) + else: + logger.error(f"Problem at broker server: {r.status_code}") + except Exception as e: + logger.error(f"Problem at broker url: {e}") + return + + +# ====================================================================================== +# Discover Hue bridges +# + +def discover_bridges_by_method(mdns=True, upnp=True, broker=False, httponly=True): + + global discovered_bridges + discovered_bridges = {} + + if mdns: + discover_via_mdns() + if upnp: + discover_via_upnp() + if broker: + discover_via_broker() + + if httponly: + for br_id in discovered_bridges: + discovered_bridges[br_id] = discovered_bridges[br_id].replace(':443', ':80') + + return discovered_bridges + + +def discover_bridges(upnp=False, httponly=False): + + upnp_discovered_bridges = {} + if upnp: + # discover via upnp + discover_bridges_by_method(mdns=False, upnp=True, broker=False, httponly=httponly) + upnp_discovered_bridges = discovered_bridges.copy() + + # discover via mDNS + discover_bridges_by_method(mdns=True, upnp=False, broker=False, httponly=httponly) + if discovered_bridges != {}: + if upnp_discovered_bridges != {}: + for key, value in upnp_discovered_bridges.items(): + discovered_bridges[key] = value + return discovered_bridges + + # discover via broker server + discover_bridges_by_method(mdns=False, upnp=False, broker=True, httponly=httponly) + if discovered_bridges != {}: + if upnp_discovered_bridges != {}: + for key, value in upnp_discovered_bridges.items(): + discovered_bridges[key] = value + return discovered_bridges + + return discovered_bridges + + +def test_all_methods(): + + discover_bridges_by_method(mdns=False, upnp=False, broker=True, httponly=False) + print("\nDiscover via broker") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + discover_bridges_by_method(mdns=False, upnp=False, broker=True, httponly=True) + print("\nDiscover via broker (http-only):") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + discover_bridges_by_method(upnp=False, broker=False, httponly=False) + print("\nDiscover mDNS") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + discover_bridges_by_method(upnp=False, broker=False) + print("\nDiscover mDNS (http-only):") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + from ssdp import discover as ssdp_discover + + discover_bridges_by_method(mdns=False, broker=False, httponly=False) + print("\nDiscover upnp") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + discover_bridges_by_method(mdns=False, broker=False) + print("\nDiscover upnp (http-only):") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + discover_bridges_by_method(broker=True) + print("\nDiscover all:") + for br_id in discovered_bridges: + print(f" {br_id}: {discovered_bridges[br_id]}") + + +if __name__ == '__main__': + + my_discovered_bridges = discover_bridges(upnp=True, httponly=True) + print("\nDiscover all:") + for br_id in my_discovered_bridges: + print(f" {br_id}: {my_discovered_bridges[br_id]}") diff --git a/hue_apiv2/locale.yaml b/hue_apiv2/locale.yaml new file mode 100755 index 000000000..d6ddad116 --- /dev/null +++ b/hue_apiv2/locale.yaml @@ -0,0 +1,26 @@ +# translations for the web interface +plugin_translations: + # Translations for the plugin specially for the web interface + 'Bridge suchen': {'de': '=', 'en': 'Look for Bridge'} + 'Gefundene Bridges': {'de': '=', 'en': 'Discovered Bridges'} + 'Verbinden': {'de': '=', 'en': 'Connect'} + 'Serien-Nr.': {'de': '=', 'en': 'Serial'} + 'Hersteller': {'de': '=', 'en': 'Manufacturer'} + 'Version': {'de': '=', 'en': '='} + 'Modell Name': {'de': '=', 'en': 'Model Name'} + 'Modell Nr.': {'de': '=', 'en': 'Model No'} + 'IP': {'de': '=', 'en': 'IP address'} + 'UDN': {'de': '=', 'en': '='} + 'Konfigurierte Bridge': {'de': '=', 'en': 'Configured Bridges'} + 'Anwendungsschlüssel': {'de': '=', 'en': 'Application Key'} + 'True': {'de':'Ja', 'en': 'Yes'} + 'False': {'de':'Nein', 'en': 'No'} + + # Alternative format for translations of longer texts: + 'Es ist keine Bridge mit dieser Plugin Instanz verbunden.': + de: '=' + en: 'No bridge is commected to this plugin instance.' + 'Zum Verbinden den Knopf an der Hue Bridge drücken und anschließend in der Liste der gefundenen Bridges den Button "Verbinden" drücken.': + de: '=' + en: 'To connect first press the link button on the hue bridge and then press the button "Connect" in the list of found bridges.' + diff --git a/hue_apiv2/plugin.yaml b/hue_apiv2/plugin.yaml new file mode 100755 index 000000000..9969794d3 --- /dev/null +++ b/hue_apiv2/plugin.yaml @@ -0,0 +1,427 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: gateway # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Anbindung des Philips/Signify Hue Systems über eine Hue Bridge unter Nutzung des neuen API v2' + en: 'Gateway for connection to the Philips/Signify Hue system through a bridge using the new API v2' + maintainer: msinn +# tester: # Who tests this plugin? + state: develop # change to ready when done with development +# keywords: iot xyz +# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1586861-support-thread-für-das-hue2-plugin + + version: 0.2.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: True # plugin supports multi instance + restartable: unknown + configuration_needed: False # False: The plugin will be enabled by the Admin GUI without configuration + classname: HueApiV2 # class containing the plugin + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) + default_transitionTime: + type: float + default: 0.4 + valid_min: 0 + description: + de: 'Zeit in sekunden welche die Leuchte benötigt um in einen neuen Zustand überzugehen' + en: 'Time in seconds required for the light to transition to a new state.' + + bridge_serial: + type: str + gui_type: readonly + description: + de: 'Seriennummer der Philips/Signify Hue Bridge + Dieser Parameter wird durch das Plugins in den Konfigurationsdaten abgelegt. Er ist in der Admin GUI nicht änderbar' + en: 'Serial number of the Philips/Signify hue bridge + The plugin ist saving this dats to the configuration. This parameter cannot be changed in the admin GUI' + + bridge_user: + type: str + gui_type: readonly + description: + de: 'Username/Anwendungsschlüssel um auf die Philips/Signify Hue Bridge zuzugreifen + Dieser Parameter wird durch das Plugins in den Konfigurationsdaten abgelegt. Er ist in der Admin GUI nicht änderbar' + en: 'Username/application key to access to the Philips/Signify hue bridge + The plugin ist saving this dats to the configuration. This parameter cannot be changed in the admin GUI' + + bridge_ip: + type: ip + gui_type: readonly + description: + de: 'IP Adresse der Philips/Signify Hue Bridge + Dieser Parameter wird durch das Plugins in den Konfigurationsdaten abgelegt. Er ist in der Admin GUI nicht änderbar' + en: 'ip address of the Philips/Signify hue bridge + The plugin ist saving this dats to the configuration. This parameter cannot be changed in the admin GUI' + + bridge_port: + type: int + gui_type: readonly + default: 80 + valid_min: 0 + description: + de: 'Port der Philips/Signify Hue Bridge + Dieser Parameter wird durch das Plugins in den Konfigurationsdaten abgelegt. Er ist in der Admin GUI nicht änderbar' + en: 'Port of the Philips/Signify hue bridge + The plugin ist saving this dats to the configuration. This parameter cannot be changed in the admin GUI' + +item_attributes: + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + + hue_apiv2_dpt3_dim: + type: bool + description: + de: "Aktiviert DPT3 dimmen" + en: "Enabled DPT3 dimming" + + hue_apiv2_transition_time: + type: num + description: + de: "Zeit für Übergang (in sec)" + en: "Time for transition (in sec)" + + hue_apiv2_resource: + type: str + description: + de: "Anzusteuernde Resource, falls nicht angegeben wird 'light' angenommen" + en: "Resource type to control/read" + valid_list: + - light + - group + - scene + - sensor + - button + - device_power + + hue_apiv2_reference_light_id: + type: str + description: + de: "ID der des referenzwert gebenden Lichtes. Nur möglich wenn resource == group" + en: "ID of the light giving the reference value. Only possible if resource == group" + + + hue_apiv2_id: + type: str + description: + de: "ID der anzusteuernden resouce. Der Typ der resource wird mit 'hue_apiv2_recource' festgelegt." + en: "ID of the resource to conteol/read. The type of the resoucre is defined by 'hue_apiv2_recource' attribute" + + hue_apiv2_function: + type: str + description: + de: "Anzusteuernde Funktion für die gewählte Resource/ID" + en: "Function of the selected resource/id that shall be read/controlled" + valid_list: + - '' + - on + - bri + - bri_inc + - xy + - ct + - dict + - name + - reachable + - connectivity + - type + - modelid + - swversion + - activate + - activate_scene + - modify_scene + - alert + - effect + - daylight + - sunriseoffset + - sunsetoffset + - temperature + - presence + - lightlevel + - status + - event + - initial_press + - repeat + - short_release + - long_release + - power_status + - battery_level + + valid_list_description: + de: + - "" + - "Ein-/Ausschalten -> bool, r/w (resource: light, group)" + - "Helligkeit in Prozent, 0 … 100 -> num, r/w (resource: light, group)" + - "Relative Helligkeitsveränderung 0 … 254, w/o (resource: light, group)" + - "xy Werte -> list, r/w (resource: light, group)" + - "ct Wert -> num, w/o (resource: light, group)" + - "Mehrere Funktionen auf einmal -> dict, w/o (resource: light, group)" + - "Name -> str, r/w (resource: light, group, scene, sensor)" + - "Erreichbar -> bool, r/o (resource: light)" + - "Connectivity status -> str, r/o (resource: light)" + - "Typ des Leuchtmittels -> str, r/o (resource: light)" + - "Model Id des Leuchtmittels -> str, r/o (resource: light)" + - "Software Version des Leuchtmittels -> str, r/o (resource: light)" + - "Aktivieren einer Szene für Leuchten -> bool w/o (resource: scene)" + - "Aktivieren einer Szene (Szenen-Id aus dem Item Wert) für Leuchten -> str w/o (resource: scene)" + - "Anpassen einer Szene für Leuchten in der selektierten Gruppe (hue_apiv2_id) -> str w/o (resource: group)" + - "Benachrichtigung -> str (Werte: none, select, lselect) (resource: light, group)" + - "Lichteffekt -> str (Werte: none, colorloop) (resource: light, group)" + - "Tageslicht -> bool, r/o (resource: sensor)" + - "Tageslicht: sunriseoffset -> num r/o (resource: sensor)" + - "Tageslicht: sunsetoffset -> num r/o (resource: sensor)" + - "Temperatur -> num r/o (resource: sensor)" + - "Präsenz -> bool, r/o (resource: sensor)" + - "Sensor 'lightlevel' -> num r/o (resource: sensor)" + - "Sensor 'status' -> num, r/o (resource: sensor)" + - "Event, welches der Button sendet -> str, r/o (resource: button)" + - "True, wenn der Button gerade gedrückt wurde -> bool, r/o (resource: button)" + - "True, wenn der Button repeat sendet -> bool, r/o (resource: button)" + - "True, wenn der Button kurz gedrückt wurde -> bool, r/o (resource: button)" + - "True, wenn der Button lang gedrückt wurde -> bool, r/o (resource: button)" + - "Power Status -> str, r/o (resource: device_power)" + - "Batterie Ladung in Prozent -> num, r/o (resource: device_power)" + en: + - "" + - "On/Off -> bool, r/w (resource: light, group)" + - "Brightness, 0 … 100 -> num, r/w (resource: light, group)" + - "Relative change of brightness 0 … 254, w/o (resource: light, group)" + - "xy values -> list, r/w (resource: light, group)" + - "ct value -> num, w/o (resource: light, group)" + - "Multiple functions at once -> dict, w/o (resource: light, group)" + - "Name -> str, r/w (resource: light, group, scene, sensor)" + - "Reachable -> bool, r/o (resource: light)" + - "Connectivity status -> str, r/o (resource: light)" + - "Typ of lamp -> str, r/o (resource: light)" + - "Model id of lamp -> str, r/o (resource: light)" + - "Software version lamp -> str, r/o (resource: light)" + - "Activate a scene for lights -> bool w/o (resource: scene)" + - "Activate a scene (scene-id from item value) for lights -> str w/o (resource: scene)" + - "Change a scene for lights in the selected group (hue_apiv2_id) -> str w/o (resource: group)" + - "Alert -> str (values: none, select, lselect) (resource: light, group)" + - "Effect -> str (values: none, colorloop) (resource: light, group)" + - "Sensor 'daylight' -> bool, r/o (resource: sensor)" + - "Sensor 'daylight': sunriseoffset -> num r/o (resource: sensor)" + - "Sensor 'daylight': sunsetoffset -> num r/o (resource: sensor)" + - "Sensor 'temperature' -> num r/o (resource: sensor)" + - "Sensor 'presence' -> bool, r/o (resource: sensor)" + - "Sensor 'lightlevel' -> num r/o (resource: sensor)" + - "Sensor 'status' -> num, r/o (resource: sensor)" + - "Event which is sent by the button -> str, r/o (resource: button)" + - "True, if the button has just been pressed -> bool, r/o (resource: button)" + - "True, if the button sends 'repeat' -> bool, r/o (resource: button)" + - "True, if the button was pressed short-> bool, r/o (resource: button)" + - "True, if the button was pressed long -> bool, r/o (resource: button)" + - "Power Status -> str, r/o (resource: device_power)" + - "Battery level in percent -> num, r/o (resource: device_power)" + +item_structs: + # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + light: + name: Vorlage-Struktur für eine Hue Leuchte + struct: hue_apiv2._light_basic + + xy: + type: list + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: xy + + + light_ww: + name: Vorlage-Struktur für eine Hue Leuchte (warm white) + struct: hue_apiv2._light_basic + + + light_xy: + name: Vorlage-Struktur für eine Hue Leuchte (Farbe nur über xy) + struct: hue_apiv2._light_basic + + xy: + type: list + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: xy + + + light_extended: + name: Vorlage-Struktur für eine Hue Leuchte mit erweiteten Attributen/Sub-Items + struct: + - hue_apiv2.light + - hue_apiv2._light_extended + + + light_ww_extended: + name: Vorlage-Struktur für eine Hue Leuchte mit erweiteten Attributen/Sub-Items (warm white) + struct: + - hue_apiv2.light_ww + - hue_apiv2._light_extended + + + light_xy_extended: + name: Vorlage-Struktur für eine Hue Leuchte mit erweiteten Attributen/Sub-Items (Farbe nur über xy) + struct: + - hue_apiv2.light_xy + - hue_apiv2._light_extended + + + group_activate_scene: + name: Vorlage-Struktur zum aktivieren von Hue Szenen + type: str + hue_apiv2_resource@instance: group + hue_apiv2_function@instance: activate_scene + #hue_apiv2_id@instance: 1 + + group: + name: Vorlage-Struktur für eine Hue Gruppe + + type: foo + hue_apiv2_resource@instance: group + #hue_apiv2_id@instance: 1 + + onoff: + type: bool + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: on + + level: + type: num + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: bri + + ct: + type: num + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: ct + + xy: + type: list + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: xy + + dict: + type: dict + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: dict + + + _light_basic: + name: interne Vorlage-Struktur für eine Hue Leuchte (basic) + + type: foo + hue_apiv2_resource@instance: light + #hue_apiv2_id@instance: 1 + + onoff: + type: bool + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: on + + level: + type: num + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: bri + + level_inc: + type: num + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: bri_inc + + ct: + type: num + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: ct + + dict: + type: dict + enforce_updates: True + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: dict + + alert: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: alert + + effect: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: effect + + + _light_extended: + name: interne Vorlage-Struktur für eine Hue Leuchte (extended attributes) + lightname: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: name + + reachable: + type: bool + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: reachable + + connectivity: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: connectivity + + lighttype: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: type + + modelid: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: modelid + + swversion: + type: str + hue_apiv2_resource@instance: ..:. + hue_apiv2_id@instance: ..:. + hue_apiv2_function@instance: swversion + + + +#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/hue_apiv2/requirements.txt b/hue_apiv2/requirements.txt new file mode 100755 index 000000000..6f6057843 --- /dev/null +++ b/hue_apiv2/requirements.txt @@ -0,0 +1,22 @@ +#xmltodict +#qhue +# zeroconf below v0.27, because newer versions need intensive testing and actual version has dropped support for Python 3.6 +#zeroconf<=0.26.3 +#Zeroconf >= 0.28 for testing (to resolve conflict with appletv plugin) +#zeroconf<=0.28.3 + +#zeroconf>0.28.3,<=0.31 # funktioniert anscheinend +# gibt folgenden Console output: +# ?gleiche? Version von zeroconf: consolidated = , further = >0.28.3, used by ["plugin 'hue2'"] + +#zeroconf>=0.32,<0.33 # 0.32.1 funktioniert anscheinend +#zeroconf>=0.33,<0.38 # 0.37 funktioniert anscheinend +#zeroconf>=0.38,<0.39 # 0.38.7 funktioniert anscheinend (nicht ganz -> Warnung im Log: ) +# 2023-05-19 11:50:15 WARNING lib.smarthome The following threads have not been terminated properly by their plugins (please report to the plugin's author): +# 2023-05-19 11:50:15 WARNING lib.smarthome -Thread: zeroconf-ServiceBrowser-_hue._tcp-29820, still alive +# 2023-05-19 11:50:15 WARNING lib.smarthome -Thread: zeroconf-ServiceBrowser-_hue._tcp-29871, still alive +# 2023-05-19 11:50:15 WARNING lib.smarthome -Thread: zeroconf-ServiceBrowser-_hue._tcp-29916, still alive + +#zeroconf<=0.52.0 + +aiohue diff --git a/hue_apiv2/ssdp.py b/hue_apiv2/ssdp.py new file mode 100755 index 000000000..f2d2a43a5 --- /dev/null +++ b/hue_apiv2/ssdp.py @@ -0,0 +1,88 @@ +# Copyright 2014 Dan Krause, Python 3 hack 2016 Adam Baxter, +# Server field addition and Win32 mod 2017 Andre Wagner +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import socket +import http.client +import io +import sys +from urllib.parse import urlsplit +import logging +logger = logging.getLogger('ssdp') + +class SSDPResponse(object): + class _FakeSocket(io.BytesIO): + def makefile(self, *args, **kw): + return self + def __init__(self, response): + r = http.client.HTTPResponse(self._FakeSocket(response)) + r.begin() + self.location = r.getheader("location") + self.usn = r.getheader("usn") + self.st = r.getheader("st") + self.cache = r.getheader("cache-control").split("=")[1] + self.server = r.getheader("server") + def __repr__(self): + return "".format(**self.__dict__) + +def discover(service, timeout=5, retries=1, mx=3): + group = ("239.255.255.250", 1900) + message = "\r\n".join([ + 'M-SEARCH * HTTP/1.1', + 'HOST: {0}:{1}', + 'MAN: "ssdp:discover"', + 'ST: {st}','MX: {mx}','','']) + socket.setdefaulttimeout(timeout) + responses = {} + for _ in range(retries): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + message_bytes = message.format(*group, st=service, mx=mx).encode('utf-8') + + # see https://stackoverflow.com/questions/32682969 + if sys.platform == "win32": + hosts = socket.gethostbyname_ex(socket.gethostname())[2] + for host in hosts: + sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(host)) + sock.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(group[0]) + socket.inet_aton(host)) + logger.debug('M-SEARCH on %s', host) + sock.sendto(message_bytes, group) + else: + logger.debug('M-SEARCH') + sock.sendto(message_bytes, group) + + while True: + try: + response = SSDPResponse(sock.recv(1024)) + responses[response.location] = response + logger.debug('Response from %s',urlsplit(response.location).netloc) + except socket.timeout: + break + return list(responses.values()) + +# Example: +# import ssdp +# ssdp.discover("roku:ecp") +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, \ + format='%(asctime)s.%(msecs)03d %(levelname)s:%(module)s:%(funcName)s: %(message)s', \ + datefmt="%Y-%m-%d %H:%M:%S") + + devices = discover("ssdp:all", timeout=8) + print('\nDiscovered {} device{pl}.'.format(len(devices), pl='s' if len(devices)!=1 else '')) + for device in devices: + print(' {0.location} ==> {0.server}'.format(device)) + diff --git a/hue_apiv2/sv_widgets/widget_hue2.html b/hue_apiv2/sv_widgets/widget_hue2.html new file mode 100755 index 000000000..b3ae35846 --- /dev/null +++ b/hue_apiv2/sv_widgets/widget_hue2.html @@ -0,0 +1,83 @@ +/** + * Hue Widget + * (c) 2020 Martin Sinn + * + * @param unique id for this widget + * @param the gad/item for hue lamp + * @param the gad/item for g_alert_effect (show buttons alert and effect if not empty) + * + */ +{% macro color_control(id, g_lamp, g_alert_effect) %} +{% import config_version_full >= "3.2.c" ? "@widgets/basic.html" : "basic.html" as basic %} + + + + + + + {% if g_alert_effect != '' %} + + + {% endif %} + +
+ {{ basic.stateswitch(id~'power', g_lamp~'.onoff', 'mini', ['0','1']) }} + + {{basic.symbol(id~'reachon', g_lamp~'.reachable', '', icon1~'it_wifi.png', '1')}} + {{basic.symbol(id~'reachoff', g_lamp~'.reachable', '', icon0~'it_wifi.png', '0')}} + + {{basic.color(id~'hsv', g_lamp~'.hue', g_lamp~'.sat', g_lamp~'.level' ,[0,0,0], [65535,255,255], '', 8, '', 'hsl')}} + + Alert:{{ basic.stateswitch(id~'alert',g_lamp~'.alert', 'mini', ['lselect','none'], [icon1~'audio_pause.png', icon1~'audio_play.png']) }} + + Effect:{{ basic.stateswitch(id~'effect', g_lamp~'.effect', 'mini', ['colorloop','none'], [icon1~'audio_pause.png', icon1~'audio_play.png']) }} +
+ + + + + + + + + + + + + +
+ Helligkeit + + {{basic.slider(id~'bri', g_lamp~'.level', 0, 255, 5)}} +
+ Sättigung + + {{basic.slider(id~'sat',g_lamp~'.sat', 0, 255, 5)}} +
+ Farbe + + {{basic.slider(id~'hue',g_lamp~'.hue', 0, 65535, 500)}} +
+{% endmacro %} + +/** + * Hue Widget + * (c) 2020 Martin Sinn + * + * @param unique id for this widget + * @param the gad/item for hue lamp + * + */ +{% macro attributes(id, g_lamp) %} +{% import config_version_full >= "3.2.c" ? "@widgets/basic.html" : "basic.html" as basic %} + + Name: {{ basic.print(id~'.lightname', g_lamp~'.lightname') }}
+ Type: {{ basic.print(id~'.lighttype', g_lamp~'.lighttype') }}
+ Model: {{ basic.print(id~'.modelid', g_lamp~'.modelid') }}
+ SW Vers.: {{ basic.print(id~'.swversion', g_lamp~'.swversion') }}
+ colormode: {{ basic.print(id~'.colormode', g_lamp~'.colormode') }}
+ reachable: {{ basic.symbol(id~'.reachable', g_lamp~'.reachable', ['Ja', 'Nein']) }}
+
+ ct {{ basic.slider(id~'.ct', g_lamp~'.ct', 153, 500) }} + +{% endmacro %} diff --git a/hue_apiv2/user_doc.rst b/hue_apiv2/user_doc.rst new file mode 100755 index 000000000..cc4b69e99 --- /dev/null +++ b/hue_apiv2/user_doc.rst @@ -0,0 +1,348 @@ +.. index:: Plugins; hue_apiv2 +.. index:: hue_apiv2 hue + +========= +hue_apiv2 +========= + +.. image:: webif/static/img/plugin_logo.png + :alt: plugin logo + :width: 300px + :height: 300px + :scale: 50 % + :align: left + + +Das Plugin unterstützt Philips/Signify Hue Bridges. Es setzt auf dem API v2 von Philips auf und unterstützt daher +nur die aktuellen Bridges der zweiten Generation (die Eckigen). Diese Bridges unterstützen zwar auch noch das API v1, +allerdings soll laut Philips diese Unterstützung irgendwann enden. Neue Hue Features werden allerdings nur im API v2 +implementiert. + +Wenn eine Philips Hue Bridge der ersten Generation (die Runden) angesprochen werden soll, muss das bisherige +**Plugin hue2** verwendet werden. Diese Bridges sind von Philips inzwischen retired worden und werden keine +Unterstützung durch Philips mehr erfahren. + +Das Hue API v2 enthält gegenüber dem API v1 eine Reihe von breaking Changes, so dass die Entwicklung eines neuen +Plugin notwendig wurde, um das neue API zu unterstützen. + +Die wichtigsten Features/Änderungen beim API v2: + +- Aktive Meldung von Veränderungen durch die Bridge +- https Verbindung statt http +- neue (längere) Ids (z.B.: 2915002c-6c8f-4d9b-9134-6b1a8ded4be3) +- Unterstützung mehrerer Lights in einem Device +- Andere Ansteuerung von Szenen +- Konzentration auf das Farbsystem xy +- Keine Unterstützung für hue und sat Werte +- Unterstützung von ct nur beim setzen von Werten (keine ct Werte von der Bridge) +- Keine Untersützung von bri_inc +- brightness Werte in Prozent (also 0 ... 100, nicht mehr 0 ... 255) +- Keine Unterstützung durch die alten (runden) Bridges (und deCONZ) + +| + +Das Plugin ist noch nicht FeatureComplete. + +**Ab hie muss die Doku noch überarbeitet werden:** + +| + +Neue Features +============= + +Das Plugin bietet im Vergleich zum **hue** Plugin zusätzlich folgende Features: + +- Die Authorisierung an der Hue Bride ist in das Plugin integriert und erfolgt über das Webinferface des Plugins. +- Das Plugin hat eine Funktion um aktive Hue Bridges im lokalen Netzwerk zu finden. +- Das Plugin unterstützt je Instanz im Gegensatz zum alten Plugin 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. +- Zur Vereinfachten Einrichtung von Items liefert das Plugin Struktur Templates. +- Funktionalitäten von Hue Gruppen werden großteils unterstützt. + + +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. + + +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. Abfrage Zyklus) +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/hue2` beschrieben. + + +Bridge verbinden +---------------- + +Die Herstellung der Verbindung zu einer Bridge erfolgt für das Web Interface des Plugins. Dazu in der Admin GUI +die **Liste der Plugins** aufrufen und in der Zeile des Hue Plugins auf den Button **Web IF** klicken. (Das +Webinterface ist weiter unten ausführlich beschrieben.) + +Das Web Interface zeigt wenn keine Bridge mit der Instanz des Plugins verbunden ist, automatisch den Tab +**Hue Bridge** an. + +.. image:: assets/webif_connect_1.jpg + :class: screenshot + +Um die Verbindung zu einer Bridge herzustellen, zuerst den Link-Button auf der Bridge drücken und anschließend +den **Verbinden** Button neben der entsprechenden Bridge im Web Interface klicken. + +Sollte die gewünschte Bridge in der Liste der gefundenen Bridges nicht angezeigt werden, kann über den Button +**Bridge suchen** eine neue Suche nach Hue Bridges im LAN gestartet werden. Nach Abschluß der Suche zum Verbinden +wie oben beschrieben verfahren. + +Anschließend wird die verbundende Bridge mit dem generierten Anwendungsschlüssel/Username angezeigt. Diese +Konfiguration des Plugins wird automatisch gespeichert. (in der Konfigurationsdatei ``/etc/plugin.yaml``) + +.. image:: assets/webif_connect_2.jpg + :class: screenshot + + +.. note:: + + Es wird auch bei einer neuen bisher nicht genutzten Bridge ein Sensor gefunden, da der **Daylight Sensor** + Bestandteil der Firmware der Hue Bridges ist. + +| + +Verwendung von structs +---------------------- + +Mit der Hilfe von Struktur Templates wird die Einrichtung von Items stark vereinfacht. Hierzu werden für +Leuchten Templates vom Plugin mitgeliefert. + +Grundsätzliche Item Definitionen für Leuchten: + +- **hue2.light** - Standard Definition für Philips Hue Leuchten +- **hue2.light_ww** - Standard Definition für Philips Warmwhite Leuchten +- **hue2.light_xy** - Standard Definition für Leuchten von Dritt-Anbietern, die kein **sat** und **hue** unterstützen, sondern nur **xy** + +Erweiterte Item Definitionen für oben genannten Leuchten-Typen: + +- **hue2.light_extended** +- **hue2.light_ww_extended** +- **hue2.light_xy_extended** + + +Ein Item für eine Hue Leuchte kann einfach folgendermaßen konfiguriert werden, indem nur die Id der zu +steuernden Leuchts als ``hue2_id`` angegeben wird: + +.. code-block:: yaml + + test_leuchte: + hue2_id: 3 + struct: hue2.light + +Damit werden zum Item ``test_leuchte`` die Sub-Items ``onoff``, ``level``, ``hue``, ``sat`` und ``ct`` definiert +und passend konfiguriert. + +Das hat die selbe Wirkung, als hätte man ohne Struktur Template folgende Item-Konfiguration vorgenommen: + +.. code-block:: yaml + + test_leuchte: + hue2_id: 3 + + name: Vorlage-Struktur für eine Hue Leuchte + type: foo + hue2_resource: light + + onoff: + type: bool + hue2_resource: ..:. + hue2_id: ..:. + hue2_function: on + + level: + type: num + hue2_resource: ..:. + hue2_id: ..:. + hue2_function: bri + + hue: + type: num + hue2_resource: ..:. + hue2_id: ..:. + hue2_function: hue + + sat: + type: num + hue2_resource: ..:. + hue2_id: ..:. + hue2_function: sat + + ct: + type: num + hue2_resource: ..:. + hue2_id: ..:. + hue2_function: ct + + +Das Struktur Template **hue2.light_extended** definiert zusätzlich noch die Sub-Items ``light_name``, ``reachable``, +``colormode``, ``xy``, ``light_type``, ``modelid`` und ``swversion``. Die Sub-Items +``reachable``, ``colormode``, ``light_type``, ``modelid`` und ``swversion`` können nur aus der Bridge gelesen +werden. Änderungen an dem Item werden von der Bridge ignoriert. + + +Item Attribute +-------------- + +Das Plugin verwendet drei Item Attribute: ``hue2_resource``, ``hue2_id`` und ``hue2_function``. + +Mit ``hue2_resource`` wird festgelegt, auf welche Resource der Bridge zugegriffen werden soll: ``light``, ``group``, +``scene`` oder ``sensor``. + +.. note:: + + Bisher sind nur die Resouce-Typen ``light``, ``group`` und ``sensor`` implementiert. + +Mit ``hue2_id`` wird festgelegt auf welche Resource des gewählten Typs zugegriffen werden soll. Die Id kann im +Web Interface im Tab des entsprechenden Resource-Typs nachgesehen werden. + +Mit ``hue2_function`` wird festgelegt, welche Funktion der gewählten Resource abgefragt oder gesteuert werden soll. +Für den Resource-Typ ``light`` sind die folgenden Funktionen implementiert (einige erlauben nur die Abfrage): + + - ``on`` + - ``bri`` + - ``bri_inc`` + - ``hue`` + - ``sat`` + - ``ct`` + - ``dict`` + - ``name`` + - ``reachable`` + - ``colormode`` + - ``xy`` + - ``type`` + - ``modelid`` + - ``swversion`` + - ``activate_scene`` + - ``modify_scene`` + - ``alert`` + - ``effect`` + +Für den Resource-Typ ``sensor`` sind die folgenden Funktionen implementiert, welche nur die Abfrage erlauben: + + - ``daylight`` + - ``temperature`` + - ``presence`` + - ``lightlevel`` + - ``status`` + + + +Die vollständige Übersicht über die unterstützen Funktionen und die Datentypen dazu kann auf der +Seite :doc:`/plugins_doc/config/hue2` in der Beschreibung des Item Attributes ``hue2_function`` nachgelesen +werden. + +.. note:: + + Pullrequest https://github.com/smarthomeNG/plugins/pull/590 implementierte zusätzliche für hue2_function die + zusätzlichen Optionen ``bri_inc`` und ``dict``, welche noch nicht vollständig dokumentiert sind. + +Um den Namen der Leuchte mit der Id 3 abzufragen, muss ein Item folgendermaßen konfiguriert werden: + +.. code-block:: yaml + + leuchten_name: + type: str + hue2_resource: light + hue2_id: 3 + hue2_function: name + + +| + +Web Interface +============= + +Das hue_apiv2 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 an der Hue Brigde angemeldet sind. + + +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/hue_apiv2`` bzw. +``http://smarthome.local:8383/plugin/hue_apiv2`` 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/hue_apiv2/webif/__init__.py b/hue_apiv2/webif/__init__.py new file mode 100755 index 000000000..4bcdb4738 --- /dev/null +++ b/hue_apiv2/webif/__init__.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2020- 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 +# +# This file implements the web interface for the hue2 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() + + # ---------------------------------------------------------------------------------- + # Methods to handle v2bridge for webinterface + # + + def get_itemsdata(self): + + result = {} + if self.plugin.v2bridge is None: + return 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['resource'] = item_config['resource'] + value_dict['id'] = item_config['id'] + value_dict['function'] = item_config['function'] + + 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_lightsdata(self): + + result = {} + if self.plugin.v2bridge is None: + return result + + devicedata = self.get_devicesdata() + + # self.logger.info(f"get_lightsdata() - Lights:") + first = True + for light in self.plugin.v2bridge.lights: + if first: + first = False + self.logger.info(f"get_lightsdata() - dir(light): {dir(light)}") + self.logger.info(f"- dir(light.mode): {dir(light.mode)}") + self.logger.info(f"- dir(light.color): {dir(light.color)}") + self.logger.info(f"- dir(light.color_temperature): {dir(light.color_temperature)}") + self.logger.info(f"- dir(light.powerup): {dir(light.powerup)}") + value_dict = {} + value_dict['_full'] = light + + device_id = light.owner.rid + zigbee_status = self.plugin.v2bridge.devices.get_zigbee_connectivity(device_id).status.value + + value_dict['id'] = light.id + if light.id_v1 is None: + value_dict['id_v1'] = '' + else: + value_dict['id_v1'] = light.id_v1 + + value_dict['name'] = '' + for d in devicedata: + if value_dict['id_v1'] == devicedata[d]['id_v1']: + value_dict['name'] = devicedata[d]['name'] + + value_dict['on'] = light.on.on + + value_dict['is_on'] = light.is_on + value_dict['brightness'] = light.brightness + # value_dict['color'] = light.color + # value_dict['color_temperature'] = light.color_temperature + value_dict['entertainment_active'] = light.entertainment_active + # value_dict['powerup'] = light.powerup + value_dict['supports_color'] = light.supports_color + value_dict['supports_color_temperature'] = light.supports_color_temperature + value_dict['supports_dimming'] = light.supports_dimming + value_dict['zigbee_status'] = zigbee_status + + value_dict['xy'] = [light.color.xy.x, light.color.xy.y] + try: + value_dict['ct'] = light.color_temperature.mirek + if light.color_temperature.mirek_valid == False: + value_dict['ct'] = '(' + str(light.color_temperature.mirek) + ')' + except: + value_dict['ct'] = '' + value_dict['gamut_type'] = light.color.gamut_type.value + value_dict['type'] = light.type.value + + result[light.id] = value_dict + # self.logger.info(f"- {light.id}: {lightdata['mode']} - {dir(lightdata['mode'])}") + self.logger.info(f"-> {light.id}: {value_dict}") + + return result + + def get_scenesdata(self): + + result = {} + if self.plugin.v2bridge is None: + return result + + for scene in self.plugin.v2bridge.scenes: + value_dict = {} + value_dict['_full'] = scene + value_dict['id'] = scene.id + if scene.id_v1 is None: + value_dict['id_v1'] = '' + else: + value_dict['id_v1'] = scene.id_v1 + value_dict['name'] = scene.metadata.name + value_dict['type'] = scene.type.value + + result[scene.id] = value_dict + + return result + + def get_groupsdata(self): + + result = {} + if self.plugin.v2bridge is None: + return result + + for group in self.plugin.v2bridge.groups: + #self.logger.notice(f"get_groupsdata: {group=}") + if group.type.value == 'grouped_light': + value_dict = {} + value_dict['_full'] = group + value_dict['id'] = group.id + if group.id_v1 is None: + value_dict['id_v1'] = '' + else: + value_dict['id_v1'] = group.id_v1 + try: + room = self.plugin.v2bridge.groups.grouped_light.get_zone(group.id) + value_dict['name'] = room.metadata.name + if value_dict['name'] == '': + value_dict['name'] = '(All lights)' + except: + value_dict['name'] = '' + value_dict['type'] = group.type.value + + result[group.id] = value_dict + + return result + + def get_sensorsdata(self): + + result = {} + if self.plugin.v2bridge is None: + return result + + devicedata = self.get_devicesdata() + + for sensor in self.plugin.v2bridge.sensors: + value_dict = {} + value_dict['_full'] = sensor + value_dict['id'] = sensor.id + if sensor.id_v1 is None: + value_dict['id_v1'] = '' + else: + value_dict['id_v1'] = sensor.id_v1 + try: + value_dict['name'] = sensor.name + except: + value_dict['name'] = '' + self.logger.debug(f"get_sensorsdata: Exception 'no name'") + self.logger.debug(f"- sensor={sensor}") + self.logger.debug(f"- owner={sensor.owner}") + + try: + value_dict['control_id'] = sensor.metadata.control_id + except: + value_dict['control_id'] = '' + try: + value_dict['event'] = sensor.button.last_event.value + except: + value_dict['event'] = '' + try: + value_dict['last_event'] = sensor.button.last_event.value + except: + value_dict['last_event'] = '' + + try: + value_dict['battery_level'] = sensor.power_state.battery_level + value_dict['battery_state'] = sensor.power_state.battery_state.value + except: + value_dict['battery_level'] = '' + value_dict['battery_state'] = '' + + try: + value_dict['device_id'] = sensor.owner.rid + value_dict['device_name'] = '' + for d in devicedata: + if value_dict['device_id'] == devicedata[d]['id']: + value_dict['device_name'] = devicedata[d]['name'] + if value_dict['name'] == '': + value_dict['name'] = devicedata[d]['name'] + + except Exception as ex: + value_dict['device_id'] = '' + try: + value_dict['owner_type'] = sensor.owner.rtype.value + except Exception as ex: + value_dict['owner_type'] = '' + try: + value_dict['status'] = sensor.status.value + except Exception as ex: + value_dict['status'] = '' + + value_dict['type'] = sensor.type.value + + result[sensor.id] = value_dict + + return result + + def get_devicesdata(self): + + result = {} + if self.plugin.v2bridge is None: + return result + + for device in self.plugin.v2bridge.devices: + value_dict = {} + value_dict['_full'] = device + value_dict['id'] = device.id + if device.id_v1 is None: + value_dict['id_v1'] = '' + else: + value_dict['id_v1'] = device.id_v1 + value_dict['name'] = device.metadata.name + value_dict['model_id'] = device.product_data.model_id + value_dict['manufacturer_name'] = device.product_data.manufacturer_name + value_dict['product_name'] = device.product_data.product_name + value_dict['software_version'] = device.product_data.software_version + value_dict['hardware_platform_type'] = device.product_data.hardware_platform_type + if device.lights == set(): + value_dict['lights'] = [] + else: + value_dict['lights'] = list(device.lights) + value_dict['services'] = [] + for s in device.services: + if str(s.rtype.value) != 'unknown': + value_dict['services'].append(s.rtype.value) + if str(s.rtype.value) == 'zigbee_connectivity': + # value_dict['zigbee_connectivity'] = s.rtype.status.value + sensor = self.plugin.v2bridge.devices.get_zigbee_connectivity(device.id) + try: + value_dict['zigbee_connectivity'] = str(sensor.status.value) + except: + value_dict['zigbee_connectivity'] = '' + value_dict['product_archetype'] = device.product_data.product_archetype.value + value_dict['certified'] = device.product_data.certified + value_dict['archetype'] = device.metadata.archetype.value + + result[device.id] = value_dict + return result + + def idv1_to_id(self, id_v1): + + if id_v1.startswith('/lights/'): + return id_v1[8:] + return '' + + # ---------------------------------------------------------------------------------- + + def ja_nein(self, value) -> str: + """ + Bool Wert in Ja/Nein String wandeln + + :param value: + :return: + """ + if isinstance(value, bool): + if value: + return self.translate('Ja') + return self.translate('Nein') + return value + + + @cherrypy.expose + def index(self, scan=None, connect=None, disconnect=None, reload=None): + """ + Build index.html for cherrypy + + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + + if scan == 'on': + self.plugin.discovered_bridges = self.plugin.discover_bridges() + + if connect is not None: + self.logger.info("Connect: connect={}".format(connect)) + for db in self.plugin.discovered_bridges: + if db['serialNumber'] == connect: + user = self.plugin.create_new_username(db['ip'], db['port']) + if user != '': + self.plugin.bridge= db + self.plugin.bridge['username'] = user + self.plugin.bridgeinfo = self.plugin.get_bridgeinfo() + self.plugin.update_plugin_config() + + if disconnect is not None: + self.logger.info("Disconnect: disconnect={}".format(disconnect)) + self.plugin.remove_username(self.plugin.bridge['ip'], self.plugin.bridge['port'], self.plugin.bridge['username']) + self.plugin.bridge = {} + self.plugin.bridgeinfo = {} + self.plugin.update_plugin_config() + + try: + tmpl = self.tplenv.get_template('index.html') + except: + self.logger.error("Template file 'index.html' not found") + else: + # add values to be passed to the Jinja2 template eg: tmpl.render(p=self.plugin, interface=interface, ...) + #self.logger.notice(f"index: {self.plugin.get_parameter_value('webif_pagelength')}") + return tmpl.render(p=self.plugin, + webif_pagelength=self.plugin.get_parameter_value('webif_pagelength'), + #items=sorted(self.items.return_items(), key=lambda k: str.lower(k['_path'])), + items=self.plugin.plugin_items, + item_count=len(self.plugin.plugin_items), + bridge=self.plugin.bridge, + bridge_count=len(self.plugin.bridge), + discovered_bridges=self.plugin.discovered_bridges, + +# bridge_devices=sorted(self.plugin.get_devicesdata().items(), key=lambda k: str.lower(k[1]['id_v1'])), + bridge_devices=self.get_devicesdata(), + bridge_lights=self.get_lightsdata(), + bridge_groups=self.get_groupsdata(), + bridge_scenes=self.get_scenesdata(), + bridge_sensors=self.get_sensorsdata(), + + bridge_config=self.plugin.bridge_config, + br_object=self.plugin.br) + + + @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 = [] + light_list = [] + scene_list = [] + group_list = [] + sensor_list = [] + device_list = [] + if dataSet is None or dataSet == 'bridge_info': + 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) + + + # callect data for lights + lights = self.get_lightsdata() + for light in lights: + value_dict = {} + for key in lights[light]: + if key != '_full': + value_dict[key] = lights[light][key] + if key == 'id_v1' and len(value_dict[key].split('/')) > 2: + value_dict[key] = value_dict[key].split('/')[2] + elif key == 'on': + value_dict[key] = self.ja_nein(value_dict[key]) + + light_list.append(value_dict) + + # callect data for scenes + scenes = self.get_scenesdata() + for scene in scenes: + value_dict = {} + for key in scenes[scene]: + if key != '_full': + value_dict[key] = scenes[scene][key] + if key == 'id_v1' and len(value_dict[key].split('/')) > 2: + value_dict[key] = value_dict[key].split('/')[2] + + scene_list.append(value_dict) + + # callect data for groups + groups = self.get_groupsdata() + for group in groups: + value_dict = {} + for key in groups[group]: + if key != '_full': + value_dict[key] = groups[group][key] + if key == 'id_v1' and len(value_dict[key].split('/')) > 2: + value_dict[key] = value_dict[key].split('/')[2] + + group_list.append(value_dict) + + # callect data for sensors + sensors = self.get_sensorsdata() + for sensor in sensors: + value_dict = {} + for key in sensors[sensor]: + if key != '_full': + value_dict[key] = sensors[sensor][key] + #if key == 'id_v1' and len(value_dict[key].split('/')) > 2: + # value_dict[key] = value_dict[key].split('/')[2] + + sensor_list.append(value_dict) + + # callect data for devices + devices = self.get_devicesdata() + for device in devices: + value_dict = {} + for key in devices[device]: + if key != '_full': + value_dict[key] = devices[device][key] + #if key == 'id_v1' and len(value_dict[key].split('/')) > 2: + # value_dict[key] = value_dict[key].split('/')[2] + + device_list.append(value_dict) + + + if dataSet is None: + # get the new data + data = {} + + # data['item'] = {} + # for i in self.plugin.items: + # data['item'][i]['value'] = self.plugin.getitemvalue(i) + # + # return it as json the the web page + # try: + # return json.dumps(data) + # except Exception as e: + # self.logger.error("get_data_html exception: {}".format(e)) + + #result = {'items': item_list, 'devices': device_list, 'broker': broker_data} + result = {'items': item_list, 'lights': light_list, 'scenes': scene_list, 'groups': group_list, 'sensors': sensor_list, 'devices': device_list} + + # 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/hue_apiv2/webif/static/img/plugin_logo.png b/hue_apiv2/webif/static/img/plugin_logo.png new file mode 100755 index 000000000..2762a7297 Binary files /dev/null and b/hue_apiv2/webif/static/img/plugin_logo.png differ diff --git a/hue_apiv2/webif/static/img/readme.txt b/hue_apiv2/webif/static/img/readme.txt new file mode 100755 index 000000000..1a7c55eef --- /dev/null +++ b/hue_apiv2/webif/static/img/readme.txt @@ -0,0 +1,6 @@ +This directory is for storing images that are used by the web interface. + +If you want to have your own logo on the top of the web interface, store it here and name it plugin_logo.. + +Extension can be png, svg or jpg + diff --git a/hue_apiv2/webif/templates/index.html b/hue_apiv2/webif/templates/index.html new file mode 100755 index 000000000..28166345f --- /dev/null +++ b/hue_apiv2/webif/templates/index.html @@ -0,0 +1,870 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 5000 %} + + +{% set dataSet = 'bridge_info' %} + + +{% set update_params = item_id %} + + +{% set buttons = true %} + + +{% set autorefresh_buttons = true %} + + +{% set reload_button = true %} + + +{% set close_button = false %} + + +{% set row_count = true %} + + +{% set initial_update = true %} + +{% set bordered_tab = true %} + + +{% block pluginstyles %} + +{% endblock pluginstyles %} + + +{% block pluginscripts %} + + + +{% endblock pluginscripts %} + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bridge Serial{{ p.v2bridge.bridge_id }}Leuchten Zyklus{% if p.light_items_configured == true %}{{ p._cycle_lights }} {{ _('Sek.') }}{% else %}-{% endif %}
Bridge IP{% if bridge.username == undeffined %}-{% else %}{{ p.bridge.ip }}{% endif %}Sensor Zyklus{% if p.sensor_items_configured == true %}{{ p._cycle_sensors }} {{ _('Sek.') }}{% else %}-{% endif %}
Anwendungsschlüssel{% if bridge.username == undeffined %}-{% else %}{{ _('konfiguriert') }}{% endif %}Bridge Zyklus{{ p._cycle_bridge }} {{ _('Sek.') }}
+{% endblock headtable %} + + + +{% block buttons %} +
+ +
+{% endblock %} + + +{% set tabcount = 7 %} + + + +{% if item_count == 0 %} + {% set start_tab = 2 %} +{% endif %} +{% if (item_count == 0) and (bridge_lights|length == 0) %} + { % set start_tab = 6 % } +{% endif %} +{% if bridge_count == 0 %} + { % set start_tab = 7 % } +{% endif %} + + +{% set tab1title = "" ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} +
+
+
+{% endblock bodytab1 %} + + + +{% set tab2title = "" ~ _('Leuchten') ~ " (" ~ bridge_lights|length ~ ")" %} +{% block bodytab2 %} +
+
+
+{% endblock bodytab2 %} + + + +{% set tab3title = "" ~ _('Szenen') ~ " (" ~ bridge_scenes|length ~ ")" %} +{% block bodytab3 %} +
+
+
+{% endblock bodytab3 %} + + + +{% set tab4title = "" ~ _('Gruppen') ~ " (" ~ bridge_groups|length ~ ")" %} +{% block bodytab4 %} +
+
+
+{% endblock bodytab4 %} + + + +{% set tab5title = "" ~ _('Sensoren') ~ " (" ~ bridge_sensors|length ~ ")" %} +{% block bodytab5 %} +
+
+
+ + {% if true %} +
+ {% for sensor in bridge_sensors %} + sensor: {{ bridge_sensors[sensor] }}
+ type.value: {{ bridge_sensors[sensor]._full.type.value }}
+ {% if bridge_sensors[sensor]._full.type.value == 'button' %} + {% if bridge_sensors[sensor]._full.button.metadata is not undefined %} + Metadata: {{ bridge_sensors[sensor]._full.button.metadata.control_id }}
+ {% endif %} + {% endif %} +
+ {% endfor %} + {% endif %} +{% endblock bodytab5 %} + + + +{% set tab6title = "" ~ _('Devices') ~ " (" ~ bridge_devices|length ~ ")" %} +{% block bodytab6 %} +
+
+
+ {% if true %} +
+ {% for device in bridge_devices %} + sensor: {{ bridge_devices[device] }}
+ type.value: {{ bridge_devices[device]._full.type.value }}
+ {% if bridge_devices[device]._full.type.value == 'button' %} + {% endif %} +
+ {% endfor %} + {% endif %} +{% endblock bodytab6 %} + + + +{% set tab7title = "Hue Bridge" %} +{% block bodytab7 %} + {% if discovered_bridges != [] %} +
+
+ {{ _('Gefundene Bridges') }}:
+
+ {% endif %} +
+
+ + {% if discovered_bridges != [] %} +
+ + + + + + + + + + + + + + + + + + + + {% for db in discovered_bridges %} + + + + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Serien-Nr.') }}{{ _('Hersteller') }}{{ _('Modell Name') }}{{ _('Modell Nr.') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('swversion') }}{{ _('IP') }}{{ _('Port') }}
{{ db.serialNumber }}{{ db.manufacturer }}{{ db.modelName }}{{ db.modelNumber }}{{ db.version }}{{ db.gatewayName }}{{ db.apiversion }}{{ db.modelid }}{{ db.swversion }}{{ db.ip }}{{ db.port }} + {% if bridge_count == 0 %} + + {% endif %} +
+
+ {% endif %} +
+
+ +
+ {% if bridge_count == 0 %} + {{ _('Es ist keine Bridge mit dieser Plugin Instanz verbunden.') }}
+
+ {{ _('Zum Verbinden den Knopf an der Hue Bridge drücken und anschließend in der Liste der gefundenen Bridges den Button "Verbinden" drücken.') }}
+ {% endif %} +
+
+ + {% if bridge_count > 0 %} +
+ {{ _('Konfigurierte Bridge') }}:
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + {% set cb = bridge %} + + {% if bridge_count > 0 %} + + + + + + + + + + + + + + + {% endif %} + +
{{ _('Serien-Nr.') }}{{ _('Modell Name') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('IP') }}{{ _('Port') }}{{ _('Anwendungsschlüssel') }}
{{ cb.serialNumber }}{{ cb.modelName }}{{ cb.version }}{{ cb.gatewayName }}{{ cb.apiversion }}{{ cb.modelid }}{{ cb.ip }}{{ cb.port }}{{ cb.username }} + +
+
+
+
+ {% endif %} + + + {% if (bridge_count > 0) and False %} +
+
+ {{ _('Konfigurierte Benutzer') }}:
+
+ +
+
+ +
+ + + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + + + {% for u in bridge_config.whitelist %} + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + {% endfor %} + +
{{ _('Anwendungsschlüssel') }}{{ _('Anwendung') }}{{ _('Letze Nutzung') }}{{ _('Erstellt') }}
{{ u }}{{ bridge_config.whitelist[u].name }}{{ bridge_config.whitelist[u]['last use date'] }}{{ bridge_config.whitelist[u]['create date'] }}
+
+
+
+ {% endif %} +{% endblock bodytab7 %} + + +{% set tab8title = "" ~ "Hue " ~ _('Leuchten') ~ " X (" ~ bridge_lights|length ~ ")" %} +{% block bodytab8 %} +
+
+
+ + {% if false %} +----- + {% for d in bridge_devices %} +
+ {{ d }}: {{ bridge_devices[d]._full }}
+ + {% for s in bridge_devices[d]._full.services %} + - services[...]: {{ s.rtype.value }}
+ {% endfor %} + - product_data.product_archetype: {{ bridge_devices[d]._full.product_data.product_archetype.value }}
+ - product_data.certified: {{ bridge_devices[d]._full.product_data.certified }}
+ - metadata.archetype: {{ bridge_devices[d]._full.metadata.archetype.value }}
+
+
+ {% endfor %} + {% endif %} +{% endblock bodytab8 %} diff --git a/hue_apiv2/webif/templates/index2.html b/hue_apiv2/webif/templates/index2.html new file mode 100755 index 000000000..74454cf99 --- /dev/null +++ b/hue_apiv2/webif/templates/index2.html @@ -0,0 +1,772 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 0 %} + +{% set update_interval = (200 * (log_array | length)) %} + + +{% set dataSet = 'devices_info' %} + + +{% set update_params = item_id %} + + +{% set buttons = false %} + + +{% set autorefresh_buttons = false %} + + +{% set reload_button = false %} + + +{% set close_button = false %} + + +{% set row_count = true %} + + +{% set initial_update = true %} + + +{% block pluginstyles %} + +{% endblock pluginstyles %} + + +{% block pluginscripts %} + + + +{% block pluginscripts %} + + + +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bridge Serial{% if bridge.username == undeffined %}-{% else %}{{ p.bridge.serialNumber }}{% endif %}Leuchten Zyklus{% if p.light_items_configured == true %}{{ p._cycle_lights }} {{ _('Sek.') }}{% else %}-{% endif %}
Bridge IP{% if bridge.username == undeffined %}-{% else %}{{ p.bridge.ip }}{% endif %}Sensor Zyklus{% if p.sensor_items_configured == true %}{{ p._cycle_sensors }} {{ _('Sek.') }}{% else %}-{% endif %}
Anwendungsschlüssel{% if bridge.username == undeffined %}-{% else %}{{ _('konfiguriert') }}{% endif %}Bridge Zyklus{{ p._cycle_bridge }} {{ _('Sek.') }}
+{% endblock headtable %} + + + +{% block buttons %} +{% if 1==2 %} +
+ + +
+{% endif %} +{% endblock %} + + +{% set tabcount = 8 %} + + +{% if item_count == 0 %} + {% set start_tab = 2 %} +{% endif %} +{% if (item_count == 0) and (bridge_lights|length == 0) %} + {% set start_tab = 6 %} +{% endif %} +{% if bridge_count == 0 %} + {% set start_tab = 7 %} +{% endif %} + + + +{% set tab1title = "" ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + {% for i in items %} + + + + + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('hue resource') }}{{ _('hue id') }}{{ _('hue function') }}
{{ i }}{{ items[i].item.property.type }}{{ items[i].item() }}{{ items[i].resource }}{{ items[i].id }}{{ items[i].function }}
+
+
+
+ {% endif %} +{% endblock bodytab1 %} + + + +{% if bridge_count > 0 %} + {% set tab2title = "" ~ "Hue " ~ _('Leuchten') ~ " (" ~ bridge_lights|length ~ ")" %} +{% else %} + {% set tab2title = "" ~ "Hue " ~ _('Leuchten') ~ "" %} +{% endif %} +{% block bodytab2 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + + + {% for l in bridge_lights %} + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Eingeschaltet') }}{{ _('Helligkeit') }}{{ _('Hue') }}{{ _('Sättigung') }}{{ _('xy') }}{{ _('ct') }}{{ _('Mode') }}{{ _('Erreichbar') }}
{{ l }}{{ bridge_lights[l].name }}{{ _(bridge_lights[l].state.on) }}{{ bridge_lights[l].state.bri }}{{ bridge_lights[l].state.hue }}{{ bridge_lights[l].state.sat }}{{ bridge_lights[l].state.xy }}{{ bridge_lights[l].state.ct }}{{ bridge_lights[l].state.colormode }}{{ _(bridge_lights[l].state.reachable) }}
+
+
+
+ + +
+
+ {{ _('weitere Informationen') }}:
+
+
+
+ +
+ + + + + + + + + + + + + + + {% for l in bridge_lights %} + + + + + + + {% if (bridge_config.apiversion >= '1.24.0') and (bridge_lights[l].config.startup) %} + + + {% else %} + + + {% endif %} + {% if bridge_config.apiversion >= '1.22.0' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Produktname') }}{{ _('SW Version') }}{{ _('Startup') }}{{ _('Startup Settings') }}{{ _('max. Lumen') }}
{{ l }}{{ bridge_lights[l].name }}{{ bridge_lights[l].type }}{{ bridge_lights[l].productname }}{{ bridge_lights[l].swversion }}{{ bridge_lights[l].config.startup.mode }}{{ bridge_lights[l].config.startup.customsettings }}--{{ bridge_lights[l].capabilities.control.maxlumen }}-
+
+
+
+ + {% endif %} +{% endblock bodytab2 %} + + + +{% if bridge_count > 0 %} + {% set tab3title = "" ~ "Hue " ~ _('Szenen') ~ " (" ~ bridge_scenes|length ~ ")" %} +{% else %} + {% set tab3title = "" ~ "Hue " ~ _('Szenen') ~ "" %} +{% endif %} +{% block bodytab3 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + {% for s in bridge_scenes %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Lights') }}{{ _('Owner') }}{{ _('Recycle') }}{{ _('Locked') }}{{ _('last updated') }}{{ _('Version') }}
{{ s }}{{ bridge_scenes[s].name }}{{ bridge_scenes[s].type }}{{ bridge_scenes[s].lights }}{{ bridge_scenes[s].owner }}{{ _(bridge_scenes[s].recycle) }}{{ _(bridge_scenes[s].locked) }}{{ bridge_scenes[s].lastupdated }}{{ bridge_scenes[s].version }}
+
+
+
+ + {% endif %} +{% endblock bodytab3 %} + + + +{% if bridge_count > 0 %} + {% set tab4title = "" ~ "Hue " ~ _('Gruppen') ~ " (" ~ bridge_groups|length ~ ")" %} +{% else %} + {% set tab4title = "" ~ "Hue " ~ _('Gruppen') ~ "" %} +{% endif %} +{% block bodytab4 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + {% for g in bridge_groups %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Lights') }}{{ _('Sensors') }}{{ _('Status') }}{{ _('Recycle') }}{{ _('Class') }}{{ _('Action') }}
{{ g }}{{ bridge_groups[g].name }}{{ bridge_groups[g].type }}{{ bridge_groups[g].lights }}{{ bridge_groups[g].sensors }}{{ bridge_groups[g].state }}{{ _(bridge_groups[g].recycle) }}{{ bridge_groups[g].class }}{{ bridge_groups[g].action }}
+
+
+
+ + {% endif %} +{% endblock bodytab4 %} + + + +{% if bridge_count > 0 %} + {% set tab5title = "" ~ "Hue " ~ _('Sensoren') ~ " (" ~ bridge_sensors|length ~ ")" %} +{% else %} + {% set tab5title = "" ~ "Hue " ~ _('Sensoren') ~ "" %} +{% endif %} +{% block bodytab5 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + {% for s in bridge_sensors %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Status') }}{{ _('Konfiguration') }}{{ _('Model Id') }}{{ _('Hersteller') }}{{ _('SW-Version') }}
{{ s }}{{ bridge_sensors[s].name }}{{ bridge_sensors[s].type }}{{ bridge_sensors[s].state }}{{ bridge_sensors[s].config }}{{ bridge_sensors[s].modelid }}{{ bridge_sensors[s].manufacturername }}{{ bridge_sensors[s].swversion }}
+
+
+
+ + {% endif %} +{% endblock bodytab5 %} + + + +{% if bridge_count >= 0 %} + {% set tab6title = "" ~ "Hue " ~ _('Devices') ~ " (" ~ bridge_devices|length ~ ")" %} +{% else %} + {% set tab6title = "" ~ "Hue " ~ _('Devices') ~ "" %} +{% endif %} +{% block bodytab6 %} + {% if bridge_count >= 0 %} + +
+
+ +
+ + + + + + + + + + + + + + {% for d in bridge_devices %} + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('v1 Id') }}{{ _('Name') }}{{ _('Modell Id') }}{{ _('Produktname') }}{{ _('SW-Version') }}{{ _('Lights') }}
{{ d }}{{ bridge_devices[d].id_v1 }}{{ bridge_devices[d].model_id }}{{ bridge_devices[d].product_name }}{{ bridge_devices[d].software_version }}{{ bridge_devices[d].name }}{{ bridge_devices[d].lights }}
+
+
+
+ + {% endif %} +{% endblock bodytab6 %} + + + +{% set tab7title = "Hue Bridge" %} +{% block bodytab7 %} + {% if discovered_bridges != [] %} +
+
+ {{ _('Gefundene Bridges') }}:
+
+ {% endif %} +
+
+ + {% if discovered_bridges != [] %} +
+ + + + + + + + + + + + + + + + + + + + {% for db in discovered_bridges %} + + + + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Serien-Nr.') }}{{ _('Hersteller') }}{{ _('Modell Name') }}{{ _('Modell Nr.') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('swversion') }}{{ _('IP') }}{{ _('Port') }}
{{ db.serialNumber }}{{ db.manufacturer }}{{ db.modelName }}{{ db.modelNumber }}{{ db.version }}{{ db.gatewayName }}{{ db.apiversion }}{{ db.modelid }}{{ db.swversion }}{{ db.ip }}{{ db.port }} + {% if bridge_count == 0 %} + + {% endif %} +
+
+ {% endif %} +
+
+ +
+ {% if bridge_count == 0 %} + {{ _('Es ist keine Bridge mit dieser Plugin Instanz verbunden.') }}
+
+ {{ _('Zum Verbinden den Knopf an der Hue Bridge drücken und anschließend in der Liste der gefundenen Bridges den Button "Verbinden" drücken.') }}
+ {% endif %} +
+
+ + {% if bridge_count > 0 %} +
+ {{ _('Konfigurierte Bridge') }}:
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + {% set cb = bridge %} + + {% if bridge_count > 0 %} + + + + + + + + + + + + + + + {% endif %} + +
{{ _('Serien-Nr.') }}{{ _('Modell Name') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('IP') }}{{ _('Port') }}{{ _('Anwendungsschlüssel') }}
{{ cb.serialNumber }}{{ cb.modelName }}{{ cb.version }}{{ cb.gatewayName }}{{ cb.apiversion }}{{ cb.modelid }}{{ cb.ip }}{{ cb.port }}{{ cb.username }} + +
+
+
+
+ {% endif %} + + + {% if (bridge_count > 0) and (br_object.config().whitelist|length > 1) %} +
+
+ {{ _('Konfigurierte Benutzer') }}:
+
+ +
+
+ +
+ + + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + + + {% for u in bridge_config.whitelist %} + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + {% endfor %} + +
{{ _('Anwendungsschlüssel') }}{{ _('Anwendung') }}{{ _('Letze Nutzung') }}{{ _('Erstellt') }}
{{ u }}{{ bridge_config.whitelist[u].name }}{{ bridge_config.whitelist[u]['last use date'] }}{{ bridge_config.whitelist[u]['create date'] }}
+
+
+
+ {% endif %} +{% endblock bodytab7 %} + + diff --git a/hue_apiv2/webif/templates/index3.html b/hue_apiv2/webif/templates/index3.html new file mode 100755 index 000000000..531816e1c --- /dev/null +++ b/hue_apiv2/webif/templates/index3.html @@ -0,0 +1,836 @@ +{% extends "base_plugin.html" %} + +{% set logo_frame = false %} + + +{% set update_interval = 0 %} + +{% set update_interval = (200 * (log_array | length)) %} + + +{% set dataSet = 'devices_info' %} + + +{% set update_params = item_id %} + + +{% set buttons = false %} + + +{% set autorefresh_buttons = false %} + + +{% 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 %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bridge Serial{% if bridge.username == undeffined %}-{% else %}{{ p.bridge.serialNumber }}{% endif %}Leuchten Zyklus{% if p.light_items_configured == true %}{{ p._cycle_lights }} {{ _('Sek.') }}{% else %}-{% endif %}
Bridge IP{% if bridge.username == undeffined %}-{% else %}{{ p.bridge.ip }}{% endif %}Sensor Zyklus{% if p.sensor_items_configured == true %}{{ p._cycle_sensors }} {{ _('Sek.') }}{% else %}-{% endif %}
Anwendungsschlüssel{% if bridge.username == undeffined %}-{% else %}{{ _('konfiguriert') }}{% endif %}Bridge Zyklus{{ p._cycle_bridge }} {{ _('Sek.') }}
+{% endblock headtable %} + + + +{% block buttons %} +
+ +
+{% endblock %} + + +{% set tabcount = 8 %} + + + +{% if item_count == 0 %} + {% set start_tab = 2 %} +{% endif %} +{% if (item_count == 0) and (bridge_lights|length == 0) %} + {% set start_tab = 6 %} +{% endif %} +{% if bridge_count == 0 %} + {% set start_tab = 7 %} +{% endif %} + + + +{% set tab1title = "" ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + {% for i in items %} + + + + + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('hue resource') }}{{ _('hue id') }}{{ _('hue function') }}
{{ i }}{{ items[i].item.property.type }}{{ items[i].item() }}{{ items[i].resource }}{{ items[i].id }}{{ items[i].function }}
+
+
+
+ {% endif %} +{% endblock bodytab1 %} + + + +{% if bridge_count > 0 %} + {% set tab2title = "" ~ "Hue " ~ _('Leuchten') ~ " (" ~ bridge_lights|length ~ ")" %} +{% else %} + {% set tab2title = "" ~ "Hue " ~ _('Leuchten') ~ "" %} +{% endif %} +{% block bodytab2 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + + + {% for l in bridge_lights %} + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Eingeschaltet') }}{{ _('Helligkeit') }}{{ _('Hue') }}{{ _('Sättigung') }}{{ _('xy') }}{{ _('ct') }}{{ _('Mode') }}{{ _('Erreichbar') }}
{{ l }}{{ bridge_lights[l].name }}{{ _(bridge_lights[l].state.on) }}{{ bridge_lights[l].state.bri }}{{ bridge_lights[l].state.hue }}{{ bridge_lights[l].state.sat }}{{ bridge_lights[l].state.xy }}{{ bridge_lights[l].state.ct }}{{ bridge_lights[l].state.colormode }}{{ _(bridge_lights[l].state.reachable) }}
+
+
+
+ + +
+
+ {{ _('weitere Informationen') }}:
+
+
+
+ +
+ + + + + + + + + + + + + + + {% for l in bridge_lights %} + + + + + + + {% if (bridge_config.apiversion >= '1.24.0') and (bridge_lights[l].config.startup) %} + + + {% else %} + + + {% endif %} + {% if bridge_config.apiversion >= '1.22.0' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Produktname') }}{{ _('SW Version') }}{{ _('Startup') }}{{ _('Startup Settings') }}{{ _('max. Lumen') }}
{{ l }}{{ bridge_lights[l].name }}{{ bridge_lights[l].type }}{{ bridge_lights[l].productname }}{{ bridge_lights[l].swversion }}{{ bridge_lights[l].config.startup.mode }}{{ bridge_lights[l].config.startup.customsettings }}--{{ bridge_lights[l].capabilities.control.maxlumen }}-
+
+
+
+ + {% endif %} +{% endblock bodytab2 %} + + + +{% if bridge_count > 0 %} + {% set tab3title = "" ~ "Hue " ~ _('Szenen') ~ " (" ~ bridge_scenes|length ~ ")" %} +{% else %} + {% set tab3title = "" ~ "Hue " ~ _('Szenen') ~ "" %} +{% endif %} +{% block bodytab3 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + {% for s in bridge_scenes %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Lights') }}{{ _('Owner') }}{{ _('Recycle') }}{{ _('Locked') }}{{ _('last updated') }}{{ _('Version') }}
{{ s }}{{ bridge_scenes[s].name }}{{ bridge_scenes[s].type }}{{ bridge_scenes[s].lights }}{{ bridge_scenes[s].owner }}{{ _(bridge_scenes[s].recycle) }}{{ _(bridge_scenes[s].locked) }}{{ bridge_scenes[s].lastupdated }}{{ bridge_scenes[s].version }}
+
+
+
+ + {% endif %} +{% endblock bodytab3 %} + + + +{% if bridge_count > 0 %} + {% set tab4title = "" ~ "Hue " ~ _('Gruppen') ~ " (" ~ bridge_groups|length ~ ")" %} +{% else %} + {% set tab4title = "" ~ "Hue " ~ _('Gruppen') ~ "" %} +{% endif %} +{% block bodytab4 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + + {% for g in bridge_groups %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Lights') }}{{ _('Sensors') }}{{ _('Status') }}{{ _('Recycle') }}{{ _('Class') }}{{ _('Action') }}
{{ g }}{{ bridge_groups[g].name }}{{ bridge_groups[g].type }}{{ bridge_groups[g].lights }}{{ bridge_groups[g].sensors }}{{ bridge_groups[g].state }}{{ _(bridge_groups[g].recycle) }}{{ bridge_groups[g].class }}{{ bridge_groups[g].action }}
+
+
+
+ + {% endif %} +{% endblock bodytab4 %} + + + +{% if bridge_count > 0 %} + {% set tab5title = "" ~ "Hue " ~ _('Sensoren') ~ " (" ~ bridge_sensors|length ~ ")" %} +{% else %} + {% set tab5title = "" ~ "Hue " ~ _('Sensoren') ~ "" %} +{% endif %} +{% block bodytab5 %} + {% if bridge_count > 0 %} + +
+
+ +
+ + + + + + + + + + + + + + + {% for s in bridge_sensors %} + + + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('Name') }}{{ _('Typ') }}{{ _('Status') }}{{ _('Konfiguration') }}{{ _('Model Id') }}{{ _('Hersteller') }}{{ _('SW-Version') }}
{{ s }}{{ bridge_sensors[s].name }}{{ bridge_sensors[s].type }}{{ bridge_sensors[s].state }}{{ bridge_sensors[s].config }}{{ bridge_sensors[s].modelid }}{{ bridge_sensors[s].manufacturername }}{{ bridge_sensors[s].swversion }}
+
+
+
+ + {% endif %} +{% endblock bodytab5 %} + + + +{% if bridge_count >= 0 %} + {% set tab6title = "" ~ "Hue " ~ _('Devices') ~ " (" ~ bridge_devices|length ~ ")" %} +{% else %} + {% set tab6title = "" ~ "Hue " ~ _('Devices') ~ "" %} +{% endif %} +{% block bodytab6 %} + {% if bridge_count >= 0 %} + +
+
+ +
+ + + + + + + + + + + + + + {% for d in bridge_devices %} + + + + + + + + + + {% endfor %} + +
{{ _('Id') }}{{ _('v1 Id') }}{{ _('Name') }}{{ _('Modell Id') }}{{ _('Produktname') }}{{ _('SW-Version') }}{{ _('Lights') }}
{{ d }}{{ bridge_devices[d].id_v1 }}{{ bridge_devices[d].model_id }}{{ bridge_devices[d].product_name }}{{ bridge_devices[d].software_version }}{{ bridge_devices[d].name }}{{ bridge_devices[d].lights }}
+
+
+
+ + {% endif %} +{% endblock bodytab6 %} + + + +{% set tab7title = "Hue Bridge" %} +{% block bodytab7 %} + {% if discovered_bridges != [] %} +
+
+ {{ _('Gefundene Bridges') }}:
+
+ {% endif %} +
+
+ + {% if discovered_bridges != [] %} +
+ + + + + + + + + + + + + + + + + + + + {% for db in discovered_bridges %} + + + + + + + + + + + + + + + + + {% endfor %} + +
{{ _('Serien-Nr.') }}{{ _('Hersteller') }}{{ _('Modell Name') }}{{ _('Modell Nr.') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('swversion') }}{{ _('IP') }}{{ _('Port') }}
{{ db.serialNumber }}{{ db.manufacturer }}{{ db.modelName }}{{ db.modelNumber }}{{ db.version }}{{ db.gatewayName }}{{ db.apiversion }}{{ db.modelid }}{{ db.swversion }}{{ db.ip }}{{ db.port }} + {% if bridge_count == 0 %} + + {% endif %} +
+
+ {% endif %} +
+
+ +
+ {% if bridge_count == 0 %} + {{ _('Es ist keine Bridge mit dieser Plugin Instanz verbunden.') }}
+
+ {{ _('Zum Verbinden den Knopf an der Hue Bridge drücken und anschließend in der Liste der gefundenen Bridges den Button "Verbinden" drücken.') }}
+ {% endif %} +
+
+ + {% if bridge_count > 0 %} +
+ {{ _('Konfigurierte Bridge') }}:
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + {% set cb = bridge %} + + {% if bridge_count > 0 %} + + + + + + + + + + + + + + + {% endif %} + +
{{ _('Serien-Nr.') }}{{ _('Modell Name') }}{{ _('Version') }}{{ _('') }}{{ _('API-Version') }}{{ _('Modell ID') }}{{ _('IP') }}{{ _('Port') }}{{ _('Anwendungsschlüssel') }}
{{ cb.serialNumber }}{{ cb.modelName }}{{ cb.version }}{{ cb.gatewayName }}{{ cb.apiversion }}{{ cb.modelid }}{{ cb.ip }}{{ cb.port }}{{ cb.username }} + +
+
+
+
+ {% endif %} + + + {% if (bridge_count > 0) and (br_object.config().whitelist|length > 1) %} +
+
+ {{ _('Konfigurierte Benutzer') }}:
+
+ +
+
+ +
+ + + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + + + {% for u in bridge_config.whitelist %} + + {% if bridge_config.apiversion < '1.31.0' %} + + {% endif %} + + + + + + {% endfor %} + +
{{ _('Anwendungsschlüssel') }}{{ _('Anwendung') }}{{ _('Letze Nutzung') }}{{ _('Erstellt') }}
{{ u }}{{ bridge_config.whitelist[u].name }}{{ bridge_config.whitelist[u]['last use date'] }}{{ bridge_config.whitelist[u]['create date'] }}
+
+
+
+ {% endif %} +{% endblock bodytab7 %} + + +{% set tab8title = "" ~ "Hue " ~ _('Devices') ~ " (" ~ bridge_devices|length ~ ")" %} +{% block bodytab8 %} +---- +
+
+
+---- +
+
+
+---- +{% endblock bodytab8 %}