def test_check_config_section(self): """ Test check_config for section. """ checker = PluginConfigChecker([{ 'name': 'outputs', 'type': 'section', 'repeat': True, 'min': 1, 'content': [{ 'name': 'output', 'type': 'int' }] }]) checker.check_config({'outputs': []}) checker.check_config({'outputs': [{'output': 2}]}) checker.check_config({'outputs': [{'output': 2}, {'output': 4}]}) try: checker.check_config({'outputs': 'test'}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('list' in str(exception)) try: checker.check_config({'outputs': [{'test': 123}]}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('section' in str(exception) and 'output' in str(exception))
def test_check_config_int(self): """ Test check_config for int. """ checker = PluginConfigChecker([{'name': 'port', 'type': 'int'}]) checker.check_config({'port': 123}) try: checker.check_config({'port': "123"}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('int' in str(exception))
def test_check_config_bool(self): """ Test check_config for bool. """ checker = PluginConfigChecker([{'name': 'use_auth', 'type': 'bool'}]) checker.check_config({'use_auth': True}) try: checker.check_config({'use_auth': 234543}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('bool' in str(exception))
def test_check_config_str(self): """ Test check_config for str. """ checker = PluginConfigChecker([{'name': 'hostname', 'type': 'str'}]) checker.check_config({'hostname': 'cloud.openmotics.com'}) try: checker.check_config({'hostname': 123}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('str' in str(exception))
def test_simple(self): """ Test a simple valid configuration. """ checker = PluginConfigChecker([{ 'name': 'log_inputs', 'type': 'bool', 'description': 'Log the input data.' }, { 'name': 'log_outputs', 'type': 'bool', 'description': 'Log the output data.' }]) checker.check_config({'log_inputs': True, 'log_outputs': False})
def test_check_config_password(self): """ Test check_config for bool. """ checker = PluginConfigChecker([{ 'name': 'password', 'type': 'password' }]) checker.check_config({'password': '******'}) try: checker.check_config({'password': 123}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('str' in str(exception))
def test_check_config_error(self): """ Test check_config with an invalid data type """ checker = PluginConfigChecker([{'name': 'hostname', 'type': 'str'}]) try: checker.check_config('string') self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('dict' in str(exception)) try: checker.check_config({}) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('hostname' in str(exception))
class Healthbox(OMPluginBase): """ A Healthbox 3 plugin, for reading and controlling your Renson Healthbox 3 """ name = 'Healthbox' version = '1.0.0' interfaces = [('config', '1.0'), ('metrics', '1.0')] config_description = [{'name': 'serial', 'type': 'str', 'description': 'The serial of the Healthbox 3. E.g. 250424P0031'}] metric_definitions = [{'type': 'aqi', 'tags': ['type', 'description', 'serial'], 'metrics': [{'name': 'aqi', 'description': 'Global air quality index', 'type': 'gauge', 'unit': 'aqi'}]}] default_config = {'serial': ''} def __init__(self, webinterface, logger): super(Healthbox, self).__init__(webinterface, logger) self.logger('Starting Healthbox plugin...') self._config = self.read_config(Healthbox.default_config) self._config_checker = PluginConfigChecker(Healthbox.config_description) self._read_config() self._previous_output_state = {} self.logger("Started Healthbox plugin") def _read_config(self): self._serial = self._config['serial'] self._sensor_mapping = self._config.get('sensor_mapping', []) self._endpoint = 'http://{0}/v1/api/data/current' self._headers = {'X-Requested-With': 'OpenMotics plugin: Healthbox', 'X-Healthbox-Version': '2'} self._ip = self._discover_ip_for_serial(self._serial) if self._ip: self.logger("Healthbox found with serial {0}and ip address {1}".format(self._serial, self._ip)) else: self.logger("Healthbox with serial {0} not found!".format(self._serial)) self._enabled = (self._ip != '' and self._serial != '') self.logger('Healthbox is {0}'.format('enabled' if self._enabled else 'disabled')) def _byteify(self, input): if isinstance(input, dict): return {self._byteify(key): self._byteify(value) for key, value in input.items()} elif isinstance(input, list): return [self._byteify(element) for element in input] elif isinstance(input, unicode): return input.encode('utf-8') else: return input def _discover_ip_for_serial(self, serial): hb3Ip = '' # Create a UDP socket for devices discovery sock = socket(AF_INET, SOCK_DGRAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) sock.settimeout(5) server_address = ('255.255.255.255', 49152) message = 'RENSON_DEVICE/JSON?' discovered_devices = [] try: sent = sock.sendto(message.encode(), server_address) while True: data, server = sock.recvfrom(4096) if data.decode('UTF-8'): discovered_devices.append(json.loads(data)) else: print('Verification failed') print('Trying again...') except Exception as ex: if len(discovered_devices) == 0: self.logger('Error during discovery for serial: {0}'.format(ex)) finally: sock.close() for device in discovered_devices: if device.get('serial') == serial: hb3Ip = device.get('IP') if hb3Ip == '': self.logger('Error during discovery for serial: {0}'.format(serial)) return hb3Ip @background_task def run(self): while True: if not self._enabled: start = time.time() try: self._ip = self._discover_ip_for_serial(self._serial) if self._ip: self._enabled = True self.logger('Healthbox is {0}'.format('enabled' if self._enabled else 'disabled')) except Exception as ex: self.logger('Error while fetching ip address: {0}'.format(ex)) # This loop should run approx. every 60 seconds sleep = 60 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(60) @om_metric_data(interval=15) def get_metric_data(self): if self._enabled: now = time.time() try: response = requests.get(url=self._endpoint.format(self._ip)) if response.status_code != 200: self.logger('Failed to load healthbox data') return result = response.json() serial = result.get('serial') sensors = result.get('sensor') description = result.get('description') if serial and sensors and description: for sensor in result['sensor']: if sensor['type'] == 'global air quality index': yield {'type': 'aqi', 'timestamp': now, 'tags': {'type': 'Healthbox', 'description':description, 'serial': serial}, 'values': {'aqi': float(sensor['parameter']['index']['value'])} } except Exception as ex: self.logger("Error while fetching metric date from healthbox: {0}".format(ex)) self._enabled = False self.logger('Healthbox is {0}'.format('enabled' if self._enabled else 'disabled')) return @om_expose def get_config_description(self): return json.dumps(Healthbox.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class OpenWeatherMap(OMPluginBase): """ An OpenWeatherMap plugin """ name = 'OpenWeatherMap' version = '1.0.1' interfaces = [('config', '1.0')] config_description = [{'name': 'api_key', 'type': 'str', 'description': 'The API key from OpenWeatherMap.'}, {'name': 'lat', 'type': 'str', 'description': 'A location latitude which will be passed to OpenWeatherMap.'}, {'name': 'lng', 'type': 'str', 'description': 'A location longitude which will be passed to OpenWeatherMap.'}, {'name': 'main_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics Virtual Sensors and OpenWeatherMap forecasts. See README.', 'repeat': True, 'min': 0, 'content': [{'name': 'sensor_id', 'type': 'int'}, {'name': 'time_offset', 'type': 'int'}]}, {'name': 'uv_sensor_id', 'type': 'int', 'description': 'Sensor ID for storing the UV index (the UV index will be set as temperature). -1 if not needed.'}] default_config = {'api_key': ''} def __init__(self, webinterface, logger): super(OpenWeatherMap, self).__init__(webinterface, logger) self.logger('Starting OpenWeatherMap plugin...') self._config = self.read_config(OpenWeatherMap.default_config) self._config_checker = PluginConfigChecker(OpenWeatherMap.config_description) self._read_config() self._previous_output_state = {} self.logger("Started OpenWeatherMap plugin") def _read_config(self): self._api_key = self._config.get('api_key', '') main_mapping = self._config.get('main_mapping', []) self._current_mapping = [entry for entry in main_mapping if entry['time_offset'] == 0] self._forecast_mapping = [entry for entry in main_mapping if entry['time_offset'] > 0] self._uv_sensor_id = int(self._config.get('uv_sensor_id', -1)) self._uv_endpoint = 'http://api.openweathermap.org/v3/uvi/{lat},{lon}/{date}Z.json?appid={api_key}' self._forecast_endpoint = 'http://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&units=metric&appid={api_key}' self._current_endpoint = 'http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&units=metric&appid={api_key}' self._headers = {'X-Requested-With': 'OpenMotics plugin: OpenWeatherMap'} self._enabled = False if (self._config.get('lat', '') != '') and (self._config.get('lng', '') != ''): self._latitude = self._config.get('lat') self._longitude = self._config.get('lng') self.logger('Latitude: {0} - Longitude: {1}'.format(self._latitude, self._longitude)) self._enabled = True self._enabled = self._enabled and self._api_key != '' self.logger('OpenWeatherMap is {0}'.format('enabled' if self._enabled else 'disabled')) @background_task def run(self): previous_values = {} accuracy = 5 while True: if self._enabled: start = time.time() sensor_values = {} calls = 0 if len(self._forecast_mapping) > 0: try: calls += 1 response = requests.get(url=self._forecast_endpoint.format(lat=self._latitude, lon=self._longitude, api_key=self._api_key), headers=self._headers) if response.status_code != 200: self.logger('Forecast call failed: {0}'.format(response.json()['message'])) else: result = response.json()['list'] for sensor in self._forecast_mapping: sensor_id = sensor['sensor_id'] wanted_time = start + (sensor['time_offset'] * 60) selected_entry = None for entry in result: if selected_entry is None or abs(entry['dt'] - wanted_time) < abs(selected_entry['dt'] - wanted_time): selected_entry = entry if selected_entry is None: self.logger('Could not find forecast for virtual sensor {0}'.format(sensor_id)) continue sensor_values[sensor_id] = [selected_entry['main']['temp'], selected_entry['main']['humidity'], None] except Exception as ex: self.logger('Error while fetching forecast temperatures: {0}'.format(ex)) if len(self._current_mapping) > 0: try: calls += 1 response = requests.get(url=self._current_endpoint.format(lat=self._latitude, lon=self._longitude, api_key=self._api_key), headers=self._headers) if response.status_code != 200: self.logger('Current weather call failed: {0}'.format(response.json()['message'])) else: result = response.json() for sensor in self._current_mapping: sensor_id = sensor['sensor_id'] sensor_values[sensor_id] = [result['main']['temp'], result['main']['humidity'], None] except Exception as ex: self.logger('Error while fetching current temperatures: {0}'.format(ex)) if 0 <= self._uv_sensor_id <= 31: try: execute = True while execute is True: calls += 1 lat, lon = round(self._latitude, accuracy), round(self._longitude, accuracy) if accuracy == 0: lat, lon = int(self._latitude), int(self._longitude) response = requests.get(url=self._uv_endpoint.format(lat=lat, lon=lon, date=time.strftime('%Y-%m-%d'), api_key=self._api_key), headers=self._headers) result = response.json() if response.status_code != 200: self.logger('UV index call failed: {0}'.format(result['message'])) if result['message'] == 'not found': if accuracy > 0: accuracy = accuracy - 1 execute = True else: execute = False else: execute = False else: sensor_values[self._uv_sensor_id] = [result['data'], None, None] execute = False except Exception as ex: self.logger('Error while fetching UV index: {0}'.format(ex)) # Push all sensor data try: for sensor_id, values in sensor_values.items(): if values != previous_values.get(sensor_id, []): self.logger('Updating sensor {0} to temp: {1}, humidity: {2}'.format(sensor_id, values[0] if values[0] is not None else '-', values[1] if values[1] is not None else '-')) previous_values[sensor_id] = values result = json.loads(self.webinterface.set_virtual_sensor(None, sensor_id, *values)) if result['success'] is False: self.logger('Error when updating virtual sensor {0}: {1}'.format(sensor_id, result['msg'])) except Exception as ex: self.logger('Error while setting virtual sensors: {0}'.format(ex)) # Wait a given amount of seconds sleep = 60 * calls - (time.time() - start) + 1 if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(5) @om_expose def get_config_description(self): return json.dumps(OpenWeatherMap.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class MQTTClient(OMPluginBase): """ An MQTT client plugin for sending/receiving data to/from an MQTT broker. For more info: https://github.com/openmotics/plugins/blob/master/mqtt-client/README.md """ name = 'MQTTClient' version = '1.3.3' interfaces = [('config', '1.0')] config_description = [{'name': 'broker_ip', 'type': 'str', 'description': 'IP or hostname of the MQTT broker.'}, {'name': 'broker_port', 'type': 'int', 'description': 'Port of the MQTT broker. Default: 1883'}, {'name': 'username', 'type': 'str', 'description': 'Username'}, {'name': 'password', 'type': 'str', 'description': 'Password'}] default_config = {'broker_port': 1883} def __init__(self, webinterface, logger): super(MQTTClient, self).__init__(webinterface, logger) self.logger('Starting MQTTClient plugin...') self._config = self.read_config(MQTTClient.default_config) self._config_checker = PluginConfigChecker(MQTTClient.config_description) paho_mqtt_egg = '/opt/openmotics/python/plugins/MQTTClient/paho_mqtt-1.4.0-py2.7.egg' if paho_mqtt_egg not in sys.path: sys.path.insert(0, paho_mqtt_egg) self.client = None self._outputs = {} self._inputs = {} self._read_config() self._try_connect() self._load_configuration() self.logger("Started MQTTClient plugin") def _read_config(self): self._ip = self._config.get('broker_ip') self._port = self._config.get('broker_port', MQTTClient.default_config['broker_port']) self._username = self._config.get('username') self._password = self._config.get('password') self._enabled = self._ip is not None and self._port is not None self.logger('MQTTClient is {0}'.format('enabled' if self._enabled else 'disabled')) def _load_configuration(self): # Inputs try: result = json.loads(self.webinterface.get_input_configurations(None)) if result['success'] is False: self.logger('Failed to load input configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) self._inputs[input_id] = config for input_id in self._inputs.keys(): if input_id not in ids: del self._inputs[input_id] except CommunicationTimedOutException: self.logger('Error while loading input configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading input configurations: {0}'.format(ex)) # Outputs try: result = json.loads(self.webinterface.get_output_configurations(None)) if result['success'] is False: self.logger('Failed to load output configurations') else: ids = [] for config in result['config']: if config['module_type'] not in ['o', 'O', 'd', 'D']: continue output_id = config['id'] ids.append(output_id) self._outputs[output_id] = {'name': config['name'], 'module_type': {'o': 'output', 'O': 'output', 'd': 'dimmer', 'D': 'dimmer'}[config['module_type']], 'floor': config['floor'], 'type': 'relay' if config['type'] == 0 else 'light'} for output_id in self._outputs.keys(): if output_id not in ids: del self._outputs[output_id] except CommunicationTimedOutException: self.logger('Error while loading output configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading output configurations: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_output_status(None)) if result['success'] is False: self.logger('Failed to get output status') else: for output in result['status']: output_id = output['id'] if output_id not in self._outputs: continue self._outputs[output_id]['status'] = output['status'] self._outputs[output_id]['dimmer'] = output['dimmer'] except CommunicationTimedOutException: self.logger('Error getting output status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting output status: {0}'.format(ex)) def _try_connect(self): if self._enabled is True: try: import paho.mqtt.client as client self.client = client.Client() if self._username is not None: self.logger("MQTTClient is using username/password") self.client.username_pw_set(self._username, self._password) self.client.on_message = self.on_message self.client.on_connect = self.on_connect self.client.connect(self._ip, self._port, 5) self.client.loop_start() except Exception as ex: self.logger('Error connecting to MQTT broker: {0}'.format(ex)) def _log(self, info): thread = Thread(target=self._send, args=('openmotics/logging', info), kwargs={'retain': False}) thread.start() def _send(self, topic, data, retain=True): try: self.client.publish(topic, json.dumps(data), retain=retain) except Exception as ex: self.logger('Error sending data to broker: {0}'.format(ex)) @input_status def input_status(self, status): if self._enabled is True: input_id = status[0] try: if input_id in self._inputs: name = self._inputs[input_id].get('name') self._log('Input {0} ({1}) pressed'.format(input_id, name)) self.logger('Input {0} ({1}) pressed'.format(input_id, name)) data = {'id': input_id, 'name': name, 'timestamp': time.time()} thread = Thread(target=self._send, args=('openmotics/events/input/{0}'.format(input_id), data)) thread.start() else: self.logger('Got event for unknown input {0}'.format(input_id)) except Exception as ex: self.logger('Error processing input {0}: {1}'.format(input_id, ex)) @output_status def output_status(self, status): if self._enabled is True: try: on_outputs = {} for entry in status: on_outputs[entry[0]] = entry[1] outputs = self._outputs for output_id in outputs: status = outputs[output_id].get('status') dimmer = outputs[output_id].get('dimmer') name = outputs[output_id].get('name') if status is None or dimmer is None: continue changed = False if output_id in on_outputs: if status != 1: changed = True outputs[output_id]['status'] = 1 self._log('Output {0} ({1}) changed to ON'.format(output_id, name)) self.logger('Output {0} changed to ON'.format(output_id)) if dimmer != on_outputs[output_id]: changed = True outputs[output_id]['dimmer'] = on_outputs[output_id] self._log('Output {0} ({1}) changed to level {2}'.format(output_id, name, on_outputs[output_id])) self.logger('Output {0} changed to level {1}'.format(output_id, on_outputs[output_id])) elif status != 0: changed = True outputs[output_id]['status'] = 0 self._log('Output {0} ({1}) changed to OFF'.format(output_id, name)) self.logger('Output {0} changed to OFF'.format(output_id)) if changed is True: if outputs[output_id]['module_type'] == 'output': level = 100 else: level = dimmer if outputs[output_id]['status'] == 0: level = 0 data = {'id': output_id, 'name': name, 'value': level, 'timestamp': time.time()} thread = Thread(target=self._send, args=('openmotics/events/output/{0}'.format(output_id), data)) thread.start() except Exception as ex: self.logger('Error processing outputs: {0}'.format(ex)) @receive_events def recv_events(self, id): if self._enabled is True: try: self.logger('Got event {0}'.format(id)) data = {'id': id, 'timestamp': time.time()} thread = Thread(target=self._send, args=('openmotics/events/event/{0}'.format(id), data)) thread.start() except Exception as ex: self.logger('Error processing event: {0}'.format(ex)) def on_connect(self, client, userdata, flags, rc): if rc != 0: self.logger('Error connecting: rc={0}', rc) return self.logger('Connected to MQTT broker {0}:{1}'.format(self._ip, self._port)) try: self.client.subscribe('openmotics/set/output/#') self.logger('Subscribed to openmotics/set/output/#') except Exception as ex: self.logger('Could not subscribe: {0}'.format(ex)) def on_message(self, client, userdata, msg): base_topic = 'openmotics/set/output/' if msg.topic.startswith(base_topic): try: output_id = int(msg.topic.replace(base_topic, '')) if output_id in self._outputs: output = self._outputs[output_id] value = int(msg.payload) if value > 0: is_on = 'true' log_value = 'ON' else: is_on = 'false' log_value = 'OFF' dimmer = None if output['module_type'] == 'dimmer': dimmer = None if value == 0 else max(0, min(100, value)) if value > 0: log_value = 'ON ({0}%)'.format(value) result = json.loads(self.webinterface.set_output(None, output_id, is_on, dimmer, None)) if result['success'] is False: log_message = 'Failed to set output {0} to {1}: {2}'.format(output_id, log_value, result.get('msg', 'Unknown error')) self._log(log_message) self.logger(log_message) else: log_message = 'Output {0} set to {1}'.format(output_id, log_value) self._log(log_message) self.logger(log_message) else: self._log('Unknown output: {0}'.format(output_id)) except Exception as ex: self._log('Failed to process message: {0}'.format(ex)) @om_expose def get_config_description(self): return json.dumps(MQTTClient.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) # Convert unicode to str config['broker_ip'] = config['broker_ip'].encode('ascii', 'ignore') config['username'] = config['username'].encode('ascii', 'ignore') config['password'] = config['password'].encode('ascii', 'ignore') self._config_checker.check_config(config) self.write_config(config) self._config = config self._read_config() if self._enabled: thread = Thread(target=self._load_configuration) thread.start() self._try_connect() return json.dumps({'success': True})
class Hue(OMPluginBase): name = 'Hue' version = '1.0.0' interfaces = [('config', '1.0')] config_description = [{ 'name': 'api_url', 'type': 'str', 'description': 'The API URL of the Hue Bridge device. E.g. http://192.168.1.2/api' }, { 'name': 'username', 'type': 'str', 'description': 'Hue Bridge generated username.' }, { 'name': 'poll_frequency', 'type': 'int', 'description': 'The frequency used to pull the status of all lights from the Hue bridge in seconds (0 means never)' }, { 'name': 'output_mapping', 'type': 'section', 'description': 'Mapping between OpenMotics Virtual Outputs/Dimmers and Hue Outputs', 'repeat': True, 'min': 0, 'content': [{ 'name': 'output_id', 'type': 'int' }, { 'name': 'hue_output_id', 'type': 'int' }] }] default_config = { 'api_url': 'http://hue/api', 'username': '', 'poll_frequency': 60 } def __init__(self, webinterface, logger): super(Hue, self).__init__(webinterface, logger) self.logger('Starting Hue plugin...') self._config = self.read_config(Hue.default_config) self._config_checker = PluginConfigChecker(Hue.config_description) self._read_config() self._previous_output_state = {} self.logger("Hue plugin started") def _read_config(self): self._api_url = self._config['api_url'] self._output_mapping = self._config.get('output_mapping', []) self._output = self._create_output_object() self._hue = self._create_hue_object() self._username = self._config['username'] self._poll_frequency = self._config['poll_frequency'] self._endpoint = '{0}/{1}/{{0}}'.format(self._api_url, self._username) self._enabled = self._api_url != '' and self._username != '' self.logger( 'Hue is {0}'.format('enabled' if self._enabled else 'disabled')) def _create_output_object(self): # create an object with the OM output IDs as the keys and hue light IDs as the values output_object = {} for entry in self._output_mapping: output_object[entry['output_id']] = entry['hue_output_id'] return output_object def _create_hue_object(self): # create an object with the hue light IDs as the keys and OM output IDs as the values hue_object = {} for entry in self._output_mapping: hue_object[entry['hue_output_id']] = entry['output_id'] return hue_object @output_status def output_status(self, status): if self._enabled is True: try: current_output_state = {} for (output_id, dimmer_level) in status: hue_light_id = self._output.get(output_id) if hue_light_id is not None: key = '{0}_{1}'.format(output_id, hue_light_id) current_output_state[key] = dimmer_level previous_dimmer_level = self._previous_output_state.get( key, 0) if dimmer_level != previous_dimmer_level: self.logger( 'Dimming light {0} from {1} to {2}%'.format( key, previous_dimmer_level, dimmer_level)) thread = Thread(target=self._send, args=(hue_light_id, True, dimmer_level)) thread.start() else: self.logger('Light {0} unchanged at {1}%'.format( key, dimmer_level)) else: self.logger( 'Ignoring light {0}, because it is not a Hue light' .format(output_id)) for previous_key in self._previous_output_state.keys(): (output_id, hue_light_id) = previous_key.split('_') if current_output_state.get(previous_key) is None: self.logger( 'Switching light {0} OFF'.format(previous_key)) thread = Thread(target=self._send, args=(hue_light_id, False, self._previous_output_state.get( previous_key, 0))) thread.start() else: self.logger( 'Light {0} was already on'.format(previous_key)) self._previous_output_state = current_output_state except Exception as ex: self.logger( 'Error processing output_status event: {0}'.format(ex)) def _send(self, hue_light_id, state, dimmer_level): try: old_state = self._getLightState(hue_light_id) brightness = self._dimmerLevelToBrightness(dimmer_level) if old_state != False: if old_state['state'].get('on', False): if state: # light was on in Hue and is still on in OM -> send brightness command to Hue self._setLightState(hue_light_id, {'bri': brightness}) else: # light was on in Hue and is now off in OM -> send off command to Hue self._setLightState(hue_light_id, {'on': False}) else: if state: old_dimmer_level = self._brightnessToDimmerLevel( old_state['state']['bri']) if old_dimmer_level == dimmer_level: # light was off in Hue and is now on in OM with same dimmer level -> switch on command to Hue self._setLightState(hue_light_id, {'on': True}) else: # light was off in Hue and is now on in OM with different dimmer level -> switch on command to Hue and set brightness brightness = self._dimmerLevelToBrightness( dimmer_level) self._setLightState(hue_light_id, { 'on': True, 'bri': brightness }) else: self.logger( 'Unable to read current state for Hue light {0}'.format( hue_light_id)) # sleep to avoid queueing the commands on the Hue bridge # time.sleep(1) except Exception as ex: self.logger('Error sending command to Hue light {0}: {1}'.format( hue_light_id, ex)) def _getLightState(self, hue_light_id): try: start = time.time() response = requests.get( url=self._endpoint.format('lights/{0}').format(hue_light_id)) if response.status_code is 200: hue_light = response.json() self.logger( 'Getting light state for Hue light {0} took {1}s'.format( hue_light_id, round(time.time() - start, 2))) return hue_light else: self.logger( 'Failed to pull state for light {0}'.format(hue_light_id)) return False except Exception as ex: self.logger( 'Error while getting light state for Hue light {0}: {1}'. format(hue_light_id, ex)) def _setLightState(self, hue_light_id, state): try: start = time.time() response = requests.put(url=self._endpoint.format( 'lights/{0}/state').format(hue_light_id), data=json.dumps(state)) if response.status_code is 200: result = response.json() if result[0].get('success') is None: self.logger( 'Setting light state for Hue light {0} returned unexpected result. Response: {1} ({2})' .format(hue_light_id, response.text, response.status_code)) return False self.logger( 'Setting light state for Hue light {0} took {1}s'.format( hue_light_id, round(time.time() - start, 2))) return True else: self.logger( 'Setting light state for Hue light {0} failed. Response: {1} ({2})' .format(response.text, response.status_code)) return False except Exception as ex: self.logger( 'Error while setting light state for Hue light {0} to {1}: {2}' .format(hue_light_id, json.dumps(state), ex)) def _getAllLightsState(self): self.logger('Pulling state for all lights from the Hue bridge') try: response = requests.get(url=self._endpoint.format('lights')) if response.status_code is 200: hue_lights = response.json() for output in self._output_mapping: output_id = output['output_id'] hue_light_id = str(output['hue_output_id']) hue_light = self._parseLightObject( hue_light_id, hue_lights[hue_light_id]) if hue_light.get('on', False): result = json.loads( self.webinterface.set_output( None, str(output_id), 'true', str(hue_light['dimmer_level']))) else: result = json.loads( self.webinterface.set_output( None, str(output_id), 'false')) if result['success'] is False: self.logger( '--> Error when updating output {0}: {1}'.format( output_id, result['msg'])) else: self.logger('--> Failed to pull state for all lights') except Exception as ex: self.logger( '--> Error while getting state for all Hue lights: {0}'.format( ex)) def _parseLightObject(self, hue_light_id, hue_light_object): try: light = { 'id': hue_light_id, 'name': hue_light_object['name'], 'on': hue_light_object['state'].get('on', False), 'brightness': hue_light_object['state'].get('bri', 254) } light['dimmer_level'] = self._brightnessToDimmerLevel( light['brightness']) except Exception as ex: self.logger('--> Error while parsing Hue light {0}: {1}'.format( hue_light_object, ex)) return light def _brightnessToDimmerLevel(self, brightness): return int(round(brightness / 2.54)) def _dimmerLevelToBrightness(self, dimmer_level): return int(round(dimmer_level * 2.54)) @background_task def run(self): if self._enabled: while self._poll_frequency > 0: start = time.time() self._getAllLightsState() # This loop will run approx. every 'poll_frequency' seconds sleep = self._poll_frequency - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) @om_expose def get_config_description(self): return json.dumps(Hue.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Pushsafer(OMPluginBase): """ A Pushsafer (http://www.pushsafer.com) plugin for pushing events through Pushsafer """ name = 'Pushsafer' version = '2.1.0' interfaces = [('config', '1.0')] config_description = [{ 'name': 'privatekey', 'type': 'str', 'description': 'Your Private or Alias key.' }, { 'name': 'input_mapping', 'type': 'section', 'description': 'The mapping between input_id and a given Pushsafer settings', 'repeat': True, 'min': 1, 'content': [{ 'name': 'input_id', 'type': 'int', 'description': 'The ID of the (virtual) input that will trigger the event.' }, { 'name': 'message', 'type': 'str', 'description': 'The message to be send.' }, { 'name': 'title', 'type': 'str', 'description': 'The title of message to be send.' }, { 'name': 'device', 'type': 'str', 'description': 'The device or device group id where the message to be send.' }, { 'name': 'icon', 'type': 'str', 'description': 'The icon which is displayed with the message (a number 1-98).' }, { 'name': 'sound', 'type': 'int', 'description': 'The notification sound of message (a number 0-28 or empty).' }, { 'name': 'vibration', 'type': 'str', 'description': 'How often the device should vibrate (a number 1-3 or empty).' }, { 'name': 'url', 'type': 'str', 'description': 'A URL or URL scheme: https://www.pushsafer.com/en/url_schemes' }, { 'name': 'urltitle', 'type': 'str', 'description': 'the URLs title' }, { 'name': 'time2live', 'type': 'str', 'description': 'Integer number 0-43200: Time in minutes after which message automatically gets purged.' }] }] default_config = { 'privatekey': '', 'input_id': -1, 'message': '', 'title': 'OpenMotics', 'device': '', 'icon': '1', 'sound': '', 'vibration': '', 'url': '', 'urltitle': '', 'time2live': '' } def __init__(self, webinterface, logger): super(Pushsafer, self).__init__(webinterface, logger) self.logger('Starting Pushsafer plugin...') self._config = self.read_config(Pushsafer.default_config) self._config_checker = PluginConfigChecker( Pushsafer.config_description) self._cooldown = {} self._read_config() self.logger("Started Pushsafer plugin") def _read_config(self): self._privatekey = self._config['privatekey'] self._mapping = self._config.get('input_mapping', []) self._endpoint = 'https://www.pushsafer.com/api' self._headers = { 'Content-type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'OpenMotics plugin: Pushsafer' } self._enabled = self._privatekey != '' and len(self._mapping) > 0 self.logger('Pushsafer is {0}'.format( 'enabled' if self._enabled else 'disabled')) def convert(self, data): if isinstance(data, basestring): return str(data) elif isinstance(data, collections.Mapping): return dict(map(self.convert, data.iteritems())) elif isinstance(data, collections.Iterable): return type(data)(map(self.convert, data)) else: return data @input_status def input_status(self, status): now = time.time() if self._enabled is True: input_id = status[0] if self._cooldown.get(input_id, 0) > now - 10: self.logger('Ignored duplicate Input in 10 seconds.') return data_send = False for mapping in self._mapping: if input_id == mapping['input_id']: data = { 'k': self._privatekey, 'm': mapping['message'], 't': mapping['title'], 'd': mapping['device'], 'i': mapping['icon'], 's': mapping['sound'], 'v': mapping['vibration'], 'u': mapping['url'], 'ut': mapping['urltitle'], 'l': mapping['time2live'] } thread = Thread(target=self._send_data, args=(data, )) thread.start() data_send = True if data_send is True: self._cooldown[input_id] = now def _send_data(self, data): try: self.logger('Sending data') response = requests.post(url=self._endpoint, data=data, headers=self._headers, verify=False) if response.status_code != 200: self.logger('Got error response: {0} ({1})'.format( response.text, response.status_code)) else: result = json.loads(response.text) if result['status'] != 1: self.logger('Got error response: {0}'.format( result['error'])) else: self.logger('Got reply: {0}'.format(result['success'])) quotas = [] for data in result['available'].values(): device = data.keys()[0] quotas.append('{0}: {1}'.format(device, data[device])) self.logger('Remaining quotas: {0}'.format( ', '.join(quotas))) except Exception as ex: self.logger('Error sending: {0}'.format(ex)) @om_expose def get_config_description(self): return json.dumps(Pushsafer.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) config = self.convert(config) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Fibaro(OMPluginBase): """ A Fibaro plugin, for controlling devices in your Fibaro Home Center (lite) """ name = 'Fibaro' version = '1.1.5' interfaces = [('config', '1.0')] config_description = [{ 'name': 'ip', 'type': 'str', 'description': 'The IP of the Fibaro Home Center (lite) device. E.g. 1.2.3.4' }, { 'name': 'username', 'type': 'str', 'description': 'Username of a user with the required access.' }, { 'name': 'password', 'type': 'str', 'description': 'Password of the user.' }, { 'name': 'output_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics (Virtual) Outputs and Fibaro Outputs', 'repeat': True, 'min': 0, 'content': [{ 'name': 'output_id', 'type': 'int' }, { 'name': 'fibaro_output_id', 'type': 'int' }] }, { 'name': 'sensor_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics Virtual Sensors and Fibaro Sensors', 'repeat': True, 'min': 0, 'content': [{ 'name': 'sensor_id', 'type': 'int' }, { 'name': 'fibaro_temperature_id', 'type': 'int' }, { 'name': 'fibaro_brightness_id', 'type': 'int' }, { 'name': 'fibaro_brightness_max', 'type': 'int' }] }] default_config = {'ip': '', 'username': '', 'password': ''} def __init__(self, webinterface, logger): super(Fibaro, self).__init__(webinterface, logger) self.logger('Starting Fibaro plugin...') self._config = self.read_config(Fibaro.default_config) self._config_checker = PluginConfigChecker(Fibaro.config_description) self._read_config() self._previous_output_state = {} self.logger("Started Fibaro plugin") def _read_config(self): self._ip = self._config['ip'] self._output_mapping = self._config.get('output_mapping', []) self._sensor_mapping = self._config.get('sensor_mapping', []) self._username = self._config['username'] self._password = self._config['password'] self._endpoint = 'http://{0}/api/{{0}}'.format(self._ip) self._headers = { 'X-Requested-With': 'OpenMotics plugin: Fibaro', 'X-Fibaro-Version': '2' } self._enabled = self._ip != '' and self._username != '' and self._password != '' self.logger( 'Fibaro is {0}'.format('enabled' if self._enabled else 'disabled')) @output_status def output_status(self, status): if self._enabled is True: try: active_outputs = [] for entry in status: active_outputs.append(entry[0]) for entry in self._output_mapping: output_id = entry['output_id'] fibaro_output_id = entry['fibaro_output_id'] is_on = output_id in active_outputs key = '{0}_{1}'.format(output_id, fibaro_output_id) if key in self._previous_output_state: if is_on != self._previous_output_state[key]: thread = Thread( target=self._send, args=('callAction', { 'deviceID': fibaro_output_id, 'name': 'turnOn' if is_on else 'turnOff' })) thread.start() else: thread = Thread(target=self._send, args=('callAction', { 'deviceID': fibaro_output_id, 'name': 'turnOn' if is_on else 'turnOff' })) thread.start() self._previous_output_state[key] = is_on except Exception as ex: self.logger( 'Error processing output_status event: {0}'.format(ex)) def _send(self, action, data): try: url = self._endpoint.format(action) params = '&'.join([ '{0}={1}'.format(key, value) for key, value in data.iteritems() ]) self.logger('Calling {0}?{1}'.format(url, params)) response = requests.get(url=url, params=data, headers=self._headers, auth=(self._username, self._password)) if response.status_code != 202: self.logger('Call failed, received: {0} ({1})'.format( response.text, response.status_code)) return result = response.json() if result['result']['result'] not in [0, 1]: self.logger('Call failed, received: {0} ({1})'.format( response.text, response.status_code)) return except Exception as ex: self.logger('Error during call: {0}'.format(ex)) @background_task def run(self): while True: if self._enabled: start = time.time() try: response = requests.get( url='http://{0}/api/devices'.format(self._ip), headers=self._headers, auth=(self._username, self._password)) if response.status_code != 200: self.logger('Failed to load power devices') else: sensor_values = {} result = response.json() for device in result: if 'properties' in device: for sensor in self._sensor_mapping: sensor_id = sensor['sensor_id'] if sensor.get( 'fibaro_temperature_id', -1 ) == device['id'] and 'value' in device[ 'properties']: if sensor_id not in sensor_values: sensor_values[sensor_id] = [ None, None, None ] sensor_values[sensor_id][0] = max( -32.0, min( 95.0, float(device['properties'] ['value']))) if sensor.get( 'fibaro_brightness_id', -1 ) == device['id'] and 'value' in device[ 'properties']: if sensor_id not in sensor_values: sensor_values[sensor_id] = [ None, None, None ] limit = float( sensor.get('fibaro_brightness_max', 500)) value = float( device['properties']['value']) sensor_values[sensor_id][2] = max( 0.0, min(100.0, value / limit * 100)) for sensor_id, values in sensor_values.iteritems(): result = json.loads( self.webinterface.set_virtual_sensor( None, sensor_id, *values)) if result['success'] is False: self.logger( 'Error when updating virtual sensor {0}: {1}' .format(sensor_id, result['msg'])) except Exception as ex: self.logger( 'Error while setting virtual sensors: {0}'.format(ex)) # This loop should run approx. every 5 seconds sleep = 5 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(5) @om_expose def get_power_usage(self): if self._enabled: response = requests.get(url='http://{0}/api/devices'.format( self._ip), headers=self._headers, auth=(self._username, self._password)) if response.status_code != 200: self.logger('Failed to load power devices') return json.dumps({ 'success': False, 'error': 'Could not load power devices' }) result = response.json() devices = {} for device in result: if 'properties' in device and 'power' in device['properties']: devices[device['id']] = { 'id': device['id'], 'name': device['name'], 'power': float(device['properties']['power']), 'counter': float(device['properties']['energy']) * 1000 } return json.dumps({'success': True, 'result': devices}) else: return json.dumps({ 'success': False, 'error': 'Fibaro plugin not enabled' }) @om_expose def get_config_description(self): return json.dumps(Fibaro.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class SMAWebConnect(OMPluginBase): """ Reads out an SMA inverter using WebConnect """ name = 'SMAWebConnect' version = '0.0.36' interfaces = [('config', '1.0'), ('metrics', '1.0')] counter_device_types = ['gas', 'heat', 'water', 'electricity'] counter_unit_types = ['energy', 'volume', 'power', 'flow'] types_mapping = { 'energy': 'energy', 'power': 'energy', 'volume': 'volume', 'flow': 'volume' } default_config = {} FIELD_MAPPING = { '6100_40263F00': { 'name': 'grid_power', 'description': 'Grid power', 'unit': 'W', 'type': 'gauge', 'factor': 1.0 }, '6100_00465700': { 'name': 'frequency', 'description': 'Frequency', 'unit': 'Hz', 'type': 'gauge', 'factor': 100.0 }, '6100_00464800': { 'name': 'voltage_l1', 'description': 'Voltage L1', 'unit': 'V', 'type': 'gauge', 'factor': 100.0 }, '6100_00464900': { 'name': 'voltage_l2', 'description': 'Voltage L2', 'unit': 'V', 'type': 'gauge', 'factor': 100.0 }, '6100_00464A00': { 'name': 'voltage_l3', 'description': 'Voltage L3', 'unit': 'V', 'type': 'gauge', 'factor': 100.0 }, '6100_40465300': { 'name': 'current_l1', 'description': 'Current L1', 'unit': 'A', 'type': 'gauge', 'factor': 1000.0 }, '6100_40465400': { 'name': 'current_l2', 'description': 'Current L2', 'unit': 'A', 'type': 'gauge', 'factor': 1000.0 }, '6100_40465500': { 'name': 'current_l3', 'description': 'Current L3', 'unit': 'A', 'type': 'gauge', 'factor': 1000.0 }, '6100_0046C200': { 'name': 'pv_power', 'description': 'PV power', 'unit': 'W', 'type': 'gauge', 'factor': 1.0 }, '6380_40451F00': { 'name': 'pv_voltage', 'description': 'PV voltage (average of all PV channels)', 'unit': 'V', 'type': 'gauge', 'factor': 100.0 }, '6380_40452100': { 'name': 'pv_current', 'description': 'PV current (average of all PV channels)', 'unit': 'A', 'type': 'gauge', 'factor': 1000.0 }, '6400_0046C300': { 'name': 'pv_gen_meter', 'description': 'PV generation meter', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 }, '6400_00260100': { 'name': 'total_yield', 'description': 'Total yield', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 }, '6400_00262200': { 'name': 'daily_yield', 'description': 'Daily yield', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 }, '6100_40463600': { 'name': 'grid_power_supplied', 'description': 'Grid power supplied', 'unit': 'W', 'type': 'gauge', 'factor': 1.0 }, '6100_40463700': { 'name': 'grid_power_absorbed', 'description': 'Grid power absorbed', 'unit': 'W', 'type': 'gauge', 'factor': 1.0 }, '6400_00462400': { 'name': 'grid_total_yield', 'description': 'Grid total yield', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 }, '6400_00462500': { 'name': 'grid_total_absorbed', 'description': 'Grid total absorbed', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 }, '6100_00543100': { 'name': 'current_consumption', 'description': 'Current consumption', 'unit': 'W', 'type': 'gauge', 'factor': 1.0 }, '6400_00543A00': { 'name': 'total_consumption', 'description': 'Total consumption', 'unit': 'Wh', 'type': 'counter', 'factor': 1.0 } } metric_definitions = [{ 'type': 'sma', 'tags': ['device'], 'metrics': [{ 'name': 'online', 'description': 'Indicates if the SMA device is operating', 'type': 'gauge', 'unit': 'Boolean' }] + [{ 'name': entry['name'], 'description': entry['description'], 'unit': entry['unit'], 'type': entry['type'] } for entry in FIELD_MAPPING.values()] }] def __init__(self, webinterface, logger): super(SMAWebConnect, self).__init__(webinterface, logger) self.logger('Starting SMAWebConnect plugin...') self.config_description = self._create_config_description() self._config = self.read_config(SMAWebConnect.default_config) self._config_checker = PluginConfigChecker(self.config_description) self._metrics_queue = deque() self._enabled = False self._sample_rate = 30 self._sma_devices = {} self._sma_sid = {} self._counter_rate_to_total = {} self._read_config() # Disable HTTPS warnings becasue of self-signed HTTPS certificate on the SMA inverter from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) self.logger("Started SMAWebConnect plugin") def _create_config_description(self): self.pc_config = json.loads( self.webinterface.get_pulse_counter_configurations()) pc_names = [ pc_config['name'] for pc_config in self.pc_config['config'] if pc_config['name'] ] config_description = [{ 'name': 'sample_rate', 'type': 'int', 'description': 'How frequent (every x seconds) to fetch the sensor data, Default: 30' }, { 'name': 'debug', 'type': 'bool', 'description': 'Indicate whether debug logging should be enabled' }, { 'name': 'devices', 'type': 'section', 'description': 'List of all SMA devices.', 'repeat': True, 'min': 1, 'content': [{ 'name': 'sma_inverter_ip', 'type': 'str', 'description': 'IP or hostname of the SMA inverter including the scheme (e.g. http:// or https://).' }, { 'name': 'password', 'type': 'str', 'description': 'The password of the `User` account' }, { 'name': 'counter_mapping', 'type': 'section', 'description': 'Counter mapping', 'repeat': True, 'min': 0, 'content': [ { 'name': 'name', 'type': 'enum', 'description': 'Name of the pulse counter', 'choices': [ v['name'] for v in SMAWebConnect.FIELD_MAPPING.values() ] }, { 'name': 'pulsecounter_name', 'type': 'enum', 'description': 'Name of the counter to which the selected value needs to be mapped', 'choices': pc_names }, { 'name': 'unit_type', 'type': 'enum', 'description': 'Unit type of measurement', 'choices': SMAWebConnect.counter_unit_types }, { 'name': 'convert_to_counter', 'type': 'enum', 'description': 'The read value needs to be made into a counter (i.e. cumulative)', 'choices': ['NO', 'YES'] }, { 'name': 'multiplier', 'type': 'str', 'description': 'Multiplier for readout of values (default=1; required to ensure units of' 'kW(h) or m3(/h) for values given to pulse counters)' }, ] }] }] return config_description def _read_config(self): self._enabled = False self._sample_rate = int(self._config.get('sample_rate', 30)) self._sma_devices = self._config.get('devices', []) self._debug = bool(self._config.get('debug', False)) self._enabled = len(self._sma_devices) > 0 and self._sample_rate > 5 self.logger('SMAWebConnect is {0}'.format( 'enabled' if self._enabled else 'disabled')) def _log_debug(self, message): if self._debug: self.logger(message) @background_task def run(self): while True: if not self._enabled: time.sleep(5) continue for sma_device in self._sma_devices: try: self._read_data(sma_device) except Exception as ex: self.logger( 'Could not read SMA device values: {0}'.format(ex)) time.sleep(self._sample_rate) def _read_data(self, sma_device): metrics_values = {} ip = sma_device['sma_inverter_ip'] while True: sid = self._sma_sid.get(ip, '') endpoint = '{0}/dyn/getValues.json?sid={1}'.format(ip, sid) response = requests.post(endpoint, json={ 'destDev': [], 'keys': SMAWebConnect.FIELD_MAPPING.keys() }, timeout=10, verify=False).json() if response.get('err') == 401: self._login(sma_device) continue break if 'result' not in response or len(response['result']) != 1: raise RuntimeError('Unexpected response: {0}'.format(response)) serial = response['result'].keys()[0] data = response['result'][serial] if data is None: raise RuntimeError('Unexpected response: {0}'.format(response)) self._log_debug('Read values (ip: {0}, serial number: {1}):'.format( ip, serial)) for key, info in SMAWebConnect.FIELD_MAPPING.items(): name = info['name'] unit = info['unit'] if key in data: values = self._extract_values(key, data[key], info['factor']) if len(values) == 0: self._log_debug('* {0}: No values'.format(name)) elif len(values) == 1: value = values[0] self._log_debug('* {0}: {1}{2}'.format( name, value, unit if value is not None else '')) if value is not None: metrics_values[name] = value else: self._log_debug('* {0}:'.format(name)) for value in values: self._log_debug('** {0}{1}'.format( value, unit if value is not None else '')) values = [value for value in values if value is not None] if len(values) == 1: metrics_values[name] = values[0] elif len(values) > 1: metrics_values[name] = sum(values) / len(values) else: self._log_debug('* Missing key: {0}'.format(key)) for key in data: if key not in SMAWebConnect.FIELD_MAPPING.keys(): self._log_debug('* Unknown key {0}: {1}'.format( key, data[key])) offline = 'frequency' not in metrics_values or metrics_values[ 'frequency'] is None metrics_values['online'] = not offline self._enqueue_metrics(serial, metrics_values) self._update_pulsecounter(sma_device, metrics_values) def _extract_values(self, key, values, factor): if len(values) != 1 or '1' not in values: self.logger('* Unexpected structure for {0}: {1}'.format( key, values)) return [] values = values['1'] if len(values) == 0: return [] if len(values) == 1: return [self._clean_value(key, values[0], factor)] return_data = [] for raw_value in values: value = self._clean_value(key, raw_value, factor) if value is not None: return_data.append(value) return return_data def _clean_value(self, key, value_container, factor): if 'val' not in value_container: self.logger('* Unexpected structure for {0}: {1}'.format( key, value_container)) return None value = value_container['val'] if value is None: return None return float(value) / factor def _login(self, sma_device): ip = sma_device['sma_inverter_ip'] endpoint = '{0}/dyn/login.json'.format(ip) response = requests.post(endpoint, json={ 'right': 'usr', 'pass': sma_device['password'] }, verify=False).json() if 'result' in response and 'sid' in response['result']: self._sma_sid[ip] = response['result']['sid'] else: error_code = response.get('err', 'unknown') if error_code == 503: raise RuntimeError('Maximum amount of sessions') raise RuntimeError('Could not login: {0}'.format(error_code)) def _enqueue_metrics(self, device_id, values): try: now = time.time() self._metrics_queue.appendleft({ 'type': 'sma', 'timestamp': now, 'tags': { 'device': device_id }, 'values': values }) except Exception as ex: self.logger( 'Got unexpected error while enqueueing metrics: {0}'.format( ex)) @om_metric_data(interval=15) def collect_metrics(self): try: while True: yield self._metrics_queue.pop() except IndexError: pass def _update_pulsecounter(self, device_config, values): """Add the values defined in counter_mapping not only to influxdb as metrics, but also create and update pulse counters for them. If configured, convert the measurement into a counter, i.e. make it cumulative.""" for mapping in device_config.get('counter_mapping', []): value_name = str(mapping['name']) unit_type = SMAWebConnect.types_mapping.get( mapping.get('unit_type')) current_value = values[value_name] * float( mapping.get('multiplier', 1)) pc_name = str(mapping['pulsecounter_name']) self.pc_config = json.loads( self.webinterface.get_pulse_counter_configurations()) pc_id = [ pc_config['id'] for pc_config in self.pc_config['config'] if pc_config['name'] == pc_name ] if len(pc_id) == 0 or len(pc_id) > 1: self.logger( "No or multiple pulsecounters found for name {0}. Skipping this pulsecounter" .format(pc_name)) continue pc_id = pc_id[0] if not self._counter_rate_to_total.get(pc_name): self._counter_rate_to_total[pc_name] = 0.0 # If the measured value is a power measurement, it needs to be converted to energy. # Assume the value you are getting is in kW and needs to be converted to a kWh counter if mapping.get('unit_type') == 'power' or mapping.get( 'unit_type') == 'flow': self._log_debug('* Converting {0} to {1}...'.format( mapping.get('unit_type'), unit_type)) current_value_amount = current_value * self._sample_rate / 3600 # convert kW or m3/h to kWh or m3 # You need an internal counter, because a pulse counter only works with integers, while a kWh-value # over 1 minute is often lower than 1 self._counter_rate_to_total[pc_name] += current_value_amount if self._counter_rate_to_total[pc_name] < 1.0: self._log_debug( '* {0} for counter {1} has not reached cumulative value of 1 yet (currently {2}) - ' 'adding 0 to pulse counter'.format( mapping.get('unit_type'), pc_name, self._counter_rate_to_total[pc_name])) current_value = 0 else: current_value = self._counter_rate_to_total[pc_name] self._counter_rate_to_total[ pc_name] = self._counter_rate_to_total[pc_name] - 1.0 # If the measured values still need to be made cumulative if mapping.get('convert_to_counter', 'NO') == 'YES': result = json.loads( self.webinterface.get_pulse_counter_status()) previous_value = result['counters'][pc_id] if result[ 'counters'][pc_id] else 0 value = int(current_value + previous_value) else: value = int(current_value) self._log_debug( '* Processing ... Setting counter {0} with name {1} to {2}'. format(pc_id, pc_name, value)) result = json.loads( self.webinterface.set_pulse_counter_status( pulse_counter_id=pc_id, value=value)) if result['success'] is False: self._log_debug( '* Could not update counter value for {0} (ID {1}): {2}'. format(pc_name, pc_id, result.get('msg'))) @om_expose def get_config_description(self): return json.dumps(self.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self.write_config(config) self._config = config self._read_config() return json.dumps({'success': True})
class Pushetta(OMPluginBase): """ A Pushetta (http://www.pushetta.com) plugin for pushing events through Pushetta """ name = 'Pushetta' version = '1.0.12' interfaces = [('config', '1.0')] config_description = [{ 'name': 'api_key', 'type': 'str', 'description': 'Your API key.' }, { 'name': 'input_id', 'type': 'int', 'description': 'The ID of the input that will trigger the event.' }, { 'name': 'channel', 'type': 'str', 'description': 'The channel to push the event to.' }, { 'name': 'message', 'type': 'str', 'description': 'The message to be send.' }] default_config = { 'api_key': '', 'input_id': -1, 'channel': '', 'message': '' } def __init__(self, webinterface, logger): super(Pushetta, self).__init__(webinterface, logger) self.logger('Starting Pushetta plugin...') self._config = self.read_config(Pushetta.default_config) self._config_checker = PluginConfigChecker(Pushetta.config_description) self._read_config() self.logger("Started Pushetta plugin") def _read_config(self): self._api_key = self._config['api_key'] self._input_id = self._config['input_id'] self._channel = self._config['channel'] self._message = self._config['message'] self._endpoint = 'http://api.pushetta.com/api/pushes/{0}/'.format( self._channel) self._headers = { 'Accept': 'application/json', 'Authorization': 'Token {0}'.format(self._api_key), 'Content-type': 'application/json', 'X-Requested-With': 'OpenMotics plugin: Pushetta' } self._enabled = self._api_key != '' and self._input_id > -1 and self._channel != '' and self._message != '' def convert(self, data): if isinstance(data, basestring): return str(data) elif isinstance(data, collections.Mapping): return dict(map(self.convert, data.iteritems())) elif isinstance(data, collections.Iterable): return type(data)(map(self.convert, data)) else: return data @input_status def input_status(self, status): if self._enabled is True: input_id = status[0] if input_id == self._input_id: thread = Thread(target=self._process_input, args=(input_id, )) thread.start() def _process_input(self, input_id): try: data = json.dumps({ 'body': self._message, 'message_type': 'text/plain' }) self.logger('Sending: {0}'.format(data)) response = requests.post(url=self._endpoint, data=data, headers=self._headers, verify=False) self.logger('Received: {0} ({1})'.format(response.text, response.status_code)) except Exception as ex: self.logger('Error sending: {0}'.format(ex)) @om_expose def get_config_description(self): return json.dumps(Pushetta.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) config = self.convert(config) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class ModbusTCPSensor(OMPluginBase): """ Get sensor values form modbus """ name = 'modbusTCPSensor' version = '1.0.7' interfaces = [('config', '1.0')] config_description = [{'name': 'modbus_server_ip', 'type': 'str', 'description': 'IP or hostname of the ModBus server.'}, {'name': 'modbus_port', 'type': 'int', 'description': 'Port of the ModBus server. Default: 502'}, {'name': 'debug', 'type': 'int', 'description': 'Turn on debugging (0 = off, 1 = on)'}, {'name': 'sample_rate', 'type': 'int', 'description': 'How frequent (every x seconds) to fetch the sensor data, Default: 60'}, {'name': 'sensors', 'type': 'section', 'description': 'OM sensor ID (e.g. 4), a sensor type and a Modbus Address', 'repeat': True, 'min': 0, 'content': [{'name': 'sensor_id', 'type': 'int'}, {'name': 'sensor_type', 'type': 'enum', 'choices': ['temperature', 'humidity', 'brightness']}, {'name': 'modbus_address', 'type': 'int'}, {'name': 'modbus_register_length', 'type': 'int'}]}] default_config = {'modbus_port': 502, 'sample_rate': 60} def __init__(self, webinterface, logger): super(ModbusTCPSensor, self).__init__(webinterface, logger) self.logger('Starting ModbusTCPSensor plugin...') self._config = self.read_config(ModbusTCPSensor.default_config) self._config_checker = PluginConfigChecker(ModbusTCPSensor.config_description) py_modbus_tcp_egg = '/opt/openmotics/python/plugins/modbusTCPSensor/pyModbusTCP-0.1.7-py2.7.egg' if py_modbus_tcp_egg not in sys.path: sys.path.insert(0, py_modbus_tcp_egg) self._client = None self._samples = [] self._save_times = {} self._read_config() self.logger("Started ModbusTCPSensor plugin") def _read_config(self): self._ip = self._config.get('modbus_server_ip') self._port = self._config.get('modbus_port', ModbusTCPSensor.default_config['modbus_port']) self._debug = self._config.get('debug', 0) == 1 self._sample_rate = self._config.get('sample_rate', ModbusTCPSensor.default_config['sample_rate']) self._sensors = [] for sensor in self._config.get('sensors', []): if 0 <= sensor['sensor_id'] < 32: self._sensors.append(sensor) self._enabled = len(self._sensors) > 0 try: from pyModbusTCP.client import ModbusClient self._client = ModbusClient(self._ip, self._port, auto_open=True, auto_close=True) self._client.open() self._enabled = self._enabled & True except Exception as ex: self.logger('Error connecting to Modbus server: {0}'.format(ex)) self.logger('ModbusTCPSensor is {0}'.format('enabled' if self._enabled else 'disabled')) def clamp_sensor(self, value, sensor_type): clamping = {'temperature': [-32, 95.5, 1], 'humidity': [0, 100, 1], 'brightness': [0, 100, 0]} return round(max(clamping[sensor_type][0], min(value, clamping[sensor_type][1])), clamping[sensor_type][2]) @background_task def run(self): while True: try: if not self._enabled or self._client is None: time.sleep(5) continue om_sensors = {} for sensor in self._sensors: registers = self._client.read_holding_registers(sensor['modbus_address'], sensor['modbus_register_length']) if registers is None: continue sensor_value = struct.unpack('>f', struct.pack('BBBB', registers[1] >> 8, registers[1] & 255, registers[0] >> 8, registers[0] & 255))[0] if not om_sensors.get(sensor['sensor_id']): om_sensors[sensor['sensor_id']] = {'temperature': None, 'humidity': None, 'brightness': None} sensor_value = self.clamp_sensor(sensor_value, sensor['sensor_type']) om_sensors[sensor['sensor_id']][sensor['sensor_type']] = sensor_value if self._debug == 1: self.logger('The sensors values are: {0}'.format(om_sensors)) for sensor_id, values in om_sensors.iteritems(): result = json.loads(self.webinterface.set_virtual_sensor(sensor_id, **values)) if result['success'] is False: self.logger('Error when updating virtual sensor {0}: {1}'.format(sensor_id, result['msg'])) time.sleep(self._sample_rate) except Exception as ex: self.logger('Could not process sensor values: {0}'.format(ex)) time.sleep(15) @om_expose def get_config_description(self): return json.dumps(ModbusTCPSensor.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self.write_config(config) self._config = config self._read_config() return json.dumps({'success': True})
class InfluxDB(OMPluginBase): """ An InfluxDB plugin, for sending statistics to InfluxDB """ name = 'InfluxDB' version = '2.0.56' interfaces = [('config', '1.0')] config_description = [{'name': 'url', 'type': 'str', 'description': 'The enpoint for the InfluxDB using HTTP. E.g. http://1.2.3.4:8086'}, {'name': 'username', 'type': 'str', 'description': 'Optional username for InfluxDB authentication.'}, {'name': 'password', 'type': 'str', 'description': 'Optional password for InfluxDB authentication.'}, {'name': 'database', 'type': 'str', 'description': 'The InfluxDB database name to witch statistics need to be send.'}, {'name': 'add_custom_tag', 'type': 'str', 'description': 'Add custom tag to statistics'}, {'name': 'batch_size', 'type': 'int', 'description': 'The maximum batch size of grouped metrics to be send to InfluxDB.'}] default_config = {'url': '', 'database': 'openmotics'} def __init__(self, webinterface, logger): super(InfluxDB, self).__init__(webinterface, logger) self.logger('Starting InfluxDB plugin...') self._config = self.read_config(InfluxDB.default_config) self._config_checker = PluginConfigChecker(InfluxDB.config_description) self._pending_metrics = {} self._send_queue = deque() self._batch_sizes = [] self._queue_sizes = [] self._stats_time = 0 self._send_thread = Thread(target=self._sender) self._send_thread.setName('InfluxDB batch sender') self._send_thread.daemon = True self._send_thread.start() self._read_config() self.logger("Started InfluxDB plugin") def _read_config(self): self._url = self._config['url'] self._database = self._config['database'] self._batch_size = self._config.get('batch_size', 10) username = self._config.get('username', '') password = self._config.get('password', '') self._auth = None if username == '' else (username, password) self._add_custom_tag = self._config.get('add_custom_tag', '') self._endpoint = '{0}/write?db={1}'.format(self._url, self._database) self._query_endpoint = '{0}/query?db={1}&epoch=ns'.format(self._url, self._database) self._headers = {'X-Requested-With': 'OpenMotics plugin: InfluxDB'} self._enabled = self._url != '' and self._database != '' self.logger('InfluxDB is {0}'.format('enabled' if self._enabled else 'disabled')) @om_metric_receive(interval=10) def _receive_metric_data(self, metric): """ All metrics are collected, as filtering is done more finegraded when mapping to tables > example_metric = {"source": "OpenMotics", > "type": "energy", > "timestamp": 1497677091, > "tags": {"device": "OpenMotics energy ID1", > "id": 0}, > "values": {"power": 1234, > "power_counter": 1234567}} """ try: if self._enabled is False: return values = metric['values'] _values = {} for key in values.keys()[:]: value = values[key] if isinstance(value, basestring): value = '"{0}"'.format(value) if isinstance(value, bool): value = str(value) if isinstance(value, int): value = '{0}i'.format(value) _values[key] = value tags = {'source': metric['source'].lower()} if self._add_custom_tag: tags['custom_tag'] = self._add_custom_tag for tag, tvalue in metric['tags'].iteritems(): if isinstance(tvalue, basestring): tags[tag] = tvalue.replace(' ', '\ ').replace(',', '\,') else: tags[tag] = tvalue entry = self._build_entry(metric['type'], tags, _values, metric['timestamp'] * 1000000000) self._send_queue.appendleft(entry) except Exception as ex: self.logger('Error receiving metrics: {0}'.format(ex)) @staticmethod def _build_entry(key, tags, value, timestamp): if isinstance(value, dict): values = ','.join('{0}={1}'.format(vname, vvalue) for vname, vvalue in value.iteritems()) else: values = 'value={0}'.format(value) return '{0},{1} {2}{3}'.format(key, ','.join('{0}={1}'.format(tname, tvalue) for tname, tvalue in tags.iteritems()), values, '' if timestamp is None else ' {:.0f}'.format(timestamp)) def _sender(self): while True: try: data = [] try: while True: data.append(self._send_queue.pop()) if len(data) == self._batch_size: raise IndexError() except IndexError: pass if len(data) > 0: self._batch_sizes.append(len(data)) self._queue_sizes.append(len(self._send_queue)) response = requests.post(url=self._endpoint, data='\n'.join(data), headers=self._headers, auth=self._auth, verify=False) if response.status_code != 204: self.logger('Send failed, received: {0} ({1})'.format(response.text, response.status_code)) if self._stats_time < time.time() - 1800: self._stats_time = time.time() self.logger('Queue size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format( min(self._queue_sizes), sum(self._queue_sizes) / float(len(self._queue_sizes)), max(self._queue_sizes) )) self.logger('Batch size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format( min(self._batch_sizes), sum(self._batch_sizes) / float(len(self._batch_sizes)), max(self._batch_sizes) )) self._batch_sizes = [] self._queue_sizes = [] except Exception as ex: self.logger('Error sending from queue: {0}'.format(ex)) time.sleep(0.1) @om_expose def get_config_description(self): return json.dumps(InfluxDB.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Syncer(OMPluginBase): """ A syncer plugin to let two Gateways work together """ name = 'Syncer' version = '0.0.1' interfaces = [('config', '1.0')] config_description = [{'name': 'gateway_ip', 'type': 'str', 'description': 'The IP address of the other Gateway'}, {'name': 'username', 'type': 'str', 'description': 'The (local) username for the other Gateway'}, {'name': 'password', 'type': 'str', 'description': 'The (local) password for the other Gateway'}, {'name': 'sensors', 'type': 'section', 'description': 'Mapping betweet local (virtual) sensors and remote (physical or virtual) sensors. Direction: from remote to local', 'repeat': True, 'min': 0, 'content': [{'name': 'local_sensor_id', 'type': 'int'}, {'name': 'remote_sensor_id', 'type': 'int'}]}, {'name': 'outputs', 'type': 'section', 'description': 'Mapping betweet local (virtual) outputs and remote (physical or virtual) outputs. Direction: from local to remote', 'repeat': True, 'min': 0, 'content': [{'name': 'local_output_id', 'type': 'int'}, {'name': 'remote_output_id', 'type': 'int'}]}] default_config = {} def __init__(self, webinterface, logger): super(Syncer, self).__init__(webinterface, logger) self.logger('Starting Syncer plugin...') self._config = self.read_config(Syncer.default_config) self._config_checker = PluginConfigChecker(Syncer.config_description) self._token = None self._enabled = False self._previous_outputs = set() self._read_config() self.logger("Started Syncer plugin") def _read_config(self): self._ip = self._config.get('gateway_ip', '') self._username = self._config.get('username', '') self._password = self._config.get('password', '') self._sensor_mapping = {} for entry in self._config.get('sensors', []): try: self._sensor_mapping[int(entry['local_sensor_id'])] = int(entry['remote_sensor_id']) except Exception as ex: self.logger('Could not load temperature mapping: {0}'.format(ex)) self._output_mapping = {} for entry in self._config.get('outputs', []): try: self._output_mapping[int(entry['local_output_id'])] = int(entry['remote_output_id']) except Exception as ex: self.logger('Could not load output mapping: {0}'.format(ex)) self._headers = {'X-Requested-With': 'OpenMotics plugin: Syncer'} self._endpoint = 'https://{0}/{{0}}'.format(self._ip) self._enabled = self._ip != '' and self._username != '' and self._password != '' self.logger('Syncer is {0}'.format('enabled' if self._enabled else 'disabled')) @background_task def run(self): previous_values = {} while True: if not self._enabled: time.sleep(30) continue try: # Sync sensor values: data_humidities = json.loads(self.webinterface.get_sensor_humidity_status(None)) data_temperatures = json.loads(self.webinterface.get_sensor_temperature_status(None)) if data_humidities['success'] is True and data_temperatures['success'] is True: for sensor_id in range(len(data_temperatures['status'])): if sensor_id not in self._sensor_mapping: continue data = {'sensor_id': sensor_id} humidity = data_humidities['status'][sensor_id] if humidity != 255: data['humidity'] = humidity temperature = data_temperatures['status'][sensor_id] if temperature != 95.5: data['temperature'] = temperature previous = previous_values.setdefault(sensor_id, {}) if previous.get('temperature') != data.get('temperature') or \ previous.get('humidity') != data.get('humidity'): previous_values[sensor_id] = data self._call_remote('set_virtual_sensor', params=data) except Exception as ex: self.logger('Error while syncing sensors: {0}'.format(ex)) time.sleep(60) @output_status def output_status(self, status): if self._enabled is True: try: on_outputs = set() for entry in status: on_outputs.add(entry[0]) for output_id in on_outputs - self._previous_outputs: # Outputs that are turned on thread = Thread(target=self._call_remote, args=('set_output', {'id': output_id, 'is_on': '1'})) thread.start() for output_id in self._previous_outputs - on_outputs: # Outputs that are turned off thread = Thread(target=self._call_remote, args=('set_output', {'id': output_id, 'is_on': '0'})) thread.start() self._previous_outputs = on_outputs except Exception as ex: self.logger('Error processing outputs: {0}'.format(ex)) def _call_remote(self, api_call, params): # TODO: If there's an invalid_token error, call self._login() and try this call again try: if self._token is None: self._login() response = requests.get(self._endpoint.format(api_call), params=params, headers=self._headers) response_data = json.loads(response.text) if response_data.get('success', False) is False: self.logger('Could not execute API call {0}: {1}'.format(api_call, response_data.get('msg', 'Unknown error'))) except Exception as ex: self.logger('Unexpected error during API call {0}: {1}'.format(api_call, ex)) def _login(self): try: response = requests.get(self._endpoint.format('login'), params={'username': self._username, 'password': self._password, 'accept_terms': '1'}, headers=self._headers) response_data = json.loads(response.text) if response_data.get('success', False) is False: self.logger('Could not login: {0}'.format(response_data.get('msg', 'Unknown error'))) self._token = None else: self._token = response_data.get('token') self._headers['Authorization'] = 'Bearer {0}'.format(self._token) except Exception as ex: self.logger('Unexpected error during login: {0}'.format(ex)) self._token = None @om_expose def get_config_description(self): return json.dumps(Syncer.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Healthbox(OMPluginBase): """ A Healthbox 3 plugin, for reading and controlling your Renson Healthbox 3 """ name = 'Healthbox' version = '1.0.0' interfaces = [('config', '1.0'), ('metrics', '1.0')] config_description = [{ 'name': 'serial', 'type': 'str', 'description': 'The serial of the Healthbox 3. E.g. 250424P0031' }] metric_definitions = [{ 'type': 'aqi', 'tags': ['type', 'description', 'serial'], 'metrics': [{ 'name': 'aqi', 'description': 'Global air quality index', 'type': 'gauge', 'unit': 'aqi' }] }] default_config = {'serial': ''} def __init__(self, webinterface, logger): super(Healthbox, self).__init__(webinterface, logger) self.logger('Starting Healthbox plugin...') self._config = self.read_config(Healthbox.default_config) self._config_checker = PluginConfigChecker( Healthbox.config_description) self._read_config() self._previous_output_state = {} self.logger("Started Healthbox plugin") def _read_config(self): self._serial = self._config['serial'] self._sensor_mapping = self._config.get('sensor_mapping', []) self._endpoint = 'http://{0}/v1/api/data/current' self._headers = { 'X-Requested-With': 'OpenMotics plugin: Healthbox', 'X-Healthbox-Version': '2' } self._ip = self._discover_ip_for_serial(self._serial) if self._ip: self.logger( "Healthbox found with serial {0}and ip address {1}".format( self._serial, self._ip)) else: self.logger("Healthbox with serial {0} not found!".format( self._serial)) self._enabled = (self._ip != '' and self._serial != '') self.logger('Healthbox is {0}'.format( 'enabled' if self._enabled else 'disabled')) def _byteify(self, input): if isinstance(input, dict): return { self._byteify(key): self._byteify(value) for key, value in input.items() } elif isinstance(input, list): return [self._byteify(element) for element in input] elif isinstance(input, unicode): return input.encode('utf-8') else: return input def _discover_ip_for_serial(self, serial): hb3Ip = '' # Create a UDP socket for devices discovery sock = socket(AF_INET, SOCK_DGRAM) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) sock.settimeout(5) server_address = ('255.255.255.255', 49152) message = 'RENSON_DEVICE/JSON?' discovered_devices = [] try: sent = sock.sendto(message.encode(), server_address) while True: data, server = sock.recvfrom(4096) if data.decode('UTF-8'): discovered_devices.append(json.loads(data)) else: print('Verification failed') print('Trying again...') except Exception as ex: if len(discovered_devices) == 0: self.logger( 'Error during discovery for serial: {0}'.format(ex)) finally: sock.close() for device in discovered_devices: if device.get('serial') == serial: hb3Ip = device.get('IP') if hb3Ip == '': self.logger( 'Error during discovery for serial: {0}'.format(serial)) return hb3Ip @background_task def run(self): while True: if not self._enabled: start = time.time() try: self._ip = self._discover_ip_for_serial(self._serial) if self._ip: self._enabled = True self.logger('Healthbox is {0}'.format( 'enabled' if self._enabled else 'disabled')) except Exception as ex: self.logger( 'Error while fetching ip address: {0}'.format(ex)) # This loop should run approx. every 60 seconds sleep = 60 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(60) @om_metric_data(interval=15) def get_metric_data(self): if self._enabled: now = time.time() try: response = requests.get(url=self._endpoint.format(self._ip)) if response.status_code != 200: self.logger('Failed to load healthbox data') return result = response.json() serial = result.get('serial') sensors = result.get('sensor') description = result.get('description') if serial and sensors and description: for sensor in result['sensor']: if sensor['type'] == 'global air quality index': yield { 'type': 'aqi', 'timestamp': now, 'tags': { 'type': 'Healthbox', 'description': description, 'serial': serial }, 'values': { 'aqi': float( sensor['parameter']['index']['value']) } } except Exception as ex: self.logger( "Error while fetching metric date from healthbox: {0}". format(ex)) self._enabled = False self.logger('Healthbox is {0}'.format( 'enabled' if self._enabled else 'disabled')) return @om_expose def get_config_description(self): return json.dumps(Healthbox.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Astro(OMPluginBase): """ An astronomical plugin, for providing the system with astronomical data (e.g. whether it's day or not, based on the sun's location) """ name = 'Astro' version = '0.6.4' interfaces = [('config', '1.0')] config_description = [{'name': 'location', 'type': 'str', 'description': 'A written location to be translated to coordinates using Google. Leave empty and provide coordinates below to prevent using the Google services.'}, {'name': 'coordinates', 'type': 'str', 'description': 'Coordinates in the form of `lat;long` where both are a decimal numbers with dot as decimal separator. Leave empty to fill automatically using the location above.'}, {'name': 'horizon_bit', 'type': 'int', 'description': 'The bit that indicates whether it is day. -1 when not in use.'}, {'name': 'civil_bit', 'type': 'int', 'description': 'The bit that indicates whether it is day or civil twilight. -1 when not in use.'}, {'name': 'nautical_bit', 'type': 'int', 'description': 'The bit that indicates whether it is day, civil or nautical twilight. -1 when not in use.'}, {'name': 'astronomical_bit', 'type': 'int', 'description': 'The bit that indicates whether it is day, civil, nautical or astronomical twilight. -1 when not in use.'}, {'name': 'bright_bit', 'type': 'int', 'description': 'The bit that indicates the brightest part of the day. -1 when not in use.'}, {'name': 'bright_offset', 'type': 'int', 'description': 'The offset (in minutes) after sunrise and before sunset on which the bright_bit should be set.'}, {'name': 'group_action', 'type': 'int', 'description': 'The ID of a Group Action to be called when another zone is entered. -1 when not in use.'}] default_config = {'location': 'Brussels,Belgium', 'horizon_bit': -1, 'civil_bit': -1, 'nautical_bit': -1, 'astronomical_bit': -1, 'bright_bit': -1, 'bright_offset': 60, 'group_action': -1} def __init__(self, webinterface, logger): super(Astro, self).__init__(webinterface, logger) self.logger('Starting Astro plugin...') self._config = self.read_config(Astro.default_config) self._config_checker = PluginConfigChecker(Astro.config_description) pytz_egg = '/opt/openmotics/python/plugins/Astro/pytz-2017.2-py2.7.egg' if pytz_egg not in sys.path: sys.path.insert(0, pytz_egg) self._bright_bit = -1 self._horizon_bit = -1 self._civil_bit = -1 self._nautical_bit = -1 self._astronomical_bit = -1 self._previous_bits = [None, None, None, None, None] self._sleeper = Event() self._sleep_until = 0 thread = Thread(target=self._sleep_manager) thread.start() self._read_config() self.logger("Started Astro plugin") def _read_config(self): for bit in ['bright_bit', 'horizon_bit', 'civil_bit', 'nautical_bit', 'astronomical_bit']: try: value = int(self._config.get(bit, Astro.default_config[bit])) except ValueError: value = Astro.default_config[bit] setattr(self, '_{0}'.format(bit), value) try: self._bright_offset = int(self._config.get('bright_offset', Astro.default_config['bright_offset'])) except ValueError: self._bright_offset = Astro.default_config['bright_offset'] try: self._group_action = int(self._config.get('group_action', Astro.default_config['group_action'])) except ValueError: self._group_action = Astro.default_config['group_action'] self._previous_bits = [None, None, None, None, None] self._coordinates = None self._enabled = False coordinates = self._config.get('coordinates', '').strip() match = re.match(r'^(-?\d+\.\d+);(-?\d+\.\d+)$', coordinates) if match: self._latitude = match.group(1) self._longitude = match.group(2) self._enable_plugin() else: thread = Thread(target=self._translate_address) thread.start() self.logger('Astro is disabled') def _translate_address(self): wait = 0 location = self._config.get('location', '').strip() if not location: self.logger('No coordinates and no location. Please fill in one of both to enable the Astro plugin.') return while True: api = 'https://maps.googleapis.com/maps/api/geocode/json?address={0}'.format(location) try: coordinates = requests.get(api).json() if coordinates['status'] == 'OK': self._latitude = coordinates['results'][0]['geometry']['location']['lat'] self._longitude = coordinates['results'][0]['geometry']['location']['lng'] self._config['coordinates'] = '{0};{1}'.format(self._latitude, self._longitude) self.write_config(self._config) self._enable_plugin() return error = coordinates['status'] except Exception as ex: error = ex.message if wait == 0: wait = 1 elif wait == 1: wait = 5 elif wait < 60: wait = wait + 5 self.logger('Error calling Google Maps API, waiting {0} minutes to try again: {1}'.format(wait, error)) time.sleep(wait * 60) if self._enabled is True: return # It might have been set in the mean time def _enable_plugin(self): import pytz now = datetime.now(pytz.utc) local_now = datetime.now() self.logger('Latitude: {0} - Longitude: {1}'.format(self._latitude, self._longitude)) self.logger('It\'s now {0} Local time'.format(local_now.strftime('%Y-%m-%d %H:%M:%S'))) self.logger('It\'s now {0} UTC'.format(now.strftime('%Y-%m-%d %H:%M:%S'))) self.logger('Astro is enabled') self._enabled = True # Trigger complete recalculation self._previous_bits = [None, None, None, None, None] self._sleep_until = 0 def _sleep_manager(self): while True: if not self._sleeper.is_set() and self._sleep_until < time.time(): self._sleeper.set() time.sleep(5) def _sleep(self, timestamp): self._sleep_until = timestamp self._sleeper.clear() self._sleeper.wait() @staticmethod def _convert(dt_string): import pytz date = datetime.strptime(dt_string, '%Y-%m-%dT%H:%M:%S+00:00') date = pytz.utc.localize(date) if date.year == 1970: return None return date @background_task def run(self): import pytz self._previous_bits = [None, None, None, None, None] while True: if self._enabled: now = datetime.now(pytz.utc) local_now = datetime.now() local_tomorrow = datetime(local_now.year, local_now.month, local_now.day) + timedelta(days=1) try: data = requests.get('http://api.sunrise-sunset.org/json?lat={0}&lng={1}&date={2}&formatted=0'.format( self._latitude, self._longitude, local_now.strftime('%Y-%m-%d') )).json() sleep = 24 * 60 * 60 bits = [True, True, True, True, True] # ['bright', day, civil, nautical, astronomical] if data['status'] == 'OK': # Load data sunrise = Astro._convert(data['results']['sunrise']) sunset = Astro._convert(data['results']['sunset']) has_sun = sunrise is not None and sunset is not None if has_sun is True: bright_start = sunrise + timedelta(minutes=self._bright_offset) bright_end = sunset - timedelta(minutes=self._bright_offset) has_bright = bright_start < bright_end else: has_bright = False civil_start = Astro._convert(data['results']['civil_twilight_begin']) civil_end = Astro._convert(data['results']['civil_twilight_end']) has_civil = civil_start is not None and civil_end is not None nautical_start = Astro._convert(data['results']['nautical_twilight_begin']) nautical_end = Astro._convert(data['results']['nautical_twilight_end']) has_nautical = nautical_start is not None and nautical_end is not None astronomical_start = Astro._convert(data['results']['astronomical_twilight_begin']) astronomical_end = Astro._convert(data['results']['astronomical_twilight_end']) has_astronomical = astronomical_start is not None and astronomical_end is not None # Analyse data if not any([has_sun, has_civil, has_nautical, has_astronomical]): # This is an educated guess; Polar day (sun never sets) and polar night (sun never rises) can # happen in the polar circles. However, since we have far more "gradients" in the night part, # polar night (as defined here - pitch black) only happens very close to the poles. So it's # unlikely this plugin is used there. info = 'polar day' bits = [True, True, True, True, True] sleep = (local_tomorrow - local_now).total_seconds() else: if has_bright is False: bits[0] = False else: bits[0] = bright_start < now < bright_end if bits[0] is True: sleep = min(sleep, int((bright_end - now).total_seconds())) elif now < bright_start: sleep = min(sleep, int((bright_start - now).total_seconds())) if has_sun is False: bits[1] = False else: bits[1] = sunrise < now < sunset if bits[1] is True: sleep = min(sleep, (sunset - now).total_seconds()) elif now < sunrise: sleep = min(sleep, (sunrise - now).total_seconds()) if has_civil is False: if has_sun is True: bits[2] = not bits[1] else: bits[2] = False else: bits[2] = civil_start < now < civil_end if bits[2] is True: sleep = min(sleep, (civil_end - now).total_seconds()) elif now < sunrise: sleep = min(sleep, (civil_start - now).total_seconds()) if has_nautical is False: if has_sun is True or has_civil is True: bits[3] = not bits[2] else: bits[3] = False else: bits[3] = nautical_start < now < nautical_end if bits[3] is True: sleep = min(sleep, (nautical_end - now).total_seconds()) elif now < sunrise: sleep = min(sleep, (nautical_start - now).total_seconds()) if has_astronomical is False: if has_sun is True or has_civil is True or has_nautical is True: bits[4] = not bits[3] else: bits[4] = False else: bits[4] = astronomical_start < now < astronomical_end if bits[4] is True: sleep = min(sleep, (astronomical_end - now).total_seconds()) elif now < sunrise: sleep = min(sleep, (astronomical_start - now).total_seconds()) sleep = min(sleep, int((local_tomorrow - local_now).total_seconds())) info = 'night' if bits[4] is True: info = 'astronimical twilight' if bits[3] is True: info = 'nautical twilight' if bits[2] is True: info = 'civil twilight' if bits[1] is True: info = 'day' if bits[0] is True: info = 'day (bright)' # Set bits in system for index, bit in {0: self._bright_bit, 1: self._horizon_bit, 2: self._civil_bit, 3: self._nautical_bit, 4: self._astronomical_bit}.iteritems(): if bit > -1: result = json.loads(self.webinterface.do_basic_action(None, 237 if bits[index] else 238, bit)) if result['success'] is False: self.logger('Failed to set bit {0} to {1}'.format(bit, 1 if bits[index] else 0)) if self._previous_bits != bits: if self._group_action != -1: result = json.loads(self.webinterface.do_basic_action(None, 2, self._group_action)) if result['success'] is True: self.logger('Group Action {0} triggered'.format(self._group_action)) else: self.logger('Failed to trigger Group Action {0}'.format(self._group_action)) self._previous_bits = bits self.logger('It\'s {0}. Going to sleep for {1} seconds'.format(info, round(sleep, 1))) self._sleep(time.time() + sleep + 5) else: self.logger('Could not load data: {0}'.format(data['status'])) sleep = (local_tomorrow - local_now).total_seconds() self._sleep(time.time() + sleep + 5) except Exception as ex: self.logger('Error figuring out where the sun is: {0}'.format(ex)) sleep = (local_tomorrow - local_now).total_seconds() self._sleep(time.time() + sleep + 5) else: self._sleep(time.time() + 30) @om_expose def get_config_description(self): return json.dumps(Astro.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Hue(OMPluginBase): name = 'Hue' version = '1.1.2' interfaces = [('config', '1.0')] config_description = [{ 'name': 'api_url', 'type': 'str', 'description': 'The API URL of the Hue Bridge device. E.g. http://192.168.1.2/api' }, { 'name': 'username', 'type': 'str', 'description': 'Hue Bridge generated username.' }, { 'name': 'poll_frequency', 'type': 'int', 'description': 'The frequency used to pull the status of all outputs from the Hue bridge in seconds (0 means never)' }, { 'name': 'output_mapping', 'type': 'section', 'description': 'Mapping between OpenMotics Virtual Outputs/Dimmers and Hue Outputs', 'repeat': True, 'min': 0, 'content': [{ 'name': 'output_id', 'type': 'int' }, { 'name': 'hue_output_id', 'type': 'int' }] }] default_config = { 'api_url': 'http://hue/api', 'username': '', 'poll_frequency': 60 } def __init__(self, webinterface, gateway_logger): self.setup_logging(log_function=gateway_logger) super(Hue, self).__init__(webinterface, logger) logger.info('Starting Hue plugin %s ...', self.version) self.discover_hue_bridges() self._config = self.read_config(Hue.default_config) self._config_checker = PluginConfigChecker(Hue.config_description) self._read_config() self._io_lock = Lock() self._output_event_queue = Queue(maxsize=256) logger.info("Hue plugin started") @staticmethod def setup_logging(log_function): # type: (Callable) -> None logger.setLevel(logging.INFO) log_handler = PluginLogHandler(log_function=log_function) # some elements like time and name are added by the plugin runtime already # formatter = logging.Formatter('%(asctime)s - %(name)s - %(threadName)s - %(levelname)s - %(message)s') formatter = logging.Formatter( '%(threadName)s - %(levelname)s - %(message)s') log_handler.setFormatter(formatter) logger.addHandler(log_handler) def _read_config(self): self._api_url = self._config['api_url'] self._output_mapping = self._config.get('output_mapping', []) self._output = self._create_output_object() self._hue = self._create_hue_object() self._username = self._config['username'] self._poll_frequency = self._config['poll_frequency'] self._endpoint = '{0}/{1}/{{0}}'.format(self._api_url, self._username) self._enabled = self._api_url != '' and self._username != '' logger.info( 'Hue is {0}'.format('enabled' if self._enabled else 'disabled')) def _create_output_object(self): # create an object with the OM output IDs as the keys and hue light IDs as the values output_object = {} for entry in self._output_mapping: output_object[entry['output_id']] = entry['hue_output_id'] return output_object def _create_hue_object(self): # create an object with the hue light IDs as the keys and OM output IDs as the values hue_object = {} for entry in self._output_mapping: hue_object[entry['hue_output_id']] = entry['output_id'] return hue_object @output_status(version=2) def output_status(self, output_event): if self._enabled is True: try: output_id = output_event['id'] state = output_event['status'].get('on') dimmer_level = output_event['status'].get('value') hue_light_id = self._output.get(output_id) if hue_light_id is not None: logger.info( 'Switching output %s (hue id: %s) %s (dimmer: %s)', output_id, hue_light_id, 'ON' if state else 'OFF', dimmer_level) data = (hue_light_id, state, dimmer_level) self._output_event_queue.put(data) else: logger.debug( 'Ignoring output %s, because it is not Hue'.format( output_id)) except Exception as ex: logger.exception( 'Error processing output_status event: {0}'.format(ex)) def _send(self, hue_light_id, state, dimmer_level): try: state = {'on': state} if dimmer_level is not None: state.update( {'bri': self._dimmerLevelToBrightness(dimmer_level)}) self._setLightState(hue_light_id, state) except Exception as ex: logger.exception('Error sending command to output with hue id: %s', hue_light_id) def _getLightState(self, hue_light_id): try: start = time.time() response = requests.get( url=self._endpoint.format('lights/{0}').format(hue_light_id)) if response.status_code is 200: hue_light = response.json() logger.info('Getting output state for hue id: %s took %ss', hue_light_id, round(time.time() - start, 2)) return hue_light else: logger.warning('Failed to pull state for hue id: %s', hue_light_id) return False except Exception as ex: logger.exception('Error while getting output state for hue id: %s', hue_light_id) def _setLightState(self, hue_light_id, state): try: start = time.time() response = requests.put(url=self._endpoint.format( 'lights/{0}/state').format(hue_light_id), data=json.dumps(state)) if response.status_code is 200: result = response.json() if result[0].get('success') is None: logger.info( 'Setting output state for Hue output {0} returned unexpected result. Response: {1} ({2})' .format(hue_light_id, response.text, response.status_code)) return False logger.info('Setting output state for hue id: %s took %ss', hue_light_id, round(time.time() - start, 2)) return True else: logger.error( 'Setting output state for hue id: %s failed. Response: %s (%s)', hue_light_id, response.text, response.status_code) return False except Exception as ex: logger.exception( 'Error while setting output state for hue id: %s to %s', hue_light_id, json.dumps(state)) def import_remote_state(self): if not self._output_event_queue.empty(): logger.info( 'Ignoring syncing remote state because we still need to process %s output events', self._output_event_queue.qsize()) else: try: self._import_lights_state() except Exception as ex: logger.exception( 'Error while getting state for all Hue outputs') try: self._import_sensors_state() except Exception as ex: logger.exception( 'Error while getting state for all Hue sensors') def _import_lights_state(self): logger.debug( 'Syncing remote state for all outputs from the Hue bridge') hue_lights = self._getAllLightsState() for output in self._output_mapping: output_id = output['output_id'] hue_light_id = str(output['hue_output_id']) hue_light_state = hue_lights.get(hue_light_id) if hue_light_state is not None: if hue_light_state.get('on', False): result = json.loads( self.webinterface.set_output( None, str(output_id), 'true', str(hue_light_state['dimmer_level']))) else: result = json.loads( self.webinterface.set_output(None, str(output_id), 'false')) if result['success'] is False: logger.error( 'Error when updating output %s (hue id: %s): %s', output_id, hue_light_id, result['msg']) else: logger.warning( 'Output %s (hue id: %s) not found on Hue bridge', output_id, hue_light_id) def _import_sensors_state(self): logger.debug( 'Syncing remote state for all sensors from the Hue bridge') known_sensors = self._get_known_sensors() hue_sensors = self._getAllSensorsState() for hue_sensor_id, sensor in hue_sensors.items(): sensor_external_id = sensor['external_id'] if sensor_external_id not in known_sensors.keys(): name = 'Hue Sensor {}'.format(hue_sensor_id) om_sensor_id = self._register_sensor(name, sensor_external_id) else: om_sensor_id = known_sensors[sensor_external_id] value = float(sensor.get('value')) if om_sensor_id is not None: self._update_sensor(om_sensor_id, value) else: logger.error('Hue sensor %s (%s) not found', hue_sensor_id, sensor_external_id) def _get_known_sensors(self): response = self.webinterface.get_sensor_configurations() data = json.loads(response) return { x['external_id']: x['id'] for x in data['config'] if x.get('source', {}).get('name') == Hue.name and x['external_id'] not in [None, ''] } def _getAllSensorsState(self): hue_sensors = {} response = requests.get(url=self._endpoint.format('sensors')) if response.status_code is 200: for hue_sensor_id, data in response.json().items(): if data.get('type') == 'ZLLTemperature': hue_sensors[hue_sensor_id] = self._parseSensorObject( hue_sensor_id, data, sensor_type='temperature') else: logger.error('Failed to pull state for all sensors (HTTP %s)', response.status_code) return hue_sensors def _getAllLightsState(self): hue_lights = {} response = requests.get(url=self._endpoint.format('lights')) if response.status_code is 200: for hue_light_id, data in response.json().items(): hue_lights[hue_light_id] = self._parseLightObject( hue_light_id, data) else: logger.error('Failed to pull state for all outputs (HTTP %s)', response.status_code) return hue_lights def _parseLightObject(self, hue_light_id, hue_light_object): light = {'id': hue_light_id} try: light.update({ 'name': hue_light_object['name'], 'on': hue_light_object['state'].get('on', False), 'brightness': hue_light_object['state'].get('bri', 254) }) light['dimmer_level'] = self._brightnessToDimmerLevel( light['brightness']) except Exception as ex: logger.exception('Error while parsing Hue light %s', hue_light_object) return light def _parseSensorObject(self, hue_sensor_id, hue_sensor_object, sensor_type='temperature'): sensor = {'id': hue_sensor_id} try: value = hue_sensor_object['state'][sensor_type] if sensor_type == 'temperature': value /= 100.0 sensor.update({ 'external_id': hue_sensor_object['uniqueid'], 'name': hue_sensor_object['name'], 'type': hue_sensor_object['type'], 'value': value }) except Exception as ex: logger.exception('Error while parsing Hue sensor %s', hue_sensor_object) return sensor def _brightnessToDimmerLevel(self, brightness): return int(round(brightness / 2.54)) def _dimmerLevelToBrightness(self, dimmer_level): return int(round(dimmer_level * 2.54)) @background_task def run(self): if self._enabled: event_processor = Thread(target=self.output_event_processor, name='output_event_processor') event_processor.start() self.log_remote_asset_list() self.start_state_poller() def log_remote_asset_list(self): hue_lights = self._getAllLightsState() for hue_id, hue_light in hue_lights.items(): logger.info('Discovered hue output %s (hue id: %s)', hue_light.get('name'), hue_id) hue_sensors = self._getAllSensorsState() for hue_id, hue_sensor in hue_sensors.items(): logger.info('Discovered hue sensor %s (hue id: %s)', hue_sensor.get('name'), hue_id) def start_state_poller(self): while self._poll_frequency > 0: start = time.time() self.import_remote_state() # This loop will run approx. every 'poll_frequency' seconds sleep = self._poll_frequency - (time.time() - start) if sleep < 0: sleep = 1 self.sleep(sleep) @om_expose def get_config_description(self): return json.dumps(Hue.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True}) def output_event_processor(self): while self._enabled: try: _latest_value_buffer = {} while True: try: hue_light_id, status, dimmer = self._output_event_queue.get( block=True, timeout=1) _latest_value_buffer[hue_light_id] = ( status, dimmer ) # this will ensure only the latest value is taken except Empty: break for hue_light_id, (status, dimmer) in _latest_value_buffer.items(): self._send(hue_light_id, status, dimmer) self.sleep( 0.1 ) # "throttle" requests to the bridge to avoid overloading except Exception as ex: if 'maintenance_mode' in ex.message: logger.warning( 'System in maintenance mode. Processing paused for 1 minute.' ) self.sleep(60) else: logger.exception( 'Unexpected error processing output events') self.sleep(10) def sleep(self, timer): now = time.time() expired = time.time() - now > timer while self._enabled and not expired: time.sleep(min(timer, 0.5)) expired = time.time() - now > timer def _getLightState(self, hue_light_id): try: start = time.time() response = requests.get(url=self._endpoint.format( 'lights/{0}').format(hue_light_id)) if response.status_code is 200: hue_light = response.json() logger.info( 'Getting output state for hue id: %s took %ss', hue_light_id, round(time.time() - start, 2)) return hue_light else: logger.warning('Failed to pull state for hue id: %s', hue_light_id) return False except Exception as ex: logger.exception( 'Error while getting output state for hue id: %s', hue_light_id) def discover_hue_bridges(self): try: response = requests.get(url='https://discovery.meethue.com/') if response.status_code is 200: hue_bridge_data = response.json() for hue_bridge in hue_bridge_data: logger.info('Discovered hue bridge %s @ %s', hue_bridge.get('id'), hue_bridge.get('internalipaddress')) else: logger.warning('Failed to discover bridges on this network') return False except Exception as ex: logger.exception( 'Error while discovering hue bridges on this network') def _register_sensor(self, name, external_id): logger.debug('Registering sensor with name %s and external_id %s', name, external_id) try: config = { 'name': name, } response = self.webinterface.sensor.register( external_id=external_id, physical_quantity='temperature', unit='celcius', config=config) return response.id except Exception as e: logger.warning( 'Failed registering sensor with name %s and external_id %s with exception %s', name, external_id, str(e.message)) return None def _update_sensor(self, sensor_id, value): logger.debug('Updating sensor %s with status %s', sensor_id, value) response = self.webinterface.sensor.set_status(sensor_id=sensor_id, value=value) if response is None: logger.warning('Could not set the updated sensor value') return False return True
class MQTTClient(OMPluginBase): """ An MQTT client plugin for sending/receiving data to/from an MQTT broker. For more info: https://github.com/openmotics/plugins/blob/master/mqtt-client/README.md """ name = 'MQTTClient' version = '3.0.1' interfaces = [('config', '1.0')] energy_module_config = { 1: 8, 8: 8, 12: 12 } config_description = [ {'name': 'hostname', 'type': 'str', 'description': 'MQTT broker hostname or IP address.'}, {'name': 'port', 'type': 'int', 'description': 'MQTT broker port. Default: 1883'}, {'name': 'username', 'type': 'str', 'description': 'MQTT broker username. Default: openmotics'}, {'name': 'password', 'type': 'password', 'description': 'MQTT broker password'}, # input status {'name': 'input_status_enabled', 'type': 'bool', 'description': 'Enable input status publishing of messages.'}, {'name': 'input_status_topic_format', 'type': 'str', 'description': 'Input status topic format. Default: openmotics/input/{id}/state'}, {'name': 'input_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Input status message quality of service. Default: 0'}, {'name': 'input_status_retain', 'type': 'bool', 'description': 'Input status message retain.'}, # output status {'name': 'output_status_enabled', 'type': 'bool', 'description': 'Enable output status publishing of messages.'}, {'name': 'output_status_topic_format', 'type': 'str', 'description': 'Output status topic format. Default: openmotics/output/{id}/state'}, {'name': 'output_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Output status message quality of service. Default: 0'}, {'name': 'output_status_retain', 'type': 'bool', 'description': 'Output status message retain.'}, # event status {'name': 'event_status_enabled', 'type': 'bool', 'description': 'Enable event status publishing of messages.'}, {'name': 'event_status_topic_format', 'type': 'str', 'description': 'Event status topic format. Default: openmotics/event/{id}/state'}, {'name': 'event_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Event status message quality of service. Default: 0'}, {'name': 'event_status_retain', 'type': 'bool', 'description': 'Event status message retain.'}, # sensor status {'name': 'sensor_status_enabled', 'type': 'bool', 'description': 'Enable sensor status publishing of messages.'}, {'name': 'sensor_status_topic_format', 'type': 'str', 'description': 'Sensor status topic format. Default: openmotics/sensor/{id}/state'}, {'name': 'sensor_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Sensor status message quality of service. Default: 0'}, {'name': 'sensor_status_retain', 'type': 'bool', 'description': 'Sensor status message retain.'}, {'name': 'sensor_status_poll_frequency', 'type': 'int', 'description': 'Polling frequency for sensor status in seconds. Default: 300, minimum: 10'}, # power status {'name': 'power_status_enabled', 'type': 'bool', 'description': 'Enable power status publishing of messages.'}, {'name': 'power_status_topic_format', 'type': 'str', 'description': 'Power status topic format. Default: openmotics/power/{module_id}/{sensor_id}/state'}, {'name': 'power_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Power status quality of Service. Default: 0'}, {'name': 'power_status_retain', 'type': 'bool', 'description': 'Power status retain.'}, {'name': 'power_status_poll_frequency', 'type': 'int', 'description': 'Polling frequency for power status in seconds. Default: 60, minimum: 10'}, # energy status {'name': 'energy_status_enabled', 'type': 'bool', 'description': 'Enable energy status publishing of messages.'}, {'name': 'energy_status_topic_format', 'type': 'str', 'description': 'Energy status topic format. Default: openmotics/energy/{module_id}/{sensor_id}/state'}, {'name': 'energy_status_qos', 'type': 'enum', 'choices': ['0', '1', '2'], 'description': 'Energy status quality of Service. Default: 0'}, {'name': 'energy_status_retain', 'type': 'bool', 'description': 'Energy status retain.'}, {'name': 'energy_status_poll_frequency', 'type': 'int', 'description': 'Polling frequency for energy status in seconds. Default: 3600 (1 hour), minimum: 10'}, # output command {'name': 'output_command_topic', 'type': 'str', 'description': 'Topic to subscribe to for output command messages. Leave empty to turn off.'}, # logging {'name': 'logging_topic', 'type': 'str', 'description': 'Topic for logging messages. Leave empty to turn off.'}, # timestamp timezone {'name': 'timezone', 'type': 'str', 'description': 'Timezone. Default: UTC. Example: Europe/Brussels'} ] default_config = { 'port': 1883, 'username': '******', 'input_status_topic_format': 'openmotics/input/{id}/state', 'input_status_qos': 0, 'output_status_topic_format': 'openmotics/output/{id}/state', 'output_status_qos': 0, 'event_status_topic_format': 'openmotics/event/{id}/state', 'event_status_qos': 0, 'sensor_status_topic_format': 'openmotics/sensor/{id}/state', 'sensor_status_qos': 0, 'sensor_status_poll_frequency': 300, 'power_status_topic_format': 'openmotics/power/{module_id}/{sensor_id}/state', 'power_status_qos': 0, 'power_status_poll_frequency': 60, 'energy_status_topic_format': 'openmotics/energy/{module_id}/{sensor_id}/state', 'energy_status_qos': 0, 'energy_status_poll_frequency': 3600, 'output_command_topic': 'openmotics/output/+/set', 'logging_topic': 'openmotics/logging', 'timezone': 'UTC' } def __init__(self, webinterface, logger): super(MQTTClient, self).__init__(webinterface, logger) self.logger('Starting MQTTClient plugin...') self._config = self.read_config(MQTTClient.default_config) #self.logger("Default configuration '{0}'".format(self._config)) self._config_checker = PluginConfigChecker(MQTTClient.config_description) paho_mqtt_wheel = '/opt/openmotics/python/plugins/MQTTClient/paho_mqtt-1.5.0-py2-none-any.whl' if paho_mqtt_wheel not in sys.path: sys.path.insert(0, paho_mqtt_wheel) self.client = None self._sensor_config = {} self._inputs = {} self._outputs = {} self._sensors = {} self._power_modules = {} self._read_config() self._try_connect() self._load_configuration() self.logger("Started MQTTClient plugin") def _read_config(self): # broker self._hostname = self._config.get('hostname') self._port = self._config.get('port') self._username = self._config.get('username') self._password = self._config.get('password') # inputs self._input_enabled = self._config.get('input_status_enabled') self._input_topic = self._config.get('input_status_topic_format') self._input_qos = int(self._config.get('input_status_qos')) self._input_retain = self._config.get('input_status_retain') # outputs self._output_enabled = self._config.get('output_status_enabled') self._output_topic = self._config.get('output_status_topic_format') self._output_qos = int(self._config.get('output_status_qos')) self._output_retain = self._config.get('output_status_retain') # events self._event_enabled = self._config.get('event_status_enabled') self._event_topic = self._config.get('event_status_topic_format') self._event_qos = int(self._config.get('event_status_qos')) self._event_retain = self._config.get('event_status_retain') # sensors self._sensor_config = { 'sensor': { 'enabled': self._config.get('sensor_status_enabled'), 'topic': self._config.get('sensor_status_topic_format'), 'qos': int(self._config.get('sensor_status_qos')), 'retain': self._config.get('sensor_status_retain'), 'poll_frequency': int(self._config.get('sensor_status_poll_frequency')) }, 'power': { 'enabled': self._config.get('power_status_enabled'), 'topic': self._config.get('power_status_topic_format'), 'qos': int(self._config.get('power_status_qos')), 'retain': self._config.get('power_status_retain'), 'poll_frequency': int(self._config.get('power_status_poll_frequency')) }, 'energy': { 'enabled': self._config.get('energy_status_enabled'), 'topic': self._config.get('energy_status_topic_format'), 'qos': int(self._config.get('energy_status_qos')), 'retain': self._config.get('energy_status_retain'), 'poll_frequency': int(self._config.get('energy_status_poll_frequency')) } } self._sensor_enabled = self._sensor_config.get('sensor').get('enabled') self._power_enabled = (self._sensor_config.get('power').get('enabled') or self._sensor_config.get('energy').get('enabled')) # output command self._output_command_topic = self._config.get('output_command_topic') # logging topic self._logging_topic = self._config.get('logging_topic') # timezone self._timezone = self._config.get('timezone') self._enabled = self._hostname is not None and self._port is not None self.logger('MQTTClient is {0}'.format('enabled' if self._enabled else 'disabled')) def _load_configuration(self): inputs_loaded = False outputs_loaded = False sensors_loaded = False power_loaded = False should_load = True while should_load: if not inputs_loaded: inputs_loaded = self._load_input_configuration() if not outputs_loaded: outputs_loaded = self._load_output_configuration() if not sensors_loaded: sensors_loaded = self._load_sensor_configuration() if not power_loaded: power_loaded = self._load_power_configuration() should_load = not all([inputs_loaded, outputs_loaded, sensors_loaded, power_loaded]) if should_load: time.sleep(15) def _load_input_configuration(self): input_config_loaded = True if self._input_enabled: try: result = json.loads(self.webinterface.get_input_configurations()) if result['success'] is False: self.logger('Failed to load input configurations') input_config_loaded = False else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) self._inputs[input_id] = config for input_id in self._inputs.keys(): if input_id not in ids: del self._inputs[input_id] self.logger('Configuring {0} inputs'.format(len(ids))) except Exception as ex: self.logger('Error while loading input configurations: {0}'.format(ex)) input_config_loaded = False try: result = json.loads(self.webinterface.get_input_status()) if result['success'] is False: self.logger('Failed to get input status') input_config_loaded = False else: for input_data in result['status']: input_id = input_data['id'] if input_id not in self._inputs: continue self._inputs[input_id]['status'] = input_data['status'] except Exception as ex: self.logger('Error getting input status: {0}'.format(ex)) input_config_loaded = False return input_config_loaded def _load_output_configuration(self): output_config_loaded = True if self._output_enabled: try: result = json.loads(self.webinterface.get_output_configurations()) if result['success'] is False: output_config_loaded = False self.logger('Failed to load output configurations') else: ids = [] for config in result['config']: if config['module_type'] not in ['o', 'O', 'd', 'D']: continue output_id = config['id'] ids.append(output_id) self._outputs[output_id] = {'name': config['name'], 'module_type': {'o': 'output', 'O': 'output', 'd': 'dimmer', 'D': 'dimmer'}[config['module_type']], 'type': 'relay' if config['type'] == 0 else 'light'} for output_id in self._outputs.keys(): if output_id not in ids: del self._outputs[output_id] self.logger('Configuring {0} outputs'.format(len(ids))) except Exception as ex: output_config_loaded = False self.logger('Error while loading output configurations: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_output_status()) if result['success'] is False: output_config_loaded = False self.logger('Failed to get output status') else: for output in result['status']: output_id = output['id'] if output_id not in self._outputs: continue self._outputs[output_id]['status'] = output['status'] self._outputs[output_id]['dimmer'] = output['dimmer'] except Exception as ex: output_config_loaded = False self.logger('Error getting output status: {0}'.format(ex)) return output_config_loaded def _load_sensor_configuration(self): sensor_config_loaded = True if self._sensor_enabled: try: result = json.loads(self.webinterface.get_sensor_configurations()) if result['success'] is False: sensor_config_loaded = False self.logger('Failed to load sensor configurations: {0}'.format(result.get('msg'))) else: ids = [] for config in result['config']: sensor_id = config['id'] ids.append(sensor_id) self._sensors[sensor_id] = {'name': config['name'], 'external_id': str(config['external_id']), 'physical_quantity': str(config['physical_quantity']), 'source': config.get('source'), 'unit': config.get('unit')} for sensor_id in self._sensors.keys(): if sensor_id not in ids: del self._sensors[sensor_id] self.logger('Configuring {0} sensors'.format(len(ids))) except Exception as ex: sensor_config_loaded = False self.logger('Error while loading sensor configurations: {0}'.format(ex)) return sensor_config_loaded def _load_power_configuration(self): power_config_loaded = True if self._power_enabled: try: result = json.loads(self.webinterface.get_power_modules()) if result['success'] is False: power_config_loaded = False self.logger('Failed to load power configurations: {0}'.format(result.get('msg'))) else: ids = [] for module in result['modules']: module_id = int(module['id']) ids.append(module_id) version = int(module['version']) input_count = MQTTClient.energy_module_config.get(version, 0) module_config = {} if input_count == 0: self.logger('Warning: Skipping energy module {0}, version {1} is currently not supported by this plugin. Only versions: {2}'.format( module_id, version, ', '.join(MQTTClient.energy_module_config.keys()))) continue else: self.logger('Configuring energy module {0} (version {1}) with {2} inputs'.format(module_id, version, input_count)) for input_id in range(0, input_count): module_config[input_id] = {'name': module['input{0}'.format(input_id)], 'sensor': module['sensor{0}'.format(input_id)], 'times': module['times{0}'.format(input_id)], 'inverted': module['inverted{0}'.format(input_id)]} self._power_modules[module_id] = module_config for module_id in self._power_modules.keys(): if module_id not in ids: del self._power_modules[module_id] except Exception as ex: power_config_loaded = False self.logger('Error while loading power configurations: {0}'.format(ex)) return power_config_loaded def _try_connect(self): if self._enabled is True: try: import paho.mqtt.client as client self.client = client.Client() if self._username is not None: self.logger("MQTTClient is using username '{0}' and password".format(self._username)) self.client.username_pw_set(self._username, self._password) self.client.on_message = self.on_message self.client.on_connect = self.on_connect self.client.connect(self._hostname, self._port, 5) self.client.loop_start() except Exception as ex: self.logger('Error connecting to MQTT broker: {0}'.format(ex)) def _log(self, info): # for log messages QoS = 0 and retain = False thread = Thread(target=self._send, args=(self._logging_topic, info, 0, False)) thread.start() def _send(self, topic, data, qos, retain): try: self.client.publish(topic, payload=json.dumps(data), qos=qos, retain=retain) except Exception as ex: self.logger('Error sending data to broker: {0}'.format(ex)) def _timestamp2isoformat(self, timestamp=None): # start with UTC dt = datetime.utcnow() if (timestamp is not None): dt.utcfromtimestamp(float(timestamp)) # localize the UTC date/time, make it "aware" instead of naive dt = pytz.timezone('UTC').localize(dt) # convert to timezone from configuration if self._timezone is not None and self._timezone is not 'UTC': dt = dt.astimezone(pytz.timezone(self._timezone)) return dt.isoformat() @input_status(version=2) def input_status(self, data): if self._enabled and self._input_enabled: input_id = data.get('input_id') status = 'ON' if data.get('status') else 'OFF' try: if input_id in self._inputs: name = self._inputs[input_id].get('name') self._log('Input {0} ({1}) switched {2}'.format(input_id, name, status)) self.logger('Input {0} ({1}) switched {2}'.format(input_id, name, status)) data = {'id': input_id, 'name': name, 'status': status, 'timestamp': self._timestamp2isoformat()} thread = Thread( target=self._send, args=(self._input_topic.format(id=input_id), data, self._input_qos, self._input_retain) ) thread.start() else: self.logger('Got event for unknown input {0}'.format(input_id)) except Exception as ex: self.logger('Error processing input {0}: {1}'.format(input_id, ex)) @output_status def output_status(self, status): if self._enabled and self._output_enabled: try: new_output_status = {} for entry in status: new_output_status[entry[0]] = entry[1] current_output_status = self._outputs for output_id in current_output_status: status = current_output_status[output_id].get('status') dimmer = current_output_status[output_id].get('dimmer') name = current_output_status[output_id].get('name') if status is None or dimmer is None: continue changed = False if output_id in new_output_status: if status != 1: changed = True current_output_status[output_id]['status'] = 1 self._log('Output {0} ({1}) changed to ON'.format(output_id, name)) self.logger('Output {0} ({1}) changed to ON'.format(output_id, name)) if dimmer != new_output_status[output_id]: changed = True current_output_status[output_id]['dimmer'] = new_output_status[output_id] self._log('Output {0} ({1}) changed to level {2}'.format(output_id, name, new_output_status[output_id])) self.logger('Output {0} ({1}) changed to level {2}'.format(output_id, name, new_output_status[output_id])) elif status != 0: changed = True current_output_status[output_id]['status'] = 0 self._log('Output {0} ({1}) changed to OFF'.format(output_id, name)) self.logger('Output {0} ({1}) changed to OFF'.format(output_id, name)) if changed is True: if current_output_status[output_id]['module_type'] == 'output': level = 100 else: level = dimmer if current_output_status[output_id]['status'] == 0: level = 0 data = {'id': output_id, 'name': name, 'value': level, 'timestamp': self._timestamp2isoformat()} thread = Thread( target=self._send, args=(self._output_topic.format(id=output_id), data, self._output_qos, self._output_retain) ) thread.start() except Exception as ex: self.logger('Error processing outputs: {0}'.format(ex)) @receive_events def receive_events(self, event_id): if self._enabled and self._event_enabled: try: self._log('Got event {0}'.format(event_id)) self.logger('Got event {0}'.format(event_id)) data = {'id': event_id, 'timestamp': self._timestamp2isoformat()} thread = Thread( target=self._send, args=(self._event_topic.format(id=event_id), data, self._event_qos, self._event_retain) ) thread.start() except Exception as ex: self.logger('Error processing event: {0}'.format(ex)) @background_task def background_task_sensor_status(self): self._create_background_task( 'sensor', self.webinterface.get_sensor_status, self._process_sensor_status )() @background_task def background_task_realtime_power(self): self._create_background_task( 'power', self.webinterface.get_realtime_power, self._process_realtime_power )() @background_task def background_task_total_energy(self): self._create_background_task( 'energy', self.webinterface.get_total_energy, self._process_total_energy )() def _process_sensor_status(self, sensor_config, json_data): mqtt_messages = [] data_list = list(filter(None, json_data.get('status', []))) for sensor_id, sensor_value in enumerate(data_list): sensor = self._sensors.get(sensor_id) if sensor: sensor_data = {'id': sensor_id, 'source': sensor.get('source'), 'external_id': sensor.get('external_id'), 'physical_quantity': sensor.get('physical_quantity'), 'unit': sensor.get('unit'), 'name': sensor.get('name'), 'value': float(sensor_value), 'timestamp': self._timestamp2isoformat()} mqtt_messages.append({'topic': sensor_config.get('topic').format(id=sensor_id), 'message': sensor_data}) return mqtt_messages def _process_realtime_power(self, sensor_config, json_data): mqtt_messages = [] json_data.pop('success') for module_id, values in json_data.items(): module = self._power_modules.get(int(module_id)) if module: for input_id, sensor_values in enumerate(values): power_input = module.get(int(input_id)) if power_input: sensor_data = {'sensor_id': input_id, 'module_id': module_id, 'name': power_input.get('name'), 'voltage': sensor_values[0], 'frequency': sensor_values[1], 'current': sensor_values[2], 'power': sensor_values[3], 'timestamp': self._timestamp2isoformat()} mqtt_messages.append({'topic': sensor_config.get('topic').format(module_id=module_id, sensor_id=input_id), 'message': sensor_data }) return mqtt_messages def _process_total_energy(self, sensor_config, json_data): mqtt_messages = [] json_data.pop('success') for module_id, values in json_data.items(): module = self._power_modules.get(int(module_id)) if module: for input_id, sensor_values in enumerate(values): power_input = module.get(int(input_id)) if power_input: sensor_data = {'sensor_id': input_id, 'module_id': module_id, 'name': power_input.get('name'), 'day': sensor_values[0], 'night': sensor_values[1], 'timestamp': self._timestamp2isoformat()} mqtt_messages.append({'topic': sensor_config.get('topic').format(module_id=module_id, sensor_id=input_id), 'message': sensor_data}) return mqtt_messages def _create_background_task(self, sensor_type, data_retriever, data_processor): def background_function(): while True: if self._enabled: sensor_config = self._sensor_config.get(sensor_type) frequency = sensor_config.get('poll_frequency') self.logger('Background task to retrieve {0} sensor data started, will run every {1} seconds.'.format(sensor_type, frequency)) # highest frequency is every 10s while frequency >= 10: start = time.time() try: if sensor_config.get('enabled'): result = json.loads(data_retriever()) if result['success'] is False: self.logger('Failed to load {0} sensor data: {1}'.format(sensor_type, result.get('msg'))) else: mqtt_messages = data_processor(sensor_config, result) for mqtt_message in mqtt_messages: thread = Thread(target=self._send, args=(mqtt_message.get('topic'), mqtt_message.get('message'), sensor_config.get('qos'), sensor_config.get('retain'))) thread.start() except Exception as ex: self.logger('Error processing {0} sensor status: {1}'.format(sensor_type, ex)) # This loop will run approx. every 'frequency' seconds sleep = frequency - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(15) return background_function def on_connect(self, client, userdata, flags, rc): if rc != 0: self.logger('Error connecting: rc={0}', rc) return self.logger('Connected to MQTT broker {0}:{1}'.format(self._hostname, self._port)) # subscribe to output command topic if provided if self._output_command_topic: try: self.client.subscribe(self._output_command_topic) self.logger('Subscribed to {0}'.format(self._output_command_topic)) except Exception as ex: self.logger('Could not subscribe: {0}'.format(ex)) def on_message(self, client, userdata, msg): if self._output_command_topic: regexp = self._output_command_topic.replace('+', '(\d+)') if re.search(regexp, msg.topic) is not None: try: # the output_id is the first match of the regular expression output_id = int(re.findall(regexp, msg.topic)[0]) if output_id in self._outputs: output = self._outputs[output_id] value = int(msg.payload) if value > 0: is_on = 'true' else: is_on = 'false' dimmer = None if output['module_type'] == 'dimmer': dimmer = None if value == 0 else max(0, min(100, value)) if value > 0: log_value = 'ON ({0}%)'.format(value) result = json.loads(self.webinterface.set_output(id=output_id, is_on=is_on, dimmer=dimmer)) if result['success'] is False: log_message = 'Failed to set output {0} to {1}: {2}'.format(output_id, value, result.get('msg', 'Unknown error')) self._log(log_message) self.logger(log_message) else: log_message = 'Message for output {0} with payload {1}'.format(output_id, value) self._log(log_message) self.logger(log_message) else: self._log('Unknown output: {0}'.format(output_id)) except Exception as ex: self._log('Failed to process message: {0}'.format(ex)) else: self._log('Message with topic {0} ignored'.format(msg.topic)) self.logger('Message with topic {0} ignored'.format(msg.topic)) @om_expose def get_config_description(self): return json.dumps(MQTTClient.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): try: config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) if self._enabled: thread = Thread(target=self._load_configuration) thread.start() except Exception as ex: self.logger('Error saving configuration: {0}'.format(ex)) self._try_connect() return json.dumps({'success': True})
class TasmotaHTTP(OMPluginBase): """ An Tasmota HTTP plugin """ name = 'tasmotaHTTP' version = '1.0.1' interfaces = [('config', '1.0')] config_description = [{ 'name': 'refresh_interval', 'type': 'int', 'description': 'Refresh interval (in seconds) to fetch values from outputs and push to tasmota devices' }, { 'name': 'tasmota_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics Virtual Sensors and Tasmota devices. See README.', 'repeat': True, 'min': 0, 'content': [{ 'name': 'label', 'type': 'str', 'description': 'Name to identify pair (ip_address, output_id)' }, { 'name': 'ip_address', 'type': 'str', 'description': 'Device IP Address.' }, { 'name': 'username', 'type': 'str', 'description': 'Device username, fill only if authentication is enabled.' }, { 'name': 'password', 'type': 'password', 'description': 'Device password, fill only if authentication is enabled.' }, { 'name': 'output_id', 'type': 'int', 'description': 'OpenMotics output id to sync with Tasmota' }] }] default_config = {'refresh_interval': 2} tasmota_http_endpoint = 'http://{ip_address}/cm?user={user}&password={password}&cmnd=Power%20{action}' def __init__(self, webinterface, logger): super(TasmotaHTTP, self).__init__(webinterface, logger) self.logger('Starting Tasmota HTTP plugin...') self._config = self.read_config(TasmotaHTTP.default_config) self._config_checker = PluginConfigChecker( TasmotaHTTP.config_description) self._read_config() self._previous_output_state = {} self.logger("Started Tasmota HTTP plugin") def _read_config(self): self._refresh_interval = self._config.get('refresh_interval', 5) tasmota_mapping = self._config.get('tasmota_mapping', []) self._tasmota_mapping = tasmota_mapping self._headers = {'X-Requested-With': 'OpenMotics plugin: Tasmota HTTP'} self._enabled = bool(self._tasmota_mapping) self.logger('Tasmota HTTP is {0}'.format( 'enabled' if self._enabled else 'disabled')) @background_task def run(self): previous_values = {} while True: if self._enabled: try: result = json.loads(self.webinterface.get_output_status()) if 'status' in result: for device in self._tasmota_mapping: if not isinstance(device['output_id'], int): continue device_output_id = device['output_id'] for output in result['status']: output_id = output['id'] if output_id != device_output_id: continue if device[ 'label'] in previous_values and previous_values[ device['label']] == output[ 'status']: continue previous_values[ device['label']] = self.update_tasmota( device, output) self.logger('Tasmota device {0} is {1}'.format( device['label'], 'on' if output['status'] == 1 else 'off')) except Exception as ex: self.logger('Failed to get output status: {0}'.format(ex)) # Wait a given amount of seconds time.sleep(self._refresh_interval) else: time.sleep(5) @om_expose def get_config_description(self): return json.dumps(TasmotaHTTP.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True}) def update_tasmota(self, device, output): response = requests.get(url=self.tasmota_http_endpoint.format( ip_address=device['ip_address'], user=device['username'], password=device['password'], action=output['status']), headers=self._headers) if response.status_code == 200: if response.json()['POWER'] == 'ON': return 1 else: self.logger('Failed to update Tasmota device {0}: {1}'.format( device['ip_address'], response.status_code)) return 0
class Ventilation(OMPluginBase): """ A ventilation plugin, using statistical humidity or the dew point data to control the ventilation """ name = 'Ventilation' version = '2.0.14' interfaces = [('config', '1.0'), ('metrics', '1.0')] config_description = [{'name': 'low', 'type': 'section', 'description': 'Output configuration for "low" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'medium', 'type': 'section', 'description': 'Output configuration for "medium" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'high', 'type': 'section', 'description': 'Output configuration for "high" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'sensors', 'type': 'section', 'description': 'Sensors to use for ventilation control.', 'repeat': True, 'min': 1, 'content': [{'name': 'sensor_id', 'type': 'int'}]}, {'name': 'mode', 'type': 'nested_enum', 'description': 'The mode of the ventilation control', 'choices': [{'value': 'statistical', 'content': [{'name': 'samples', 'type': 'int'}, {'name': 'trigger', 'type': 'int'}]}, {'value': 'dew_point', 'content': [{'name': 'outside_sensor_id', 'type': 'int'}, {'name': 'target_lower', 'type': 'int'}, {'name': 'target_upper', 'type': 'int'}, {'name': 'offset', 'type': 'int'}, {'name': 'trigger', 'type': 'int'}]}]}] metric_definitions = [{'type': 'ventilation', 'tags': ['name', 'id'], 'metrics': [{'name': 'dewpoint', 'description': 'Dew point', 'type': 'gauge', 'unit': 'degree C'}, {'name': 'absolute_humidity', 'description': 'Absolute humidity', 'type': 'gauge', 'unit': 'g/m3'}, {'name': 'level', 'description': 'Ventilation level', 'type': 'gauge', 'unit': ''}, {'name': 'medium', 'description': 'Medium ventilation limit', 'type': 'gauge', 'unit': '%'}, {'name': 'high', 'description': 'High ventilation limit', 'type': 'gauge', 'unit': '%'}, {'name': 'mean', 'description': 'Average relative humidity level', 'type': 'gauge', 'unit': '%'}, {'name': 'stddev', 'description': 'Stddev relative humidity level', 'type': 'gauge', 'unit': '%'}, {'name': 'samples', 'description': 'Amount of samples', 'type': 'gauge', 'unit': ''}]}] default_config = {} def __init__(self, webinterface, logger): super(Ventilation, self).__init__(webinterface, logger) self.logger('Starting Ventilation plugin...') self._config = self.read_config(Ventilation.default_config) self._config_checker = PluginConfigChecker(Ventilation.config_description) self._used_sensors = [] self._samples = {} self._sensors = {} self._runtime_data = {} self._settings = {} self._last_ventilation = None self._metrics_queue = deque() self._read_config() self.logger("Started Ventilation plugin") def _read_config(self): self._outputs = {1: self._config.get('low', []), 2: self._config.get('medium', []), 3: self._config.get('medium', [])} self._used_sensors = [sensor['sensor_id'] for sensor in self._config.get('sensors', [])] self._mode, self._settings = self._config.get('mode', ['disabled', {}]) self._enabled = len(self._used_sensors) > 0 and self._mode in ['dew_point', 'statistical'] self.logger('Ventilation is {0}'.format('enabled' if self._enabled else 'disabled')) def _load_sensors(self): try: configs = json.loads(self.webinterface.get_sensor_configurations()) if configs['success'] is False: self.logger('Failed to get sensor configurations: {0}'.format(configs.get('msg', 'Unknown error'))) else: sensor_ids = [] for sensor in configs['config']: sensor_id = sensor['id'] if sensor_id in self._used_sensors or sensor_id == self._settings['outside_sensor_id']: sensor_ids.append(sensor_id) self._samples[sensor_id] = [] self._sensors[sensor_id] = sensor['name'] if sensor['name'] != '' else sensor_id for sensor_id in self._sensors.keys(): if sensor_id not in sensor_ids: self._sensors.pop(sensor_id, None) self._samples.pop(sensor_id, None) except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting sensor status: {0}'.format(ex)) @background_task def run(self): self._runtime_data = {} while True: if self._enabled: start = time.time() self._load_sensors() if self._mode == 'statistical': self._process_statistics() elif self._mode == 'dew_point': self._process_dew_point() # This loop should run approx. every minute. sleep = 60 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(5) def _process_dew_point(self): try: dew_points = {} abs_humidities = {} humidities = {} sensor_temperatures = {} outdoor_abs_humidity = None outdoor_dew_point = None outdoor_sensor_id = self._settings['outside_sensor_id'] # Fetch data data_humidities = json.loads(self.webinterface.get_sensor_humidity_status()) if data_humidities['success'] is False: self.logger('Failed to read humidities: {0}'.format(data_humidities.get('msg', 'Unknown error'))) return data_temperatures = json.loads(self.webinterface.get_sensor_temperature_status()) if data_temperatures['success'] is False: self.logger('Failed to read temperatures: {0}'.format(data_temperatures.get('msg', 'Unknown error'))) return for sensor_id in range(len(data_humidities['status'])): if sensor_id not in self._used_sensors + [outdoor_sensor_id]: continue humidity = data_humidities['status'][sensor_id] if humidity is None: continue temperature = data_temperatures['status'][sensor_id] if temperature is None: continue humidities[sensor_id] = humidity if sensor_id == outdoor_sensor_id: outdoor_dew_point = Ventilation._dew_point(temperature, humidity) outdoor_abs_humidity = Ventilation._abs_humidity(temperature, humidity) else: sensor_temperatures[sensor_id] = temperature dew_points[sensor_id] = Ventilation._dew_point(temperature, humidity) abs_humidities[sensor_id] = Ventilation._abs_humidity(temperature, humidity) if outdoor_abs_humidity is None or outdoor_dew_point is None: self.logger('Could not load outdoor humidity or temperature') return # Calculate required ventilation based on sensor information target_lower = self._settings['target_lower'] target_upper = self._settings['target_upper'] offset = self._settings['offset'] ventilation = 1 trigger_sensors = {1: [], 2: [], 3: []} for sensor_id in dew_points: if sensor_id not in self._sensors: continue if sensor_id not in self._runtime_data: self._runtime_data[sensor_id] = {'trigger': 0, 'ventilation': 1, 'candidate': 1, 'reason': '', 'name': self._sensors[sensor_id], 'stats': [0, 0, 0, 0]} humidity = humidities[sensor_id] dew_point = dew_points[sensor_id] abs_humidity = abs_humidities[sensor_id] temperature = sensor_temperatures[sensor_id] self._runtime_data[sensor_id]['stats'] = [temperature, dew_point, abs_humidity, outdoor_abs_humidity] reason = 'RH {0:.2f} <= {1:.2f} <= {2:.2f}'.format(target_lower, humidity, target_upper) wanted_ventilation = 1 # First, try to get the dew point inside the target range - increasing comfort if humidity < target_lower or humidity > target_upper: if humidity < target_lower and outdoor_abs_humidity > abs_humidity: wanted_ventilation = 2 reason = 'RH {0:.2f} < {1:.2f} and AH {2:.5f} > {3:.5f}'.format(humidity, target_lower, outdoor_abs_humidity, abs_humidity) if humidity > target_upper and outdoor_abs_humidity < abs_humidity: wanted_ventilation = 2 reason = 'RH {0:.2f} > {1:.2f} and AH {2:.5f} < {3:.5f}'.format(humidity, target_upper, outdoor_abs_humidity, abs_humidity) # Second, prevent actual temperature from hitting the dew point - make sure we don't have condense if outdoor_abs_humidity < abs_humidity: if dew_point > temperature - offset: wanted_ventilation = 3 reason = 'DP {0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - offset, temperature) elif dew_point > temperature - 2 * offset: wanted_ventilation = 2 reason = 'DP {0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - 2 * offset, temperature) self._runtime_data[sensor_id]['candidate'] = wanted_ventilation current_ventilation = self._runtime_data[sensor_id]['ventilation'] if current_ventilation != wanted_ventilation: self._runtime_data[sensor_id]['trigger'] += 1 self._runtime_data[sensor_id]['reason'] = reason if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']: self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation self._runtime_data[sensor_id]['trigger'] = 0 current_ventilation = wanted_ventilation trigger_sensors[wanted_ventilation].append(sensor_id) else: self._runtime_data[sensor_id]['reason'] = '' self._runtime_data[sensor_id]['trigger'] = 0 ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation']) self._enqueue_metrics(tags={'id': sensor_id, 'name': self._sensors[sensor_id]}, values={'dewpoint': float(dew_point), 'absolute_humidity': float(abs_humidity), 'level': int(current_ventilation)}) self._enqueue_metrics(tags={'id': outdoor_sensor_id, 'name': self._sensors[outdoor_sensor_id]}, values={'dewpoint': float(outdoor_dew_point), 'absolute_humidity': float(outdoor_abs_humidity), 'level': 0}) if ventilation != self._last_ventilation: if self._last_ventilation is None: self.logger('Updating ventilation to 1 (startup)') else: self.logger('Updating ventilation to {0} because of sensors: {1}'.format( ventilation, ', '.join(['{0} ({1})'.format(self._sensors[sensor_id], self._runtime_data[sensor_id]['reason']) for sensor_id in trigger_sensors[ventilation]]) )) self._set_ventilation(ventilation) self._last_ventilation = ventilation except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error calculating ventilation: {0}'.format(ex)) self.logger('Stacktrace: {0}'.format(traceback.format_exc())) def _process_statistics(self): try: # Fetch data humidities = json.loads(self.webinterface.get_sensor_humidity_status()) if humidities['success'] is True: for sensor_id in range(len(humidities['status'])): if sensor_id not in self._samples: continue value = humidities['status'][sensor_id] if value == 255: continue self._samples[sensor_id].append(value) if len(self._samples[sensor_id]) > self._settings.get('samples', 1440): self._samples[sensor_id].pop(0) # Calculate required ventilation based on sensor information ventilation = 1 trigger_sensors = {1: [], 2: [], 3: []} for sensor_id in self._samples: if sensor_id not in self._sensors: continue if sensor_id not in self._runtime_data: self._runtime_data[sensor_id] = {'trigger': 0, 'ventilation': 1, 'candidate': 1, 'difference': '', 'name': self._sensors[sensor_id], 'stats': [0, 0, 0]} current = self._samples[sensor_id][-1] mean = Ventilation._mean(self._samples[sensor_id]) stddev = Ventilation._stddev(self._samples[sensor_id]) level_2 = mean + 2 * stddev level_3 = mean + 3 * stddev self._runtime_data[sensor_id]['stats'] = [current, level_2, level_3] if current > level_3: wanted_ventilation = 3 difference = '{0:.2f} > {1:.2f}'.format(current, level_3) elif current > level_2: wanted_ventilation = 2 difference = '{0:.2f} > {1:.2f}'.format(current, level_2) else: wanted_ventilation = 1 difference = '{0:.2f} <= {1:.2f}'.format(current, level_2) self._runtime_data[sensor_id]['candidate'] = wanted_ventilation current_ventilation = self._runtime_data[sensor_id]['ventilation'] if current_ventilation != wanted_ventilation: self._runtime_data[sensor_id]['trigger'] += 1 self._runtime_data[sensor_id]['difference'] = difference if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']: self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation self._runtime_data[sensor_id]['trigger'] = 0 current_ventilation = wanted_ventilation trigger_sensors[wanted_ventilation].append(sensor_id) else: self._runtime_data[sensor_id]['difference'] = '' self._runtime_data[sensor_id]['trigger'] = 0 ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation']) self._enqueue_metrics(tags={'id': sensor_id, 'name': self._sensors[sensor_id]}, values={'medium': float(level_2), 'high': float(level_3), 'mean': float(mean), 'stddev': float(stddev), 'samples': len(self._samples[sensor_id]), 'level': int(current_ventilation)}) if ventilation != self._last_ventilation: if self._last_ventilation is None: self.logger('Updating ventilation to 1 (startup)') else: self.logger('Updating ventilation to {0} because of sensors: {1}'.format( ventilation, ', '.join(['{0} ({1})'.format(self._sensors[sensor_id], self._runtime_data[sensor_id]['difference']) for sensor_id in trigger_sensors[ventilation]]) )) self._set_ventilation(ventilation) self._last_ventilation = ventilation except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error calculating ventilation: {0}'.format(ex)) def _set_ventilation(self, level): success = True for setting in self._outputs[level]: output_id = int(setting['output_id']) value = setting['value'] on = value > 0 if on is False: value = None result = json.loads(self.webinterface.set_output(id=output_id, is_on=str(on), dimmer=value, timer=None)) if result['success'] is False: self.logger('Error setting output {0} to {1}: {2}'.format(output_id, 'OFF' if value is None else value, result['msg'])) success = False if success is True: self.logger('Ventilation set to {0}'.format(level)) else: self.logger('Could not set ventilation to {0}'.format(level)) success = False return success def _enqueue_metrics(self, tags, values): try: now = time.time() self._metrics_queue.appendleft({'type': 'ventilation', 'timestamp': now, 'tags': tags, 'values': values}) except Exception as ex: self.logger('Got unexpected error while enqueing metrics: {0}'.format(ex)) @om_metric_data(interval=5) def collect_metrics(self): # Yield all metrics in the Queue try: while True: yield self._metrics_queue.pop() except IndexError: pass @staticmethod def _abs_humidity(temperature, humidity): """ Calculate the absolute humidity (kg/m3) based on temperature and relative humidity. Formula was taken from http://www.aprweather.com/pages/calc.htm and should be good-enough for this purpose """ dew_point = Ventilation._dew_point(temperature, humidity) return ((6.11 * 10.0 ** (7.5 * dew_point / (237.7 + dew_point))) * 100) / ((temperature + 273.16) * 461.5) @staticmethod def _dew_point(temperature, humidity): """ Calculates the dew point for a given temperature and humidity """ a = 17.27 b = 237.7 def gamma(_temperature, _humidity): return ((a * _temperature) / (b + _temperature)) + math.log(_humidity / 100.0) return (b * gamma(temperature, humidity)) / (a - gamma(temperature, humidity)) @staticmethod def _mean(entries): """ Calculates mean """ if len(entries) > 0: return sum(entries) * 1.0 / len(entries) return 0 @staticmethod def _stddev(entries): """ Calculates standard deviation """ mean = Ventilation._mean(entries) variance = map(lambda e: (e - mean) ** 2, entries) return sqrt(Ventilation._mean(variance)) @om_expose def get_debug(self): return json.dumps({'runtime_data': self._runtime_data, 'ventilation': self._last_ventilation}, indent=4) @om_expose def get_config_description(self): return json.dumps(Ventilation.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Pushsafer(OMPluginBase): """ A Pushsafer (http://www.pushsafer.com) plugin for pushing events through Pushsafer """ name = 'Pushsafer' version = '2.1.0' interfaces = [('config', '1.0')] config_description = [{'name': 'privatekey', 'type': 'str', 'description': 'Your Private or Alias key.'}, {'name': 'input_mapping', 'type': 'section', 'description': 'The mapping between input_id and a given Pushsafer settings', 'repeat': True, 'min': 1, 'content': [{'name': 'input_id', 'type': 'int', 'description': 'The ID of the (virtual) input that will trigger the event.'}, {'name': 'message', 'type': 'str', 'description': 'The message to be send.'}, {'name': 'title', 'type': 'str', 'description': 'The title of message to be send.'}, {'name': 'device', 'type': 'str', 'description': 'The device or device group id where the message to be send.'}, {'name': 'icon', 'type': 'str', 'description': 'The icon which is displayed with the message (a number 1-98).'}, {'name': 'sound', 'type': 'int', 'description': 'The notification sound of message (a number 0-28 or empty).'}, {'name': 'vibration', 'type': 'str', 'description': 'How often the device should vibrate (a number 1-3 or empty).'}, {'name': 'url', 'type': 'str', 'description': 'A URL or URL scheme: https://www.pushsafer.com/en/url_schemes'}, {'name': 'urltitle', 'type': 'str', 'description': 'the URLs title'}, {'name': 'time2live', 'type': 'str', 'description': 'Integer number 0-43200: Time in minutes after which message automatically gets purged.'}]}] default_config = {'privatekey': '', 'input_id': -1, 'message': '', 'title': 'OpenMotics', 'device': '', 'icon': '1', 'sound': '', 'vibration': '', 'url': '', 'urltitle': '', 'time2live': ''} def __init__(self, webinterface, logger): super(Pushsafer, self).__init__(webinterface, logger) self.logger('Starting Pushsafer plugin...') self._config = self.read_config(Pushsafer.default_config) self._config_checker = PluginConfigChecker(Pushsafer.config_description) self._cooldown = {} self._read_config() self.logger("Started Pushsafer plugin") def _read_config(self): self._privatekey = self._config['privatekey'] self._mapping = self._config.get('input_mapping', []) self._endpoint = 'https://www.pushsafer.com/api' self._headers = {'Content-type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'OpenMotics plugin: Pushsafer'} self._enabled = self._privatekey != '' and len(self._mapping) > 0 self.logger('Pushsafer is {0}'.format('enabled' if self._enabled else 'disabled')) def convert(self, data): if isinstance(data, basestring): return str(data) elif isinstance(data, collections.Mapping): return dict(map(self.convert, data.iteritems())) elif isinstance(data, collections.Iterable): return type(data)(map(self.convert, data)) else: return data @input_status def input_status(self, status): now = time.time() if self._enabled is True: input_id = status[0] if self._cooldown.get(input_id, 0) > now - 10: self.logger('Ignored duplicate Input in 10 seconds.') return data_send = False for mapping in self._mapping: if input_id == mapping['input_id']: data = {'k': self._privatekey, 'm': mapping['message'], 't': mapping['title'], 'd': mapping['device'], 'i': mapping['icon'], 's': mapping['sound'], 'v': mapping['vibration'], 'u': mapping['url'], 'ut': mapping['urltitle'], 'l': mapping['time2live']} thread = Thread(target=self._send_data, args=(data,)) thread.start() data_send = True if data_send is True: self._cooldown[input_id] = now def _send_data(self, data): try: self.logger('Sending data') response = requests.post(url=self._endpoint, data=data, headers=self._headers, verify=False) if response.status_code != 200: self.logger('Got error response: {0} ({1})'.format(response.text, response.status_code)) else: result = json.loads(response.text) if result['status'] != 1: self.logger('Got error response: {0}'.format(result['error'])) else: self.logger('Got reply: {0}'.format(result['success'])) quotas = [] for data in result['available'].values(): device = data.keys()[0] quotas.append('{0}: {1}'.format(device, data[device])) self.logger('Remaining quotas: {0}'.format(', '.join(quotas))) except Exception as ex: self.logger('Error sending: {0}'.format(ex)) @om_expose def get_config_description(self): return json.dumps(Pushsafer.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) config = self.convert(config) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class InfluxDB(OMPluginBase): """ An InfluxDB plugin, for sending statistics to InfluxDB """ name = 'InfluxDB' version = '1.2.2' interfaces = [('config', '1.0')] config_description = [{'name': 'url', 'type': 'str', 'description': 'The enpoint for the InfluxDB using HTTP. E.g. http://1.2.3.4:8086'}, {'name': 'username', 'type': 'str', 'description': 'Optional username for InfluxDB authentication.'}, {'name': 'password', 'type': 'str', 'description': 'Optional password for InfluxDB authentication.'}, {'name': 'database', 'type': 'str', 'description': 'The InfluxDB database name to witch statistics need to be send.'}, {'name': 'intervals', 'type': 'section', 'description': 'Optional interval overrides.', 'repeat': True, 'min': 0, 'content': [{'name': 'component', 'type': 'str'}, {'name': 'interval', 'type': 'int'}]}] default_config = {'url': '', 'database': 'openmotics'} def __init__(self, webinterface, logger): super(InfluxDB, self).__init__(webinterface, logger) self.logger('Starting InfluxDB plugin...') self._start = time.time() self._last_service_uptime = 0 self._config = self.read_config(InfluxDB.default_config) self._config_checker = PluginConfigChecker(InfluxDB.config_description) self._environment = {'inputs': {}, 'outputs': {}, 'sensors': {}, 'pulse_counters': {}} self._timings = {} self._load_environment_configurations() self._read_config() self._has_fibaro_power = False if self._enabled: thread = Thread(target=self._check_fibaro_power) thread.start() self.logger("Started InfluxDB plugin") def _read_config(self): self._url = self._config['url'] self._database = self._config['database'] intervals = self._config.get('intervals', []) self._intervals = {} for item in intervals: self._intervals[item['component']] = item['interval'] username = self._config.get('username', '') password = self._config.get('password', '') self._auth = None if username == '' else (username, password) self._endpoint = '{0}/write?db={1}'.format(self._url, self._database) self._query_endpoint = '{0}/query?db={1}&epoch=ns'.format(self._url, self._database) self._headers = {'X-Requested-With': 'OpenMotics plugin: InfluxDB'} self._enabled = self._url != '' and self._database != '' self.logger('InfluxDB is {0}'.format('enabled' if self._enabled else 'disabled')) def _load_environment_configurations(self, interval=None): while True: start = time.time() # Inputs try: result = json.loads(self.webinterface.get_input_configurations(None)) if result['success'] is False: self.logger('Failed to load input configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name(config['name']) self._environment['inputs'][input_id] = config for input_id in self._environment['inputs'].keys(): if input_id not in ids: del self._environment['inputs'][input_id] except CommunicationTimedOutException: self.logger('Error while loading input configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading input configurations: {0}'.format(ex)) # Outputs try: result = json.loads(self.webinterface.get_output_configurations(None)) if result['success'] is False: self.logger('Failed to load output configurations') else: ids = [] for config in result['config']: if config['module_type'] not in ['o', 'O', 'd', 'D']: continue output_id = config['id'] ids.append(output_id) self._environment['outputs'][output_id] = {'name': InfluxDB.clean_name(config['name']), 'module_type': {'o': 'output', 'O': 'output', 'd': 'dimmer', 'D': 'dimmer'}[config['module_type']], 'floor': config['floor'], 'type': 'relay' if config['type'] == 0 else 'light'} for output_id in self._environment['outputs'].keys(): if output_id not in ids: del self._environment['outputs'][output_id] except CommunicationTimedOutException: self.logger('Error while loading output configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading output configurations: {0}'.format(ex)) # Sensors try: result = json.loads(self.webinterface.get_sensor_configurations(None)) if result['success'] is False: self.logger('Failed to load sensor configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name(config['name']) self._environment['sensors'][input_id] = config for input_id in self._environment['sensors'].keys(): if input_id not in ids: del self._environment['sensors'][input_id] except CommunicationTimedOutException: self.logger('Error while loading sensor configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading sensor configurations: {0}'.format(ex)) # Pulse counters try: result = json.loads(self.webinterface.get_pulse_counter_configurations(None)) if result['success'] is False: self.logger('Failed to load pulse counter configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name(config['name']) self._environment['pulse_counters'][input_id] = config for input_id in self._environment['pulse_counters'].keys(): if input_id not in ids: del self._environment['pulse_counters'][input_id] except CommunicationTimedOutException: self.logger('Error while loading pulse counter configurations: CommunicationTimedOutException') except Exception as ex: self.logger('Error while loading pulse counter configurations: {0}'.format(ex)) if interval is None: return else: self._pause(start, interval, 'environment_configurations') def _check_fibaro_power(self): time.sleep(10) try: self._has_fibaro_power = self._get_fibaro_power() is not None except: time.sleep(50) self._has_fibaro_power = self._get_fibaro_power() is not None self.logger('Fibaro plugin {0}detected'.format('' if self._has_fibaro_power else 'not ')) @staticmethod def clean_name(name): return name.replace(' ', '\ ') @input_status def process_input_status(self, status): if self._enabled is True: input_id = status[0] thread = Thread(target=self._process_input, args=(input_id,)) thread.start() def _process_input(self, input_id): try: inputs = self._environment['inputs'] if input_id not in inputs: return input_name = inputs[input_id]['clean_name'] if input_name != '': data = {'type': 'input', 'id': input_id, 'name': input_name} self._send(self._build_command('event', data, 'true')) self.logger('Processed input {0} ({1})'.format(input_id, inputs[input_id]['name'])) else: self.logger('Not sending input {0}: Name is empty'.format(input_id)) except Exception as ex: self.logger('Error processing input: {0}'.format(ex)) @output_status def process_output_status(self, status): if self._enabled is True: try: on_outputs = {} for entry in status: on_outputs[entry[0]] = entry[1] outputs = self._environment['outputs'] for output_id in outputs: status = outputs[output_id].get('status') dimmer = outputs[output_id].get('dimmer') if status is None or dimmer is None: continue changed = False if output_id in on_outputs: if status != 1: changed = True outputs[output_id]['status'] = 1 self.logger('Output {0} changed to ON'.format(output_id)) if dimmer != on_outputs[output_id]: changed = True outputs[output_id]['dimmer'] = on_outputs[output_id] self.logger('Output {0} changed to level {1}'.format(output_id, on_outputs[output_id])) elif status != 0: changed = True outputs[output_id]['status'] = 0 self.logger('Output {0} changed to OFF'.format(output_id)) if changed is True: thread = Thread(target=self._process_outputs, args=([output_id],)) thread.start() except Exception as ex: self.logger('Error processing outputs: {0}'.format(ex)) def _process_outputs(self, output_ids): try: influx_data = [] outputs = self._environment['outputs'] for output_id in output_ids: output_name = outputs[output_id].get('name') status = outputs[output_id].get('status') dimmer = outputs[output_id].get('dimmer') if output_name != '' and status is not None and dimmer is not None: if outputs[output_id]['module_type'] == 'output': level = 100 else: level = dimmer if status == 0: level = 0 data = {'id': output_id, 'name': output_name} for key in ['module_type', 'type', 'floor']: if key in outputs[output_id]: data[key] = outputs[output_id][key] influx_data.append(self._build_command('output', data, '{0}i'.format(level))) self._send(influx_data) except Exception as ex: self.logger('Error processing outputs {0}: {1}'.format(output_ids, ex)) @background_task def run(self): threads = [InfluxDB._start_thread(self._run_system, self._intervals.get('system', 60)), InfluxDB._start_thread(self._run_outputs, self._intervals.get('outputs', 60)), InfluxDB._start_thread(self._run_sensors, self._intervals.get('sensors', 60)), InfluxDB._start_thread(self._run_thermostats, self._intervals.get('thermostats', 60)), InfluxDB._start_thread(self._run_errors, self._intervals.get('errors', 120)), InfluxDB._start_thread(self._run_pulsecounters, self._intervals.get('pulsecounters', 30)), InfluxDB._start_thread(self._run_power_openmotics, self._intervals.get('power_openmotics', 10)), InfluxDB._start_thread(self._run_power_openmotics_analytics, self._intervals.get('power_openmotics_analytics', 60)), InfluxDB._start_thread(self._run_power_fibaro, self._intervals.get('power_fibaro', 15)), InfluxDB._start_thread(self._load_environment_configurations, 900)] for thread in threads: thread.join() @staticmethod def _start_thread(workload, interval): thread = Thread(target=workload, args=(interval,)) thread.start() return thread def _pause(self, start, interval, name): elapsed = time.time() - start if name not in self._timings: self._timings[name] = [] self._timings[name].append(elapsed) if len(self._timings[name]) == 100: min_elapsed = round(min(self._timings[name]), 2) max_elapsed = round(max(self._timings[name]), 2) avg_elapsed = round(sum(self._timings[name]) / 100.0, 2) self.logger('Duration stats of {0}: min {1}s, avg {2}s, max {3}s'.format(name, min_elapsed, avg_elapsed, max_elapsed)) self._timings[name] = [] if elapsed > interval: self.logger('Duration of {0} ({1}s) longer than interval ({2}s)'.format(name, round(elapsed, 2), interval)) sleep = max(0.1, interval - elapsed) time.sleep(sleep) def _run_system(self, interval): while True: start = time.time() try: with open('/proc/uptime', 'r') as f: system_uptime = float(f.readline().split()[0]) service_uptime = time.time() - self._start if service_uptime > self._last_service_uptime + 3600: self._start = time.time() service_uptime = 0 self._last_service_uptime = service_uptime self._send(self._build_command('system', {'name': 'gateway'}, {'service_uptime': service_uptime, 'system_uptime': system_uptime})) except Exception as ex: self.logger('Error sending system data: {0}'.format(ex)) self._pause(start, interval, 'system') def _run_outputs(self, interval): while True: start = time.time() try: result = json.loads(self.webinterface.get_output_status(None)) if result['success'] is False: self.logger('Failed to get output status') else: for output in result['status']: output_id = output['id'] if output_id not in self._environment['outputs']: continue self._environment['outputs'][output_id]['status'] = output['status'] self._environment['outputs'][output_id]['dimmer'] = output['dimmer'] except CommunicationTimedOutException: self.logger('Error getting output status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting output status: {0}'.format(ex)) self._process_outputs(self._environment['outputs'].keys()) self._pause(start, interval, 'outputs') def _run_sensors(self, interval): while True: start = time.time() try: temperatures = json.loads(self.webinterface.get_sensor_temperature_status(None)) humidities = json.loads(self.webinterface.get_sensor_humidity_status(None)) brightnesses = json.loads(self.webinterface.get_sensor_brightness_status(None)) influx_data = [] for sensor_id, sensor in self._environment['sensors'].iteritems(): name = sensor['clean_name'] if name == '' or name == 'NOT_IN_USE': continue data = {'id': sensor_id, 'name': name} values = {} if temperatures['success'] is True and temperatures['status'][sensor_id] is not None: values['temp'] = temperatures['status'][sensor_id] if humidities['success'] is True and humidities['status'][sensor_id] is not None: values['hum'] = humidities['status'][sensor_id] if brightnesses['success'] is True and brightnesses['status'][sensor_id] is not None: values['bright'] = brightnesses['status'][sensor_id] influx_data.append(self._build_command('sensor', data, values)) if len(influx_data) > 0: self._send(influx_data) except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting sensor status: {0}'.format(ex)) self._pause(start, interval, 'sensors') def _run_thermostats(self, interval): while True: start = time.time() try: thermostats = json.loads(self.webinterface.get_thermostat_status(None)) if thermostats['success'] is False: self.logger('Failed to get thermostat status') else: influx_data = [self._build_command('thermostat', {'id': 'G.0', 'name': 'Global\ configuration'}, {'on': str(thermostats['thermostats_on']), 'cooling': str(thermostats['cooling'])})] for thermostat in thermostats['status']: values = {'setpoint': '{0}i'.format(thermostat['setpoint']), 'output0': thermostat['output0'], 'output1': thermostat['output1'], 'outside': thermostat['outside'], 'mode': '{0}i'.format(thermostat['mode']), 'type': '"tbs"' if thermostat['sensor_nr'] == 240 else '"normal"', 'automatic': str(thermostat['automatic']), 'current_setpoint': thermostat['csetp']} if thermostat['sensor_nr'] != 240: values['temperature'] = thermostat['act'] influx_data.append(self._build_command('thermostat', {'id': '{0}.{1}'.format('C' if thermostats['cooling'] is True else 'H', thermostat['id']), 'name': InfluxDB.clean_name(thermostat['name'])}, values)) self._send(influx_data) except CommunicationTimedOutException: self.logger('Error getting thermostat status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting thermostat status: {0}'.format(ex)) self._pause(start, interval, 'thermostats') def _run_errors(self, interval): while True: start = time.time() try: errors = json.loads(self.webinterface.get_errors(None)) if errors['success'] is False: self.logger('Failed to get module errors') else: influx_data = [] for error in errors['errors']: module = error[0] count = error[1] types = {'i': 'Input', 'I': 'Input', 'T': 'Temperature', 'o': 'Output', 'O': 'Output', 'd': 'Dimmer', 'D': 'Dimmer', 'R': 'Shutter', 'C': 'CAN', 'L': 'OLED'} data = {'type': types[module[0]], 'id': module, 'name': '{0}\ {1}'.format(types[module[0]], module)} influx_data.append(self._build_command('error', data, '{0}i'.format(count))) self._send(influx_data) except CommunicationTimedOutException: self.logger('Error getting module errors: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting module errors: {0}'.format(ex)) self._pause(start, interval, 'errors') def _run_pulsecounters(self, interval): while True: start = time.time() counters_data = {} try: for counter_id, counter in self._environment['pulse_counters'].iteritems(): counters_data[counter_id] = {'name': counter['clean_name'], 'input': counter['input']} except Exception as ex: self.logger('Error getting pulse counter configuration: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_pulse_counter_status(None)) if result['success'] is False: self.logger('Failed to get pulse counter status') else: counters = result['counters'] for counter_id in counters_data: if len(counters) > counter_id: counters_data[counter_id]['count'] = counters[counter_id] except CommunicationTimedOutException: self.logger('Error getting pulse counter status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting pulse counter status: {0}'.format(ex)) influx_data = [] for counter_id in counters_data: counter = counters_data[counter_id] if counter['name'] != '': data = {'name': counter['name'], 'input': counter['input']} influx_data.append(self._build_command('counter', data, counter['count'])) self._send(influx_data) self._pause(start, interval, 'pulse counters') def _run_power_openmotics(self, interval): while True: start = time.time() mapping = {} power_data = {} try: result = json.loads(self.webinterface.get_power_modules(None)) if result['success'] is False: self.logger('Failed to get power modules') else: for module in result['modules']: device_id = '{0}.{{0}}'.format(module['address']) mapping[str(module['id'])] = device_id if module['version'] in [8, 12]: for i in xrange(module['version']): power_data[device_id.format(i)] = {'name': InfluxDB.clean_name(module['input{0}'.format(i)])} else: self.logger('Unknown power module version: {0}'.format(module['version'])) except CommunicationTimedOutException: self.logger('Error getting power modules: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting power modules: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_realtime_power(None)) if result['success'] is False: self.logger('Failed to get realtime power') else: for module_id, device_id in mapping.iteritems(): if module_id in result: for index, entry in enumerate(result[module_id]): if device_id.format(index) in power_data: usage = power_data[device_id.format(index)] usage.update({'voltage': entry[0], 'frequency': entry[1], 'current': entry[2], 'power': entry[3]}) except CommunicationTimedOutException: self.logger('Error getting realtime power: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting realtime power: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_total_energy(None)) if result['success'] is False: self.logger('Failed to get total energy') else: for module_id, device_id in mapping.iteritems(): if module_id in result: for index, entry in enumerate(result[module_id]): if device_id.format(index) in power_data: usage = power_data[device_id.format(index)] usage.update({'counter': entry[0] + entry[1], 'counter_day': entry[0], 'counter_night': entry[1]}) except CommunicationTimedOutException: self.logger('Error getting total energy: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting total energy: {0}'.format(ex)) influx_data = [] for device_id in power_data: device = power_data[device_id] if device['name'] != '': try: data = {'type': 'openmotics', 'id': device_id, 'name': device['name']} values = {'voltage': device['voltage'], 'current': device['current'], 'frequency': device['frequency'], 'power': device['power'], 'counter': device['counter'], 'counter_day': device['counter_day'], 'counter_night': device['counter_night']} influx_data.append(self._build_command('energy', data, values)) except Exception as ex: self.logger('Error processing OpenMotics power device {0}: {1}'.format(device_id, ex)) self._send(influx_data) self._pause(start, interval, 'power (OpenMotics)') def _run_power_openmotics_analytics(self, interval): while True: start = time.time() try: result = json.loads(self.webinterface.get_power_modules(None)) if result['success'] is False: self.logger('Failed to get power modules') else: for module in result['modules']: device_id = '{0}.{{0}}'.format(module['address']) if module['version'] != 12: if module['version'] != 8: self.logger('Unknown power module version: {0}'.format(module['version'])) continue result = json.loads(self.webinterface.get_energy_time(None, module['id'])) if result['success'] is False: self.logger('Failed to get time data') continue base_timestamp = None abort = False for i in xrange(12): if abort is True: break name = InfluxDB.clean_name(module['input{0}'.format(i)]) if name == '': continue timestamp = base_timestamp length = min(len(result[str(i)]['current']), len(result[str(i)]['voltage'])) influx_data = [] for j in xrange(length): data = self._build_command('energy_analytics', {'type': 'time', 'id': device_id.format(i), 'name': name}, {'current': result[str(i)]['current'][j], 'voltage': result[str(i)]['voltage'][j]}, timestamp=timestamp) if base_timestamp is not None: influx_data.append(data) else: self._send(data) query = 'SELECT current FROM energy_analytics ORDER BY time DESC LIMIT 1' response = requests.get(url=self._query_endpoint, params={'q': query}, headers=self._headers, auth=self._auth, verify=False) if response.status_code != 200: self.logger('Query time failed, received: {0} ({1})'.format(response.text, response.status_code)) abort = True break base_timestamp = response.json()['results'][0]['series'][0]['values'][0][0] timestamp = base_timestamp timestamp += 250000000 # Stretch actual data by 1000 for visualtisation purposes self._send(influx_data) result = json.loads(self.webinterface.get_energy_frequency(None, module['id'])) if result['success'] is False: self.logger('Failed to get frequency data') continue base_timestamp = None abort = False for i in xrange(12): if abort is True: break name = InfluxDB.clean_name(module['input{0}'.format(i)]) if name == '': continue timestamp = base_timestamp length = min(len(result[str(i)]['current'][0]), len(result[str(i)]['voltage'][0])) influx_data = [] for j in xrange(length): data = self._build_command('energy_analytics', {'type': 'frequency', 'id': device_id.format(i), 'name': name}, {'current_harmonics': result[str(i)]['current'][0][j], 'current_phase': result[str(i)]['current'][1][j], 'voltage_harmonics': result[str(i)]['voltage'][0][j], 'voltage_phase': result[str(i)]['voltage'][1][j]}, timestamp=timestamp) if base_timestamp is not None: influx_data.append(data) else: self._send(data) query = 'SELECT current_harmonics FROM energy_analytics ORDER BY time DESC LIMIT 1' response = requests.get(url=self._query_endpoint, params={'q': query}, headers=self._headers, auth=self._auth, verify=False) if response.status_code != 200: self.logger('Query frequency failed, received: {0} ({1})'.format(response.text, response.status_code)) abort = True break base_timestamp = response.json()['results'][0]['series'][0]['values'][0][0] timestamp = base_timestamp timestamp += 250000000 # Stretch actual data by 1000 for visualtisation purposes self._send(influx_data) except CommunicationTimedOutException: self.logger('Error getting power analytics: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting power analytics: {0}'.format(ex)) self._pause(start, interval, 'power analysis') def _run_power_fibaro(self, interval): while True: start = time.time() if self._has_fibaro_power is True: usage = self._get_fibaro_power() if usage is not None: influx_data = [] for device_id in usage: try: device = usage[device_id] name = InfluxDB.clean_name(device['name']) if name == '': return data = {'type': 'fibaro', 'id': device_id, 'name': name} values = {'power': device['power'], 'counter': device['counter']} influx_data.append(self._build_command('energy', data, values)) except Exception as ex: self.logger('Error processing Fibaro power device {0}: {1}'.format(device_id, ex)) self._send(influx_data) self._pause(start, interval, 'power (Fibaro)') @staticmethod def _build_command(key, tags, value, timestamp=None): if isinstance(value, dict): values = ','.join('{0}={1}'.format(vname, vvalue) for vname, vvalue in value.iteritems()) else: values = 'value={0}'.format(value) return '{0},{1} {2}{3}'.format(key, ','.join('{0}={1}'.format(tname, tvalue) for tname, tvalue in tags.iteritems()), values, '' if timestamp is None else ' {0}'.format(timestamp)) def _send(self, data): try: if not isinstance(data, list) and not isinstance(data, basestring): raise RuntimeError('Invalid data passed in _send ({0})'.format(type(data))) if isinstance(data, basestring): data = [data] if len(data) == 0: return True, '' response = requests.post(url=self._endpoint, data='\n'.join(data), headers=self._headers, auth=self._auth, verify=False) if response.status_code != 204: self.logger('Send failed, received: {0} ({1})'.format(response.text, response.status_code)) return False, 'Send failed, received: {0} ({1})'.format(response.text, response.status_code) return True, '' except Exception as ex: self.logger('Error sending: {0}'.format(ex)) return False, 'Error sending: {0}'.format(ex) def _get_fibaro_power(self): try: response = requests.get(url='https://127.0.0.1/plugins/Fibaro/get_power_usage', params={'token': 'None'}, verify=False) if response.status_code == 200: result = response.json() if result['success'] is True: return result['result'] else: self.logger('Error loading Fibaro data: {0}'.format(result['msg'])) else: self.logger('Error loading Fibaro data: {0}'.format(response.status_code)) return None except Exception as ex: self.logger('Got unexpected error during Fibaro power load: {0}'.format(ex)) return None @om_expose def send_data(self, key, tags, value): if self._enabled is True: tags = json.loads(tags) value = json.loads(value) success, result = self._send(self._build_command(key, tags, value)) return json.dumps({'success': success, 'result' if success else 'error': result}) else: return json.dumps({'success': False, 'error': 'InfluxDB plugin not enabled'}) @om_expose def get_config_description(self): return json.dumps(InfluxDB.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() if self._enabled: cthread = Thread(target=self._load_environment_configurations) cthread.start() fthread = Thread(target=self._check_fibaro_power) fthread.start() self.write_config(config) return json.dumps({'success': True})
class InfluxDB(OMPluginBase): """ An InfluxDB plugin, for sending statistics to InfluxDB """ name = 'InfluxDB' version = '1.2.2' interfaces = [('config', '1.0')] config_description = [{ 'name': 'url', 'type': 'str', 'description': 'The enpoint for the InfluxDB using HTTP. E.g. http://1.2.3.4:8086' }, { 'name': 'username', 'type': 'str', 'description': 'Optional username for InfluxDB authentication.' }, { 'name': 'password', 'type': 'str', 'description': 'Optional password for InfluxDB authentication.' }, { 'name': 'database', 'type': 'str', 'description': 'The InfluxDB database name to witch statistics need to be send.' }, { 'name': 'intervals', 'type': 'section', 'description': 'Optional interval overrides.', 'repeat': True, 'min': 0, 'content': [{ 'name': 'component', 'type': 'str' }, { 'name': 'interval', 'type': 'int' }] }] default_config = {'url': '', 'database': 'openmotics'} def __init__(self, webinterface, logger): super(InfluxDB, self).__init__(webinterface, logger) self.logger('Starting InfluxDB plugin...') self._start = time.time() self._last_service_uptime = 0 self._config = self.read_config(InfluxDB.default_config) self._config_checker = PluginConfigChecker(InfluxDB.config_description) self._environment = { 'inputs': {}, 'outputs': {}, 'sensors': {}, 'pulse_counters': {} } self._timings = {} self._load_environment_configurations() self._read_config() self._has_fibaro_power = False if self._enabled: thread = Thread(target=self._check_fibaro_power) thread.start() self.logger("Started InfluxDB plugin") def _read_config(self): self._url = self._config['url'] self._database = self._config['database'] intervals = self._config.get('intervals', []) self._intervals = {} for item in intervals: self._intervals[item['component']] = item['interval'] username = self._config.get('username', '') password = self._config.get('password', '') self._auth = None if username == '' else (username, password) self._endpoint = '{0}/write?db={1}'.format(self._url, self._database) self._query_endpoint = '{0}/query?db={1}&epoch=ns'.format( self._url, self._database) self._headers = {'X-Requested-With': 'OpenMotics plugin: InfluxDB'} self._enabled = self._url != '' and self._database != '' self.logger('InfluxDB is {0}'.format( 'enabled' if self._enabled else 'disabled')) def _load_environment_configurations(self, interval=None): while True: start = time.time() # Inputs try: result = json.loads( self.webinterface.get_input_configurations(None)) if result['success'] is False: self.logger('Failed to load input configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name( config['name']) self._environment['inputs'][input_id] = config for input_id in self._environment['inputs'].keys(): if input_id not in ids: del self._environment['inputs'][input_id] except CommunicationTimedOutException: self.logger( 'Error while loading input configurations: CommunicationTimedOutException' ) except Exception as ex: self.logger( 'Error while loading input configurations: {0}'.format(ex)) # Outputs try: result = json.loads( self.webinterface.get_output_configurations(None)) if result['success'] is False: self.logger('Failed to load output configurations') else: ids = [] for config in result['config']: if config['module_type'] not in ['o', 'O', 'd', 'D']: continue output_id = config['id'] ids.append(output_id) self._environment['outputs'][output_id] = { 'name': InfluxDB.clean_name(config['name']), 'module_type': { 'o': 'output', 'O': 'output', 'd': 'dimmer', 'D': 'dimmer' }[config['module_type']], 'floor': config['floor'], 'type': 'relay' if config['type'] == 0 else 'light' } for output_id in self._environment['outputs'].keys(): if output_id not in ids: del self._environment['outputs'][output_id] except CommunicationTimedOutException: self.logger( 'Error while loading output configurations: CommunicationTimedOutException' ) except Exception as ex: self.logger( 'Error while loading output configurations: {0}'.format( ex)) # Sensors try: result = json.loads( self.webinterface.get_sensor_configurations(None)) if result['success'] is False: self.logger('Failed to load sensor configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name( config['name']) self._environment['sensors'][input_id] = config for input_id in self._environment['sensors'].keys(): if input_id not in ids: del self._environment['sensors'][input_id] except CommunicationTimedOutException: self.logger( 'Error while loading sensor configurations: CommunicationTimedOutException' ) except Exception as ex: self.logger( 'Error while loading sensor configurations: {0}'.format( ex)) # Pulse counters try: result = json.loads( self.webinterface.get_pulse_counter_configurations(None)) if result['success'] is False: self.logger('Failed to load pulse counter configurations') else: ids = [] for config in result['config']: input_id = config['id'] ids.append(input_id) config['clean_name'] = InfluxDB.clean_name( config['name']) self._environment['pulse_counters'][input_id] = config for input_id in self._environment['pulse_counters'].keys(): if input_id not in ids: del self._environment['pulse_counters'][input_id] except CommunicationTimedOutException: self.logger( 'Error while loading pulse counter configurations: CommunicationTimedOutException' ) except Exception as ex: self.logger( 'Error while loading pulse counter configurations: {0}'. format(ex)) if interval is None: return else: self._pause(start, interval, 'environment_configurations') def _check_fibaro_power(self): time.sleep(10) try: self._has_fibaro_power = self._get_fibaro_power() is not None except: time.sleep(50) self._has_fibaro_power = self._get_fibaro_power() is not None self.logger('Fibaro plugin {0}detected'.format( '' if self._has_fibaro_power else 'not ')) @staticmethod def clean_name(name): return name.replace(' ', '\ ') @input_status def process_input_status(self, status): if self._enabled is True: input_id = status[0] thread = Thread(target=self._process_input, args=(input_id, )) thread.start() def _process_input(self, input_id): try: inputs = self._environment['inputs'] if input_id not in inputs: return input_name = inputs[input_id]['clean_name'] if input_name != '': data = {'type': 'input', 'id': input_id, 'name': input_name} self._send(self._build_command('event', data, 'true')) self.logger('Processed input {0} ({1})'.format( input_id, inputs[input_id]['name'])) else: self.logger( 'Not sending input {0}: Name is empty'.format(input_id)) except Exception as ex: self.logger('Error processing input: {0}'.format(ex)) @output_status def process_output_status(self, status): if self._enabled is True: try: on_outputs = {} for entry in status: on_outputs[entry[0]] = entry[1] outputs = self._environment['outputs'] for output_id in outputs: status = outputs[output_id].get('status') dimmer = outputs[output_id].get('dimmer') if status is None or dimmer is None: continue changed = False if output_id in on_outputs: if status != 1: changed = True outputs[output_id]['status'] = 1 self.logger( 'Output {0} changed to ON'.format(output_id)) if dimmer != on_outputs[output_id]: changed = True outputs[output_id]['dimmer'] = on_outputs[ output_id] self.logger( 'Output {0} changed to level {1}'.format( output_id, on_outputs[output_id])) elif status != 0: changed = True outputs[output_id]['status'] = 0 self.logger( 'Output {0} changed to OFF'.format(output_id)) if changed is True: thread = Thread(target=self._process_outputs, args=([output_id], )) thread.start() except Exception as ex: self.logger('Error processing outputs: {0}'.format(ex)) def _process_outputs(self, output_ids): try: influx_data = [] outputs = self._environment['outputs'] for output_id in output_ids: output_name = outputs[output_id].get('name') status = outputs[output_id].get('status') dimmer = outputs[output_id].get('dimmer') if output_name != '' and status is not None and dimmer is not None: if outputs[output_id]['module_type'] == 'output': level = 100 else: level = dimmer if status == 0: level = 0 data = {'id': output_id, 'name': output_name} for key in ['module_type', 'type', 'floor']: if key in outputs[output_id]: data[key] = outputs[output_id][key] influx_data.append( self._build_command('output', data, '{0}i'.format(level))) self._send(influx_data) except Exception as ex: self.logger('Error processing outputs {0}: {1}'.format( output_ids, ex)) @background_task def run(self): threads = [ InfluxDB._start_thread(self._run_system, self._intervals.get('system', 60)), InfluxDB._start_thread(self._run_outputs, self._intervals.get('outputs', 60)), InfluxDB._start_thread(self._run_sensors, self._intervals.get('sensors', 60)), InfluxDB._start_thread(self._run_thermostats, self._intervals.get('thermostats', 60)), InfluxDB._start_thread(self._run_errors, self._intervals.get('errors', 120)), InfluxDB._start_thread(self._run_pulsecounters, self._intervals.get('pulsecounters', 30)), InfluxDB._start_thread(self._run_power_openmotics, self._intervals.get('power_openmotics', 10)), InfluxDB._start_thread( self._run_power_openmotics_analytics, self._intervals.get('power_openmotics_analytics', 60)), InfluxDB._start_thread(self._run_power_fibaro, self._intervals.get('power_fibaro', 15)), InfluxDB._start_thread(self._load_environment_configurations, 900) ] for thread in threads: thread.join() @staticmethod def _start_thread(workload, interval): thread = Thread(target=workload, args=(interval, )) thread.start() return thread def _pause(self, start, interval, name): elapsed = time.time() - start if name not in self._timings: self._timings[name] = [] self._timings[name].append(elapsed) if len(self._timings[name]) == 100: min_elapsed = round(min(self._timings[name]), 2) max_elapsed = round(max(self._timings[name]), 2) avg_elapsed = round(sum(self._timings[name]) / 100.0, 2) self.logger( 'Duration stats of {0}: min {1}s, avg {2}s, max {3}s'.format( name, min_elapsed, avg_elapsed, max_elapsed)) self._timings[name] = [] if elapsed > interval: self.logger( 'Duration of {0} ({1}s) longer than interval ({2}s)'.format( name, round(elapsed, 2), interval)) sleep = max(0.1, interval - elapsed) time.sleep(sleep) def _run_system(self, interval): while True: start = time.time() try: with open('/proc/uptime', 'r') as f: system_uptime = float(f.readline().split()[0]) service_uptime = time.time() - self._start if service_uptime > self._last_service_uptime + 3600: self._start = time.time() service_uptime = 0 self._last_service_uptime = service_uptime self._send( self._build_command('system', {'name': 'gateway'}, { 'service_uptime': service_uptime, 'system_uptime': system_uptime })) except Exception as ex: self.logger('Error sending system data: {0}'.format(ex)) self._pause(start, interval, 'system') def _run_outputs(self, interval): while True: start = time.time() try: result = json.loads(self.webinterface.get_output_status(None)) if result['success'] is False: self.logger('Failed to get output status') else: for output in result['status']: output_id = output['id'] if output_id not in self._environment['outputs']: continue self._environment['outputs'][output_id][ 'status'] = output['status'] self._environment['outputs'][output_id][ 'dimmer'] = output['dimmer'] except CommunicationTimedOutException: self.logger( 'Error getting output status: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting output status: {0}'.format(ex)) self._process_outputs(self._environment['outputs'].keys()) self._pause(start, interval, 'outputs') def _run_sensors(self, interval): while True: start = time.time() try: temperatures = json.loads( self.webinterface.get_sensor_temperature_status(None)) humidities = json.loads( self.webinterface.get_sensor_humidity_status(None)) brightnesses = json.loads( self.webinterface.get_sensor_brightness_status(None)) influx_data = [] for sensor_id, sensor in self._environment[ 'sensors'].iteritems(): name = sensor['clean_name'] if name == '' or name == 'NOT_IN_USE': continue data = {'id': sensor_id, 'name': name} values = {} if temperatures['success'] is True and temperatures[ 'status'][sensor_id] is not None: values['temp'] = temperatures['status'][sensor_id] if humidities['success'] is True and humidities['status'][ sensor_id] is not None: values['hum'] = humidities['status'][sensor_id] if brightnesses['success'] is True and brightnesses[ 'status'][sensor_id] is not None: values['bright'] = brightnesses['status'][sensor_id] influx_data.append( self._build_command('sensor', data, values)) if len(influx_data) > 0: self._send(influx_data) except CommunicationTimedOutException: self.logger( 'Error getting sensor status: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting sensor status: {0}'.format(ex)) self._pause(start, interval, 'sensors') def _run_thermostats(self, interval): while True: start = time.time() try: thermostats = json.loads( self.webinterface.get_thermostat_status(None)) if thermostats['success'] is False: self.logger('Failed to get thermostat status') else: influx_data = [ self._build_command( 'thermostat', { 'id': 'G.0', 'name': 'Global\ configuration' }, { 'on': str(thermostats['thermostats_on']), 'cooling': str(thermostats['cooling']) }) ] for thermostat in thermostats['status']: values = { 'setpoint': '{0}i'.format(thermostat['setpoint']), 'output0': thermostat['output0'], 'output1': thermostat['output1'], 'outside': thermostat['outside'], 'mode': '{0}i'.format(thermostat['mode']), 'type': '"tbs"' if thermostat['sensor_nr'] == 240 else '"normal"', 'automatic': str(thermostat['automatic']), 'current_setpoint': thermostat['csetp'] } if thermostat['sensor_nr'] != 240: values['temperature'] = thermostat['act'] influx_data.append( self._build_command( 'thermostat', { 'id': '{0}.{1}'.format( 'C' if thermostats['cooling'] is True else 'H', thermostat['id']), 'name': InfluxDB.clean_name(thermostat['name']) }, values)) self._send(influx_data) except CommunicationTimedOutException: self.logger( 'Error getting thermostat status: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting thermostat status: {0}'.format(ex)) self._pause(start, interval, 'thermostats') def _run_errors(self, interval): while True: start = time.time() try: errors = json.loads(self.webinterface.get_errors(None)) if errors['success'] is False: self.logger('Failed to get module errors') else: influx_data = [] for error in errors['errors']: module = error[0] count = error[1] types = { 'i': 'Input', 'I': 'Input', 'T': 'Temperature', 'o': 'Output', 'O': 'Output', 'd': 'Dimmer', 'D': 'Dimmer', 'R': 'Shutter', 'C': 'CAN', 'L': 'OLED' } data = { 'type': types[module[0]], 'id': module, 'name': '{0}\ {1}'.format(types[module[0]], module) } influx_data.append( self._build_command('error', data, '{0}i'.format(count))) self._send(influx_data) except CommunicationTimedOutException: self.logger( 'Error getting module errors: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting module errors: {0}'.format(ex)) self._pause(start, interval, 'errors') def _run_pulsecounters(self, interval): while True: start = time.time() counters_data = {} try: for counter_id, counter in self._environment[ 'pulse_counters'].iteritems(): counters_data[counter_id] = { 'name': counter['clean_name'], 'input': counter['input'] } except Exception as ex: self.logger( 'Error getting pulse counter configuration: {0}'.format( ex)) try: result = json.loads( self.webinterface.get_pulse_counter_status(None)) if result['success'] is False: self.logger('Failed to get pulse counter status') else: counters = result['counters'] for counter_id in counters_data: if len(counters) > counter_id: counters_data[counter_id]['count'] = counters[ counter_id] except CommunicationTimedOutException: self.logger( 'Error getting pulse counter status: CommunicationTimedOutException' ) except Exception as ex: self.logger( 'Error getting pulse counter status: {0}'.format(ex)) influx_data = [] for counter_id in counters_data: counter = counters_data[counter_id] if counter['name'] != '': data = {'name': counter['name'], 'input': counter['input']} influx_data.append( self._build_command('counter', data, counter['count'])) self._send(influx_data) self._pause(start, interval, 'pulse counters') def _run_power_openmotics(self, interval): while True: start = time.time() mapping = {} power_data = {} try: result = json.loads(self.webinterface.get_power_modules(None)) if result['success'] is False: self.logger('Failed to get power modules') else: for module in result['modules']: device_id = '{0}.{{0}}'.format(module['address']) mapping[str(module['id'])] = device_id if module['version'] in [8, 12]: for i in xrange(module['version']): power_data[device_id.format(i)] = { 'name': InfluxDB.clean_name( module['input{0}'.format(i)]) } else: self.logger( 'Unknown power module version: {0}'.format( module['version'])) except CommunicationTimedOutException: self.logger( 'Error getting power modules: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting power modules: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_realtime_power(None)) if result['success'] is False: self.logger('Failed to get realtime power') else: for module_id, device_id in mapping.iteritems(): if module_id in result: for index, entry in enumerate(result[module_id]): if device_id.format(index) in power_data: usage = power_data[device_id.format(index)] usage.update({ 'voltage': entry[0], 'frequency': entry[1], 'current': entry[2], 'power': entry[3] }) except CommunicationTimedOutException: self.logger( 'Error getting realtime power: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting realtime power: {0}'.format(ex)) try: result = json.loads(self.webinterface.get_total_energy(None)) if result['success'] is False: self.logger('Failed to get total energy') else: for module_id, device_id in mapping.iteritems(): if module_id in result: for index, entry in enumerate(result[module_id]): if device_id.format(index) in power_data: usage = power_data[device_id.format(index)] usage.update({ 'counter': entry[0] + entry[1], 'counter_day': entry[0], 'counter_night': entry[1] }) except CommunicationTimedOutException: self.logger( 'Error getting total energy: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting total energy: {0}'.format(ex)) influx_data = [] for device_id in power_data: device = power_data[device_id] if device['name'] != '': try: data = { 'type': 'openmotics', 'id': device_id, 'name': device['name'] } values = { 'voltage': device['voltage'], 'current': device['current'], 'frequency': device['frequency'], 'power': device['power'], 'counter': device['counter'], 'counter_day': device['counter_day'], 'counter_night': device['counter_night'] } influx_data.append( self._build_command('energy', data, values)) except Exception as ex: self.logger( 'Error processing OpenMotics power device {0}: {1}' .format(device_id, ex)) self._send(influx_data) self._pause(start, interval, 'power (OpenMotics)') def _run_power_openmotics_analytics(self, interval): while True: start = time.time() try: result = json.loads(self.webinterface.get_power_modules(None)) if result['success'] is False: self.logger('Failed to get power modules') else: for module in result['modules']: device_id = '{0}.{{0}}'.format(module['address']) if module['version'] != 12: if module['version'] != 8: self.logger( 'Unknown power module version: {0}'.format( module['version'])) continue result = json.loads( self.webinterface.get_energy_time( None, module['id'])) if result['success'] is False: self.logger('Failed to get time data') continue base_timestamp = None abort = False for i in xrange(12): if abort is True: break name = InfluxDB.clean_name( module['input{0}'.format(i)]) if name == '': continue timestamp = base_timestamp length = min(len(result[str(i)]['current']), len(result[str(i)]['voltage'])) influx_data = [] for j in xrange(length): data = self._build_command( 'energy_analytics', { 'type': 'time', 'id': device_id.format(i), 'name': name }, { 'current': result[str(i)]['current'][j], 'voltage': result[str(i)]['voltage'][j] }, timestamp=timestamp) if base_timestamp is not None: influx_data.append(data) else: self._send(data) query = 'SELECT current FROM energy_analytics ORDER BY time DESC LIMIT 1' response = requests.get( url=self._query_endpoint, params={'q': query}, headers=self._headers, auth=self._auth, verify=False) if response.status_code != 200: self.logger( 'Query time failed, received: {0} ({1})' .format(response.text, response.status_code)) abort = True break base_timestamp = response.json( )['results'][0]['series'][0]['values'][0][ 0] timestamp = base_timestamp timestamp += 250000000 # Stretch actual data by 1000 for visualtisation purposes self._send(influx_data) result = json.loads( self.webinterface.get_energy_frequency( None, module['id'])) if result['success'] is False: self.logger('Failed to get frequency data') continue base_timestamp = None abort = False for i in xrange(12): if abort is True: break name = InfluxDB.clean_name( module['input{0}'.format(i)]) if name == '': continue timestamp = base_timestamp length = min(len(result[str(i)]['current'][0]), len(result[str(i)]['voltage'][0])) influx_data = [] for j in xrange(length): data = self._build_command( 'energy_analytics', { 'type': 'frequency', 'id': device_id.format(i), 'name': name }, { 'current_harmonics': result[str(i)]['current'][0][j], 'current_phase': result[str(i)]['current'][1][j], 'voltage_harmonics': result[str(i)]['voltage'][0][j], 'voltage_phase': result[str(i)]['voltage'][1][j] }, timestamp=timestamp) if base_timestamp is not None: influx_data.append(data) else: self._send(data) query = 'SELECT current_harmonics FROM energy_analytics ORDER BY time DESC LIMIT 1' response = requests.get( url=self._query_endpoint, params={'q': query}, headers=self._headers, auth=self._auth, verify=False) if response.status_code != 200: self.logger( 'Query frequency failed, received: {0} ({1})' .format(response.text, response.status_code)) abort = True break base_timestamp = response.json( )['results'][0]['series'][0]['values'][0][ 0] timestamp = base_timestamp timestamp += 250000000 # Stretch actual data by 1000 for visualtisation purposes self._send(influx_data) except CommunicationTimedOutException: self.logger( 'Error getting power analytics: CommunicationTimedOutException' ) except Exception as ex: self.logger('Error getting power analytics: {0}'.format(ex)) self._pause(start, interval, 'power analysis') def _run_power_fibaro(self, interval): while True: start = time.time() if self._has_fibaro_power is True: usage = self._get_fibaro_power() if usage is not None: influx_data = [] for device_id in usage: try: device = usage[device_id] name = InfluxDB.clean_name(device['name']) if name == '': return data = { 'type': 'fibaro', 'id': device_id, 'name': name } values = { 'power': device['power'], 'counter': device['counter'] } influx_data.append( self._build_command('energy', data, values)) except Exception as ex: self.logger( 'Error processing Fibaro power device {0}: {1}' .format(device_id, ex)) self._send(influx_data) self._pause(start, interval, 'power (Fibaro)') @staticmethod def _build_command(key, tags, value, timestamp=None): if isinstance(value, dict): values = ','.join('{0}={1}'.format(vname, vvalue) for vname, vvalue in value.iteritems()) else: values = 'value={0}'.format(value) return '{0},{1} {2}{3}'.format( key, ','.join('{0}={1}'.format(tname, tvalue) for tname, tvalue in tags.iteritems()), values, '' if timestamp is None else ' {0}'.format(timestamp)) def _send(self, data): try: if not isinstance(data, list) and not isinstance(data, basestring): raise RuntimeError('Invalid data passed in _send ({0})'.format( type(data))) if isinstance(data, basestring): data = [data] if len(data) == 0: return True, '' response = requests.post(url=self._endpoint, data='\n'.join(data), headers=self._headers, auth=self._auth, verify=False) if response.status_code != 204: self.logger('Send failed, received: {0} ({1})'.format( response.text, response.status_code)) return False, 'Send failed, received: {0} ({1})'.format( response.text, response.status_code) return True, '' except Exception as ex: self.logger('Error sending: {0}'.format(ex)) return False, 'Error sending: {0}'.format(ex) def _get_fibaro_power(self): try: response = requests.get( url='https://127.0.0.1/plugins/Fibaro/get_power_usage', params={'token': 'None'}, verify=False) if response.status_code == 200: result = response.json() if result['success'] is True: return result['result'] else: self.logger('Error loading Fibaro data: {0}'.format( result['msg'])) else: self.logger('Error loading Fibaro data: {0}'.format( response.status_code)) return None except Exception as ex: self.logger( 'Got unexpected error during Fibaro power load: {0}'.format( ex)) return None @om_expose def send_data(self, key, tags, value): if self._enabled is True: tags = json.loads(tags) value = json.loads(value) success, result = self._send(self._build_command(key, tags, value)) return json.dumps({ 'success': success, 'result' if success else 'error': result }) else: return json.dumps({ 'success': False, 'error': 'InfluxDB plugin not enabled' }) @om_expose def get_config_description(self): return json.dumps(InfluxDB.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() if self._enabled: cthread = Thread(target=self._load_environment_configurations) cthread.start() fthread = Thread(target=self._check_fibaro_power) fthread.start() self.write_config(config) return json.dumps({'success': True})
class Pushetta(OMPluginBase): """ A Pushetta (http://www.pushetta.com) plugin for pushing events through Pushetta """ name = 'Pushetta' version = '1.0.12' interfaces = [('config', '1.0')] config_description = [{'name': 'api_key', 'type': 'str', 'description': 'Your API key.'}, {'name': 'input_id', 'type': 'int', 'description': 'The ID of the input that will trigger the event.'}, {'name': 'channel', 'type': 'str', 'description': 'The channel to push the event to.'}, {'name': 'message', 'type': 'str', 'description': 'The message to be send.'}] default_config = {'api_key': '', 'input_id': -1, 'channel': '', 'message': ''} def __init__(self, webinterface, logger): super(Pushetta, self).__init__(webinterface, logger) self.logger('Starting Pushetta plugin...') self._config = self.read_config(Pushetta.default_config) self._config_checker = PluginConfigChecker(Pushetta.config_description) self._read_config() self.logger("Started Pushetta plugin") def _read_config(self): self._api_key = self._config['api_key'] self._input_id = self._config['input_id'] self._channel = self._config['channel'] self._message = self._config['message'] self._endpoint = 'http://api.pushetta.com/api/pushes/{0}/'.format(self._channel) self._headers = {'Accept': 'application/json', 'Authorization': 'Token {0}'.format(self._api_key), 'Content-type': 'application/json', 'X-Requested-With': 'OpenMotics plugin: Pushetta'} self._enabled = self._api_key != '' and self._input_id > -1 and self._channel != '' and self._message != '' def convert(self,data): if isinstance(data,basestring): return str(data) elif isinstance(data,collections.Mapping): return dict(map(self.convert, data.iteritems())) elif isinstance(data,collections.Iterable): return type(data)(map(self.convert,data)) else: return data @input_status def input_status(self, status): if self._enabled is True: input_id = status[0] if input_id == self._input_id: thread = Thread(target=self._process_input, args=(input_id,)) thread.start() def _process_input(self,input_id): try: data = json.dumps({'body': self._message, 'message_type': 'text/plain'}) self.logger('Sending: {0}'.format(data)) response = requests.post(url=self._endpoint, data=data, headers=self._headers, verify=False) self.logger('Received: {0} ({1})'.format(response.text, response.status_code)) except Exception as ex: self.logger('Error sending: {0}'.format(ex)) @om_expose def get_config_description(self): return json.dumps(Pushetta.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) config = self.convert(config) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
def test_check_config_nested_enum(self): """ Test check_config for nested_enum. """ checker = PluginConfigChecker([{ 'name': 'network', 'type': 'nested_enum', 'choices': [{ 'value': 'Facebook', 'content': [{ 'name': 'likes', 'type': 'int' }] }, { 'value': 'Twitter', 'content': [{ 'name': 'followers', 'type': 'int' }] }] }]) checker.check_config({'network': ['Twitter', {'followers': 3}]}) checker.check_config({'network': ['Facebook', {'likes': 3}]}) try: checker.check_config({'network': 'test'}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('list' in str(exception)) try: checker.check_config({'network': []}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('list' in str(exception) and '2' in str(exception)) try: checker.check_config({'network': ['something else', {}]}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('choices' in str(exception)) try: checker.check_config({'network': ['Twitter', {}]}) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('nested_enum dict' in str(exception) and 'followers' in str(exception))
class Statful(OMPluginBase): """ An Statful plugin, for sending metrics to Statful (adapted from InfluxDB plugin) """ name = 'Statful' version = '1.0.0' url = 'https://api.statful.com/tel/v2.0/metrics' interfaces = [('config', '1.0')] config_description = [{'name': 'token', 'type': 'str', 'description': 'Statful API token for authentication.'}, {'name': 'add_custom_tag', 'type': 'str', 'description': 'Add custom tag to statistics'}, {'name': 'batch_size', 'type': 'int', 'description': 'The maximum batch size of grouped metrics to be send to Statful.'}] default_config = {} def __init__(self, webinterface, logger): super(Statful, self).__init__(webinterface, logger) self.logger('Starting Statful plugin...') self._config = self.read_config(Statful.default_config) self._config_checker = PluginConfigChecker(Statful.config_description) self._pending_metrics = {} self._send_queue = deque() self._send_thread = Thread(target=self._sender) self._send_thread.setName('Statful batch sender') self._send_thread.daemon = True self._send_thread.start() self._read_config() self.logger("Started Statful plugin") def _read_config(self): self._batch_size = self._config.get('batch_size', 10) self._add_custom_tag = self._config.get('add_custom_tag', '') token = self._config.get('token', '') self._headers = {'M-Api-Token': token, 'X-Requested-With': 'OpenMotics plugin: Statful'} self._enabled = token != '' self.logger('Statful is {0}'.format('enabled' if self._enabled else 'disabled')) @om_metric_receive(interval=30) def _receive_metric_data(self, metric): """ All metrics are collected, as filtering is done more finegraded when mapping to tables > example_metric = {"source": "OpenMotics", > "type": "energy", > "timestamp": 1497677091, > "tags": {"device": "OpenMotics energy ID1", > "id": 0}, > "values": {"power": 1234, > "power_counter": 1234567}} """ try: if self._enabled is False: return values = metric['values'] _values = {} for key in values.keys()[:]: value = values[key] if isinstance(value, basestring): value = '"{0}"'.format(value) if isinstance(value, bool): value = int(value == True) if isinstance(value, int) or isinstance(value, long): value = '{0}'.format(value) _values[key] = value tags = {'source': metric['source'].lower()} if self._add_custom_tag: tags['custom_tag'] = self._add_custom_tag for tag, tvalue in metric['tags'].iteritems(): if isinstance(tvalue, basestring): # send tag values as ascii. specification details at https://www.statful.com/docs/metrics-ingestion-protocol.html#Metrics-Ingestion-Protocol tags[tag] = tvalue.decode("utf-8").encode('ascii', 'ignore').replace(' ', '_').replace(',', '.') else: tags[tag] = tvalue entries = self._build_entries(metric['type'], tags, _values, metric['timestamp']) for entry in entries: self._send_queue.appendleft(entry) except Exception as ex: self.logger('Error receiving metrics: {0}'.format(ex)) @staticmethod def _build_entries(key, tags, value, timestamp): if isinstance(value, dict): _entries = [] for vname, vvalue in value.iteritems(): _entries.append(Statful._build_entry(key, tags, vname, vvalue, timestamp)) return _entries return [Statful._build_entry(key, tags, None, value, timestamp)] @staticmethod def _build_entry(metric, tags, key, value, timestamp): return 'openmotics.{0},{1} {2}{3}'.format(metric if key is None else '{0}.{1}'.format(metric, key), ','.join('{0}={1}'.format(tname, tvalue) for tname, tvalue in tags.iteritems()), value, '' if timestamp is None else ' {:.0f}'.format(timestamp)) def _sender(self): _stats_time = 0 _batch_sizes = [] _queue_sizes = [] _run_amount = 0 _batch_amount = 0 while True: try: data = [] try: while True: data.append(self._send_queue.pop()) if len(data) == self._batch_size: raise IndexError() except IndexError: pass if len(data) > 0: _batch_sizes.append(len(data)) _run_amount += len(data) _batch_amount += 1 _queue_sizes.append(len(self._send_queue)) response = requests.put(url=Statful.url, data='\n'.join(data), headers=self._headers, verify=False) if response.status_code != 201: self.logger('Send failed, received: {0} ({1})'.format(response.text, response.status_code)) if _stats_time < time.time() - 1800: _stats_time = time.time() self.logger('Queue size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format( min(_queue_sizes), sum(_queue_sizes) / float(len(_queue_sizes)), max(_queue_sizes) )) self.logger('Batch size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format( min(_batch_sizes), sum(_batch_sizes) / float(len(_batch_sizes)), max(_batch_sizes) )) self.logger('Total {0} metric(s) over {1} batche(s)'.format(_run_amount, _batch_amount)) _batch_sizes = [] _queue_sizes = [] _run_amount = 0 _batch_amount = 0 except Exception as ex: self.logger('Error sending from queue: {0}'.format(ex)) time.sleep(0.1) @om_expose def get_config_description(self): return json.dumps(Statful.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class EventForwarder(OMPluginBase): name = 'EventForwarder' version = '0.0.7' interfaces = [('config', '1.0')] config_description = [ { 'name': 'port', 'type': 'int', 'description': 'Internal port for this process to listen on', }, ] default_config = { 'port': 6666, } def __init__(self, webinterface, logger): super(EventForwarder, self).__init__(webinterface, logger) self.config = self.read_config(self.default_config) self.config_checker = PluginConfigChecker(self.config_description) self.connection = None self._connected = False @background_task def listen(self): port = self.config['port'] listener = Listener(('localhost', port)) while True: if self._connected: time.sleep(1) else: if port != self.config['port']: listener.close() port = self.config['port'] listener = Listener('localhost', port) self.connection = listener.accept() # Hangs until a client connects self._connected = True @receive_events def forward_master_events(self, event): """ Forward events of the master. You can set events e.g. as advanced action when configuring inputs. :param event: Event number sent by the master :type event: int """ self.logger('Received event: %s' % event) if not self._connected: return try: self.connection.send(event) except IOError: self.logger('Failed to forward event') self.connection.close() self._connected = False @om_expose def get_config_description(self): return json.dumps(EventForwarder.config_description) @om_expose def get_config(self): return json.dumps(self.config) @om_expose def set_config(self, config): config = json.loads(config) self.config_checker.check_config(config) self.config = config self.write_config(config) return json.dumps({'success': True})
class ModbusTCPSensor(OMPluginBase): """ Get sensor values form modbus """ name = 'modbusTCPSensor' version = '1.0.18' interfaces = [('config', '1.0')] config_description = [{ 'name': 'modbus_server_ip', 'type': 'str', 'description': 'IP or hostname of the ModBus server.' }, { 'name': 'modbus_port', 'type': 'int', 'description': 'Port of the ModBus server. Default: 502' }, { 'name': 'debug', 'type': 'int', 'description': 'Turn on debugging (0 = off, 1 = on)' }, { 'name': 'sample_rate', 'type': 'int', 'description': 'How frequent (every x seconds) to fetch the sensor data, Default: 60' }, { 'name': 'sensors', 'type': 'section', 'description': 'OM sensor ID (e.g. 4), a sensor type and a Modbus Address', 'repeat': True, 'min': 0, 'content': [{ 'name': 'sensor_id', 'type': 'int' }, { 'name': 'sensor_type', 'type': 'enum', 'choices': ['temperature', 'humidity', 'brightness', 'validation_bit'] }, { 'name': 'modbus_address', 'type': 'int' }, { 'name': 'modbus_register_length', 'type': 'int' }] }, { 'name': 'bits', 'type': 'section', 'description': 'OM validation bit ID (e.g. 4), and a Modbus Coil Address', 'repeat': True, 'min': 0, 'content': [{ 'name': 'validation_bit_id', 'type': 'int' }, { 'name': 'modbus_coil_address', 'type': 'int' }] }] default_config = {'modbus_port': 502, 'sample_rate': 60} def __init__(self, webinterface, logger): super(ModbusTCPSensor, self).__init__(webinterface, logger) self.logger('Starting ModbusTCPSensor plugin...') self._config = self.read_config(ModbusTCPSensor.default_config) self._config_checker = PluginConfigChecker( ModbusTCPSensor.config_description) py_modbus_tcp_egg = '/opt/openmotics/python/plugins/modbusTCPSensor/pyModbusTCP-0.1.7-py2.7.egg' if py_modbus_tcp_egg not in sys.path: sys.path.insert(0, py_modbus_tcp_egg) self._client = None self._samples = [] self._save_times = {} self._read_config() self.logger("Started ModbusTCPSensor plugin") def _read_config(self): self._ip = self._config.get('modbus_server_ip') self._port = self._config.get( 'modbus_port', ModbusTCPSensor.default_config['modbus_port']) self._debug = self._config.get('debug', 0) == 1 self._sample_rate = self._config.get( 'sample_rate', ModbusTCPSensor.default_config['sample_rate']) self._sensors = [] # Load Sensors for sensor in self._config.get('sensors', []): if 0 <= sensor['sensor_id'] < 32: self._sensors.append(sensor) # Load Validation bits self._validation_bits = [] for validation_bit in self._config.get('bits', []): if 0 <= validation_bit['validation_bit_id'] < 256: self._validation_bits.append(validation_bit) self._enabled = len(self._sensors) > 0 try: from pyModbusTCP.client import ModbusClient self._client = ModbusClient(self._ip, self._port, auto_open=True, auto_close=True) self._client.open() self._enabled = self._enabled & True except Exception as ex: self.logger('Error connecting to Modbus server: {0}'.format(ex)) self.logger('ModbusTCPSensor is {0}'.format( 'enabled' if self._enabled else 'disabled')) @background_task def run(self): while True: try: if not self._enabled or self._client is None: time.sleep(5) continue # Process all configured sensors self.process_sensors() # Process all validation bits self.process_validation_bits() time.sleep(self._sample_rate) except Exception as ex: self.logger('Could not process sensor values: {0}'.format(ex)) time.sleep(15) def clamp_sensor(self, value, sensor_type): clamping = { 'temperature': [-32, 95.5, 1], 'humidity': [0, 100, 1], 'brightness': [0, 100, 0] } return round( max(clamping[sensor_type][0], min(value, clamping[sensor_type][1])), clamping[sensor_type][2]) def process_sensors(self): om_sensors = {} for sensor in self._sensors: registers = self._client.read_holding_registers( sensor['modbus_address'], sensor['modbus_register_length']) if registers is None: continue sensor_value = struct.unpack( '>f', struct.pack('BBBB', registers[1] >> 8, registers[1] & 255, registers[0] >> 8, registers[0] & 255))[0] if not om_sensors.get(sensor['sensor_id']): om_sensors[sensor['sensor_id']] = { 'temperature': None, 'humidity': None, 'brightness': None } sensor_value = self.clamp_sensor(sensor_value, sensor['sensor_type']) om_sensors[sensor['sensor_id']][ sensor['sensor_type']] = sensor_value if self._debug == 1: self.logger('The sensors values are: {0}'.format(om_sensors)) for sensor_id, values in om_sensors.iteritems(): result = json.loads( self.webinterface.set_virtual_sensor(sensor_id, **values)) if result['success'] is False: self.logger( 'Error when updating virtual sensor {0}: {1}'.format( sensor_id, result['msg'])) def process_validation_bits(self): for validation_bit in self._validation_bits: bit = self._client.read_coils( validation_bit['modbus_coil_address'], 1) if bit is None or len(bit) != 1: if self._debug == 1: self.logger('Failed to read bit {0}, bit is {1}'.format( validation_bit['validation_bit_id'], bit)) continue result = json.loads( self.webinterface.do_basic_action( None, 237 if bit[0] else 238, validation_bit['validation_bit_id'])) if result['success'] is False: self.logger('Failed to set bit {0} to {1}'.format( validation_bit['validation_bit_id'], 1 if bit[0] else 0)) else: if self._debug == 1: self.logger('Successfully set bit {0} to {1}'.format( validation_bit['validation_bit_id'], 1 if bit[0] else 0)) @om_expose def get_config_description(self): return json.dumps(ModbusTCPSensor.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self.write_config(config) self._config = config self._read_config() return json.dumps({'success': True})
class Pumpy(OMPluginBase): """ Plugin to prevent flooding. """ name = 'Pumpy' version = '1.0.0' interfaces = [('config', '1.0')] config_descr = [ {'name':'output_id', 'type':'int', 'description':'The output id for the pump.'}, {'name':'power_id', 'type':'int', 'description':'The power id for the pump.'}, {'name':'watts', 'type':'int', 'description':'The average power used by the pump,' ' when running (in watts).'}, {'name':'email', 'type':'str', 'description':'The email address to send the shutdown notification ' 'to.'} ] def __init__(self, webinterface, logger): """ Default constructor, called by the plugin manager. """ OMPluginBase.__init__(self, webinterface, logger) self.__last_energy = None # The list containing whether the pump was on the last 10 minutes self.__window = [] self.__config = self.read_config() self.__config_checker = PluginConfigChecker(Pumpy.config_descr) self.logger("Started Pumpy plugin") @background_task def run(self): """ Background task that checks the power usage of the pump every minute. """ while True: if self.__config is not None: self.__do_check() time.sleep(60) # Sleep one minute before checking again. def __do_check(self): """ Code for the actual check. """ watts = self.__get_total_energy() if self.__last_energy == None: # The first time we only set the last_energy value. self.__last_energy = watts else: # The next times we calculate the difference: the watts diff = (watts - self.__last_energy) * 1000 # Convert from kWh to Wh pump_energy_in_one_minute = self.__config['watts'] / 60.0 pump_on = (diff >= pump_energy_in_one_minute) if pump_on: self.logger("Pump was running during the last minute") self.__window = self.__window[-9:] # Keep the last 9 'on' values self.__window.append(pump_on) # Add the last 'on' value running_for_10_mintues = True for pump_on in self.__window: running_for_10_mintues = running_for_10_mintues and pump_on if running_for_10_mintues: self.logger("Pump was running for 10 minutes") self.__pump_alert_triggered() self.__last_energy = watts def __pump_alert_triggered(self): """ Actions to complete when a floodding was detected. """ # This method is called when the pump is running for 10 minutes. self.__stop_pump() # The smtp configuration below could be stored in the configuration. try: smtp = smtplib.SMTP('localhost') smtp.sendmail('power@localhost', [self.__config['email']], 'Your pump was shut down because of high power ' 'usage !') except smtplib.SMTPException as exc: self.logger("Failed to send email: %s" % exc) def __get_total_energy(self): """ Get the total energy consumed by the pump. """ energy = self.webinterface.get_total_energy(None) # energy contains a dict of "power_id" to [day, night] array. energy_values = energy[str(self.__config['power_id'])] # Return the sum of the night and day values. return energy_values[0] + energy_values[1] def __stop_pump(self): """ Stop the pump. """ self.webinterface.set_output(self.__config['output_id'], False) def __start_pump(self): """ Start the pump. """ self.webinterface.set_output(self.__config['output_id'], True) @om_expose def get_config_description(self): """ Get the configuration description. """ return json.dumps(Pumpy.config_descr) @om_expose def get_config(self): """ Get the current configuration. """ config = self.__config if self.__config is not None else {} return json.dumps(self.__config) @om_expose def set_config(self, config): """ Set a new configuration. """ config = json.loads(config) self.__config_checker.check_config(config) self.write_config(config) self.__config = config return json.dumps({'success':True}) @om_expose def reset(self): """ Resets the counters and start the pump. """ self.__window = [] if self.__config is not None: self.__start_pump() return json.dumps({'success':True})
class Pumpy(OMPluginBase): """ Plugin to prevent flooding. """ name = 'Pumpy' version = '1.0.0' interfaces = [('config', '1.0')] config_descr = [{ 'name': 'output_id', 'type': 'int', 'description': 'The output id for the pump.' }, { 'name': 'power_id', 'type': 'int', 'description': 'The power id for the pump.' }, { 'name': 'watts', 'type': 'int', 'description': 'The average power used by the pump,' ' when running (in watts).' }, { 'name': 'email', 'type': 'str', 'description': 'The email address to send the shutdown notification ' 'to.' }] def __init__(self, webinterface, logger): """ Default constructor, called by the plugin manager. """ OMPluginBase.__init__(self, webinterface, logger) self.__last_energy = None # The list containing whether the pump was on the last 10 minutes self.__window = [] self.__config = self.read_config() self.__config_checker = PluginConfigChecker(Pumpy.config_descr) self.logger("Started Pumpy plugin") @background_task def run(self): """ Background task that checks the power usage of the pump every minute. """ while True: if self.__config is not None: self.__do_check() time.sleep(60) # Sleep one minute before checking again. def __do_check(self): """ Code for the actual check. """ watts = self.__get_total_energy() if self.__last_energy == None: # The first time we only set the last_energy value. self.__last_energy = watts else: # The next times we calculate the difference: the watts diff = (watts - self.__last_energy) * 1000 # Convert from kWh to Wh pump_energy_in_one_minute = self.__config['watts'] / 60.0 pump_on = (diff >= pump_energy_in_one_minute) if pump_on: self.logger("Pump was running during the last minute") self.__window = self.__window[-9:] # Keep the last 9 'on' values self.__window.append(pump_on) # Add the last 'on' value running_for_10_mintues = True for pump_on in self.__window: running_for_10_mintues = running_for_10_mintues and pump_on if running_for_10_mintues: self.logger("Pump was running for 10 minutes") self.__pump_alert_triggered() self.__last_energy = watts def __pump_alert_triggered(self): """ Actions to complete when a floodding was detected. """ # This method is called when the pump is running for 10 minutes. self.__stop_pump() # The smtp configuration below could be stored in the configuration. try: smtp = smtplib.SMTP('localhost') smtp.sendmail( 'power@localhost', [self.__config['email']], 'Your pump was shut down because of high power ' 'usage !') except smtplib.SMTPException as exc: self.logger("Failed to send email: %s" % exc) def __get_total_energy(self): """ Get the total energy consumed by the pump. """ energy = self.webinterface.get_total_energy(None) # energy contains a dict of "power_id" to [day, night] array. energy_values = energy[str(self.__config['power_id'])] # Return the sum of the night and day values. return energy_values[0] + energy_values[1] def __stop_pump(self): """ Stop the pump. """ self.webinterface.set_output(self.__config['output_id'], False) def __start_pump(self): """ Start the pump. """ self.webinterface.set_output(self.__config['output_id'], True) @om_expose def get_config_description(self): """ Get the configuration description. """ return json.dumps(Pumpy.config_descr) @om_expose def get_config(self): """ Get the current configuration. """ config = self.__config if self.__config is not None else {} return json.dumps(self.__config) @om_expose def set_config(self, config): """ Set a new configuration. """ config = json.loads(config) self.__config_checker.check_config(config) self.write_config(config) self.__config = config return json.dumps({'success': True}) @om_expose def reset(self): """ Resets the counters and start the pump. """ self.__window = [] if self.__config is not None: self.__start_pump() return json.dumps({'success': True})
class SensorDotCommunity(OMPluginBase): name = 'SensorDotCommunity' version = '1.0.1' interfaces = [('config', '1.0')] config_description = [] default_config = [] def __init__(self, webinterface, gateway_logger): self.setup_logging(log_function=gateway_logger) super(SensorDotCommunity, self).__init__(webinterface, logger) logger.info('Starting %s plugin %s ...', self.name, self.version) self._config = self.default_config self._config_checker = PluginConfigChecker( SensorDotCommunity.config_description) logger.info("%s plugin started", self.name) @staticmethod def setup_logging(log_function): # type: (Callable) -> None logger.setLevel(logging.INFO) log_handler = PluginLogHandler(log_function=log_function) # some elements like time and name are added by the plugin runtime already # formatter = logging.Formatter('%(asctime)s - %(name)s - %(threadName)s - %(levelname)s - %(message)s') formatter = logging.Formatter( '%(threadName)s - %(levelname)s - %(message)s') log_handler.setFormatter(formatter) logger.addHandler(log_handler) @om_expose(version=2, auth=False) def api(self, plugin_web_request): """ { "esp8266id": "12345678", "software_version": "NRZ-2020-133", "sensordatavalues": [ { "value_type": "SDS_P1", "value": "7.18" }, { "value_type": "SDS_P2", "value": "2.58" }, { "value_type": "temperature", "value": "23.20" }, { "value_type": "humidity", "value": "46.40" }, { "value_type": "samples", "value": "5029246" }, { "value_type": "min_micro", "value": "28" }, { "value_type": "max_micro", "value": "20132" }, { "value_type": "interval", "value": "145000" }, { "value_type": "signal", "value": "-60" } ] } """ method = plugin_web_request.method path = plugin_web_request.path body = plugin_web_request.body params = plugin_web_request.params headers = plugin_web_request.headers errors = [] logger.debug('%s %s %s', method, path, params) logger.debug('%s', headers) logger.debug('%s', body) data = json.loads(body) known_sensors = self._get_known_sensors() device_id = data["esp8266id"] for entry in data.get("sensordatavalues", []): value_type = entry["value_type"] value = float(entry["value"]) sensor_external_id = "{}/{}".format(device_id, value_type) if value_type == "temperature": name = "Temperature" physical_quantity = "temperature" unit = "celcius" elif value_type == "humidity": name = "Humidity" physical_quantity = "humidity" unit = "percent" elif value_type == "SDS_P1": name = "PM10" physical_quantity = "dust" unit = "micro_gram_per_cubic_meter" elif value_type == "SDS_P2": name = "PM2.5" physical_quantity = "dust" unit = "micro_gram_per_cubic_meter" else: logger.debug('unsupported sensor value %s', value_type) continue if sensor_external_id not in known_sensors.keys(): logger.info('Registering new sensor %s with external id %s', name, sensor_external_id) om_sensor_id = self._register_sensor(name, sensor_external_id, physical_quantity, unit) else: om_sensor_id = known_sensors[sensor_external_id] if om_sensor_id is not None: logger.info('Updating sensor %s (%s) with %s (%s)', name, om_sensor_id, value, unit) self._update_sensor(om_sensor_id, value) else: msg = 'Sensor.community sensor {} ({}) not found'.format( name, sensor_external_id) logger.error(msg) errors.append(msg) if errors: return PluginWebResponse(status_code=500, body='\n'.join(errors), path=plugin_web_request.path) else: return PluginWebResponse(status_code=200, body='success', path=plugin_web_request.path) def _get_known_sensors(self): response = self.webinterface.get_sensor_configurations() data = json.loads(response) return { x['external_id']: x['id'] for x in data['config'] if x.get('source', {}).get('name') == SensorDotCommunity.name and x['external_id'] not in [None, ''] } @om_expose def get_config_description(self): return json.dumps(SensorDotCommunity.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self.write_config(config) return json.dumps({'success': True}) def _register_sensor(self, name, external_id, physical_quantity, unit): logger.info('Registering sensor with name %s and external_id %s', name, external_id) try: config = { 'name': name, } response = self.webinterface.sensor.register( external_id=external_id, physical_quantity=physical_quantity, unit=unit, config=config) logger.info( 'Registered new sensor with name %s and external_id %s', name, external_id) return response.id except Exception as e: logger.warning( 'Failed registering sensor with name %s and external_id %s with exception %s', name, external_id, str(e.message)) return None def _update_sensor(self, sensor_id, value): logger.debug('Updating sensor %s with status %s', sensor_id, value) response = self.webinterface.sensor.set_status(sensor_id=sensor_id, value=value) if response is None: logger.warning('Could not set the updated sensor value') return False return True
class RTD10(OMPluginBase): """ RTD10 plugin """ name = 'RTD10' version = '0.1.4' interfaces = [('config', '1.0')] config_description = [{ 'name': 'thermostats', 'type': 'section', 'description': 'Thermostats', 'repeat': True, 'min': 1, 'content': [{ 'name': 'thermostat_id', 'type': 'int', 'description': 'OpenMotics thermostat ID' }, { 'name': 's1_output_id', 'type': 'int', 'description': 'Output connected to the S1 port (setpoint control)' }, { 'name': 's1_temperature_curve', 'type': 'str', 'description': 'Setpoint control temperature curve' }, { 'name': 's2_output_id', 'type': 'int', 'description': 'Output connected to the S2 port (ventilation control)' }, { 'name': 's2_value', 'type': 'int', 'description': 'Ventilation output value (0-100, in percent of 0-10V)' }, { 'name': 's3_output_id', 'type': 'int', 'description': 'Output connected to the S3 port (mode)' }, { 'name': 's4_output_id', 'type': 'int', 'description': 'Output connected to the S4 port (air direction)' }, { 'name': 's4_value', 'type': 'int', 'description': 'Air direction output value (0-100, in percent of 0-10V)' }, { 'name': 's5_output_id', 'type': 'int', 'description': 'Output connected to the S5 port (state)' }] }] default_config = {} def __init__(self, webinterface, logger): super(RTD10, self).__init__(webinterface, logger) self.logger('Starting RTD10 plugin...') self._config = self.read_config(RTD10.default_config) self._config_checker = PluginConfigChecker(RTD10.config_description) self._enabled = False self._syncing = False self._thermostats = {} self._s_values = {} self._read_config() self.logger("Started RTD10 plugin") def _read_config(self): self._enabled = False try: used_ids = [] for thermostat in self._config.get('thermostats', []): thermostat_id = int(thermostat.get('thermostat_id')) config = { 's{0}_output_id'.format(i): int(thermostat.get('s{0}_output_id'.format(i))) for i in range(1, 6) } temperature_curve = { float(key): int(value) for key, value in json.loads( thermostat.get('s1_temperature_curve')).items() } config.update({ 's1_temperature_curve': temperature_curve, 's2_value': int(thermostat.get('s2_value')), 's4_value': int(thermostat.get('s4_value')) }) self._thermostats[thermostat_id] = config used_ids.append(thermostat_id) for thermostat_id in list(self._thermostats.keys()): if thermostat_id not in used_ids: self._thermostats.pop(thermostat_id, None) self._enabled = True except Exception as ex: self.logger('Could not read/process configuration: {0}'.format(ex)) self.logger( 'RTD10 is {0}'.format('enabled' if self._enabled else 'disabled')) if self._enabled: thread = Thread(target=self._sync) thread.start() def _sync(self): if self._syncing: return self.logger('Performing initial sync...') try: self._syncing = True while True: try: result = json.loads( self.webinterface.get_thermostat_group_status()) if result.get('success', False) is False: raise RuntimeError(result.get('msg', 'Unknown error')) for thermostat_group_status in result.get('status', []): mode = thermostat_group_status['mode'].upper( ) # COOLING / HEATING for thermostat_status in thermostat_group_status[ 'thermostats']: thermostat_id = thermostat_status['id'] state = thermostat_status['state'].upper( ) # ON / OFF setpoint = thermostat_status[ 'setpoint_temperature'] self._drive_device(thermostat_id=thermostat_id, mode=mode, state=state, setpoint=setpoint) return except Exception as ex: self.logger( 'Could not load thermostat group states: {0}'.format( ex)) time.sleep(30) finally: self.logger('Performing initial sync... Done') self._syncing = False @thermostat_status(version=1) def thermostat_status(self, status): thermostat_id = status['id'] mode = status['status']['mode'].upper() # COOLING / HEATING state = status['status']['state'].upper() # ON / OFF setpoint = status['status']['current_setpoint'] self._drive_device(thermostat_id=thermostat_id, mode=mode, state=state, setpoint=setpoint) def _drive_device(self, thermostat_id, mode, state, setpoint): configuration = self._thermostats.get(thermostat_id) if configuration is None: return # S1 - Temperature curve: Map the current setpoint to a valve output value s1_value = 0 output_id = configuration['s1_output_id'] for temperature in sorted( configuration['s1_temperature_curve'].keys()): if setpoint >= temperature: s1_value = configuration['s1_temperature_curve'][temperature] self._set_output(output_id=output_id, output_value=s1_value, s_number=1, thermostat_id=thermostat_id) # S2 - Ventilation control: Set the ventilation value (hardcoded configured value) output_id = configuration['s2_output_id'] s2_value = configuration['s2_value'] self._set_output(output_id=output_id, output_value=s2_value, s_number=2, thermostat_id=thermostat_id) # S3 - Mode: Sets the system mode output_id = configuration['s3_output_id'] s3_value = 32 if mode == 'HEATING' else 62 self._set_output(output_id=output_id, output_value=s3_value, s_number=3, thermostat_id=thermostat_id) # S4 - Air direction: Sets the RTD10 air direction output_id = configuration['s4_output_id'] s4_value = configuration['s4_value'] self._set_output(output_id=output_id, output_value=s4_value, s_number=4, thermostat_id=thermostat_id) # S5 - State: Sets the RTD10 state output_id = configuration['s5_output_id'] s5_value = 100 if state == 'ON' else 0 self._set_output(output_id=output_id, output_value=s5_value, s_number=5, thermostat_id=thermostat_id) new_s_values = [s1_value, s2_value, s3_value, s4_value, s5_value] if new_s_values != self._s_values.get(thermostat_id): self.logger('New S-values for thermostat {0}: {1}'.format( thermostat_id, ', '.join([ 'S{0}={1:.1f}V'.format(i + 1, float(new_s_values[i]) / 10.0) for i in range(5) ]))) self._s_values[thermostat_id] = new_s_values def _set_output(self, output_id, output_value, s_number, thermostat_id): try: result = json.loads( self.webinterface.set_output(id=output_id, is_on=output_value > 0, dimmer=output_value)) if result.get('success', False) is False: raise RuntimeError(result.get('msg', 'Unknown error')) except Exception as ex: self.logger( 'Could not set output {0} (S{1} for thermostat {2}) to {3}: {4}' .format(output_id, s_number, thermostat_id, output_value, ex)) @om_expose def get_config_description(self): return json.dumps(RTD10.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Polysun(OMPluginBase): """ A Polysun plugin """ class State(object): # In sync with Gateway implementation GOING_UP = 'going_up' GOING_DOWN = 'going_down' STOPPED = 'stopped' UP = 'up' DOWN = 'down' name = 'Polysun' version = '0.1.6' interfaces = [('config', '1.0')] config_description = [{ 'name': 'mapping', 'type': 'section', 'description': 'Shutter to Output mapping', 'repeat': True, 'min': 1, 'content': [{ 'name': 'shutter_id', 'type': 'int' }, { 'name': 'output_id_up', 'type': 'int' }, { 'name': 'output_id_down', 'type': 'int' }, { 'name': 'inputs', 'type': 'section', 'description': 'Inputs that directly manipulate both Outputs', 'repeat': True, 'min': 0, 'content': [{ 'name': 'input_id', 'type': 'int' }] }] }] default_config = {} def __init__(self, webinterface, logger): super(Polysun, self).__init__(webinterface, logger) self.logger('Starting Polysun plugin...') self._config = self.read_config(Polysun.default_config) self._config_checker = PluginConfigChecker(Polysun.config_description) self._states = {} self._mapping = {} self._input_shutter_mapping = {} self._lost_shutters = {} self._action_queue = deque() self._input_enabled = None self._read_config() self.logger("Started Polysun plugin") def _read_config(self): new_mapping = {} new_input_mapping = {} for entry in self._config.get('mapping', []): shutter_id = entry['shutter_id'] try: shutter_id = int(shutter_id) output_id_up = entry['output_id_up'] output_id_down = entry['output_id_down'] if not 0 <= shutter_id <= 240 or not 0 <= output_id_up <= 240 or not 0 <= output_id_down <= 240: continue new_mapping[shutter_id] = { 'up': output_id_up, 'down': output_id_down } for input_entry in entry.get('inputs', []): input_id = int(input_entry['input_id']) new_input_mapping.setdefault(input_id, set()).add(shutter_id) except ValueError: self.logger( 'Skipped entry with shutter_id {0}'.format(shutter_id)) self._mapping = new_mapping self._input_shutter_mapping = new_input_mapping self._enabled = len(self._mapping) > 0 self.logger('Polysun is {0}'.format( 'enabled' if self._enabled else 'disabled')) @shutter_status def shutter_status(self, status, detail): _ = status # We need the details for shutter_id in detail: new_state = detail[shutter_id]['state'] shutter_id = int(shutter_id) if shutter_id not in self._mapping: continue old_state = self._states.get(shutter_id, Polysun.State.STOPPED) if new_state != old_state: self._states[shutter_id] = new_state self._action_queue.appendleft( [shutter_id, new_state, old_state]) self.logger( 'Shutter {0}: Received state transition from {1} to {2}'. format(shutter_id, old_state, new_state)) @input_status(version=2) def input_status(self, data): if self._enabled and self._input_enabled: input_id = data.get('input_id') shutter_ids = self._input_shutter_mapping.get(input_id, set()) for shutter_id in shutter_ids: self.logger( 'Shutter {0}: Lost position due to Input {1}'.format( shutter_id, input_id)) self._lost_shutters[shutter_id] = time.time() self.webinterface.shutter_report_lost_position(id=shutter_id) @background_task def runner(self): while True: try: if len(self._input_shutter_mapping ) > 0 and self._input_enabled is None: result = json.loads(self.webinterface.get_features()) if not result.get('success', False): self.logger('Could not load features: {0}'.format( result.get('msg', 'Unknown'))) else: features = result.get('features', []) self._input_enabled = 'shutter_positions' in features self.logger( 'Gateway {0} support reporting lost positions'. format( 'does' if self._input_enabled else 'does not')) except Exception as ex: self.logger( 'Unexpected exception loading Gateway features: {0}'. format(ex)) try: shutter_id, new_state, old_state = self._action_queue.pop() mapping = self._mapping.get(shutter_id) if mapping is None: continue if new_state == Polysun.State.STOPPED: lost_time = self._lost_shutters.get(shutter_id, 0) if lost_time > ( time.time() - 5 ): # Ignore STOPPED state within `x` seconds of lost position self.logger( 'Shutter {0}: Ignore stopped state due to lost position' .format(shutter_id)) continue output_id_up = mapping['up'] output_id_down = mapping['down'] # If there's an immediate direction change (the shutter is going up and is suddenly going down or vice versa, # the "button" are first released and the shutter considered to be stopped for further logic if old_state in [ Polysun.State.GOING_DOWN, Polysun.State.GOING_UP ] and new_state in [ Polysun.State.GOING_DOWN, Polysun.State.GOING_UP ]: self.logger( 'Shutter {0}: Immediate direction change'.format( shutter_id)) self._turn_output(output_id_up, False) self._turn_output(output_id_down, False) old_state = Polysun.State.STOPPED self.logger( 'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off' .format(shutter_id, output_id_up, output_id_down)) # If the old state was in some stopped state (either stopped, up or down) a movement is started. This means one of the # "buttons" needs to be pressed until the shutter timeout is elapsed. if old_state in [ Polysun.State.DOWN, Polysun.State.UP, Polysun.State.STOPPED ]: self.logger( 'Shutter {0}: Started moving'.format(shutter_id)) if new_state == Polysun.State.GOING_DOWN: self._turn_output( output_id_up, False ) # Make sure only one "button" is pressed at a time self._turn_output(output_id_down, True) self.logger( 'Shutter {0}: Connected output {1} (down) is turned on' .format(shutter_id, output_id_down)) if new_state == Polysun.State.GOING_UP: self._turn_output( output_id_down, False ) # Make sure only one "button" is pressed at a time self._turn_output(output_id_up, True) self.logger( 'Shutter {0}: Connected output {1} (up) is turned on' .format(shutter_id, output_id_up)) # If the shutter is currently moving and reached the UP/DOWN position, it means the configured timeout is elapsed # and the the blinds are assumed to be moving to the correct location. The "buttons" can be released if old_state in [ Polysun.State.GOING_UP, Polysun.State.GOING_DOWN ] and new_state in [Polysun.State.UP, Polysun.State.DOWN]: self.logger('Shutter {0}: Shutter is now up/down'.format( shutter_id)) self._turn_output(output_id_up, False) self._turn_output(output_id_down, False) self.logger( 'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off' .format(shutter_id, output_id_up, output_id_down)) # If the new state is STOPPED, it (should) mean that an explicit stop action was executed. This is emulated by # briefly pressing the "button" again. if new_state == Polysun.State.STOPPED: output_id = output_id_down direction = 'down' if old_state == Polysun.State.GOING_UP: output_id = output_id_up direction = 'up' self.logger( 'Shutter {0}: Shutter is stopped'.format(shutter_id)) self._turn_output(output_id_down, False) self._turn_output(output_id_up, False) self.logger( 'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off' .format(shutter_id, output_id_up, output_id_down)) self._turn_output(output_id, True) self._turn_output(output_id, False) self.logger( 'Shutter {0}: Connected output {1} ({2}) turned on & off' .format(shutter_id, output_id, direction)) except IndexError: time.sleep(1) except Exception as ex: self.logger( 'Unexpected exception processing workload: {0}'.format(ex)) time.sleep(1) def _turn_output(self, output_id, on): try: result = json.loads( self.webinterface.set_output(id=output_id, is_on=on)) if not result.get('success', False): self.logger('Could not turn {0} output {1}: {2}'.format( 'on' if on else 'off', output_id, result.get('msg', 'Unknown'))) except Exception as ex: self.logger( 'Unexpected exception turning {0} output {1}: {2}'.format( 'on' if on else 'off', output_id, ex)) @om_expose def get_config_description(self): return json.dumps(Polysun.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], six.string_types): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Fibaro(OMPluginBase): """ A Fibaro plugin, for controlling devices in your Fibaro Home Center (lite) """ name = 'Fibaro' version = '1.1.5' interfaces = [('config', '1.0')] config_description = [{'name': 'ip', 'type': 'str', 'description': 'The IP of the Fibaro Home Center (lite) device. E.g. 1.2.3.4'}, {'name': 'username', 'type': 'str', 'description': 'Username of a user with the required access.'}, {'name': 'password', 'type': 'str', 'description': 'Password of the user.'}, {'name': 'output_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics (Virtual) Outputs and Fibaro Outputs', 'repeat': True, 'min': 0, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'fibaro_output_id', 'type': 'int'}]}, {'name': 'sensor_mapping', 'type': 'section', 'description': 'Mapping betweet OpenMotics Virtual Sensors and Fibaro Sensors', 'repeat': True, 'min': 0, 'content': [{'name': 'sensor_id', 'type': 'int'}, {'name': 'fibaro_temperature_id', 'type': 'int'}, {'name': 'fibaro_brightness_id', 'type': 'int'}, {'name': 'fibaro_brightness_max', 'type': 'int'}]}] default_config = {'ip': '', 'username': '', 'password': ''} def __init__(self, webinterface, logger): super(Fibaro, self).__init__(webinterface, logger) self.logger('Starting Fibaro plugin...') self._config = self.read_config(Fibaro.default_config) self._config_checker = PluginConfigChecker(Fibaro.config_description) self._read_config() self._previous_output_state = {} self.logger("Started Fibaro plugin") def _read_config(self): self._ip = self._config['ip'] self._output_mapping = self._config.get('output_mapping', []) self._sensor_mapping = self._config.get('sensor_mapping', []) self._username = self._config['username'] self._password = self._config['password'] self._endpoint = 'http://{0}/api/{{0}}'.format(self._ip) self._headers = {'X-Requested-With': 'OpenMotics plugin: Fibaro', 'X-Fibaro-Version': '2'} self._enabled = self._ip != '' and self._username != '' and self._password != '' self.logger('Fibaro is {0}'.format('enabled' if self._enabled else 'disabled')) @output_status def output_status(self, status): if self._enabled is True: try: active_outputs = [] for entry in status: active_outputs.append(entry[0]) for entry in self._output_mapping: output_id = entry['output_id'] fibaro_output_id = entry['fibaro_output_id'] is_on = output_id in active_outputs key = '{0}_{1}'.format(output_id, fibaro_output_id) if key in self._previous_output_state: if is_on != self._previous_output_state[key]: thread = Thread(target=self._send, args=('callAction', {'deviceID': fibaro_output_id, 'name': 'turnOn' if is_on else 'turnOff'})) thread.start() else: thread = Thread(target=self._send, args=('callAction', {'deviceID': fibaro_output_id, 'name': 'turnOn' if is_on else 'turnOff'})) thread.start() self._previous_output_state[key] = is_on except Exception as ex: self.logger('Error processing output_status event: {0}'.format(ex)) def _send(self, action, data): try: url = self._endpoint.format(action) params = '&'.join(['{0}={1}'.format(key, value) for key, value in data.iteritems()]) self.logger('Calling {0}?{1}'.format(url, params)) response = requests.get(url=url, params=data, headers=self._headers, auth=(self._username, self._password)) if response.status_code != 202: self.logger('Call failed, received: {0} ({1})'.format(response.text, response.status_code)) return result = response.json() if result['result']['result'] not in [0, 1]: self.logger('Call failed, received: {0} ({1})'.format(response.text, response.status_code)) return except Exception as ex: self.logger('Error during call: {0}'.format(ex)) @background_task def run(self): while True: if self._enabled: start = time.time() try: response = requests.get(url='http://{0}/api/devices'.format(self._ip), headers=self._headers, auth=(self._username, self._password)) if response.status_code != 200: self.logger('Failed to load power devices') else: sensor_values = {} result = response.json() for device in result: if 'properties' in device: for sensor in self._sensor_mapping: sensor_id = sensor['sensor_id'] if sensor.get('fibaro_temperature_id', -1) == device['id'] and 'value' in device['properties']: if sensor_id not in sensor_values: sensor_values[sensor_id] = [None, None, None] sensor_values[sensor_id][0] = max(-32.0, min(95.0, float(device['properties']['value']))) if sensor.get('fibaro_brightness_id', -1) == device['id'] and 'value' in device['properties']: if sensor_id not in sensor_values: sensor_values[sensor_id] = [None, None, None] limit = float(sensor.get('fibaro_brightness_max', 500)) value = float(device['properties']['value']) sensor_values[sensor_id][2] = max(0.0, min(100.0, value / limit * 100)) for sensor_id, values in sensor_values.iteritems(): result = json.loads(self.webinterface.set_virtual_sensor(None, sensor_id, *values)) if result['success'] is False: self.logger('Error when updating virtual sensor {0}: {1}'.format(sensor_id, result['msg'])) except Exception as ex: self.logger('Error while setting virtual sensors: {0}'.format(ex)) # This loop should run approx. every 5 seconds sleep = 5 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(5) @om_expose def get_power_usage(self): if self._enabled: response = requests.get(url='http://{0}/api/devices'.format(self._ip), headers=self._headers, auth=(self._username, self._password)) if response.status_code != 200: self.logger('Failed to load power devices') return json.dumps({'success': False, 'error': 'Could not load power devices'}) result = response.json() devices = {} for device in result: if 'properties' in device and 'power' in device['properties']: devices[device['id']] = {'id': device['id'], 'name': device['name'], 'power': float(device['properties']['power']), 'counter': float(device['properties']['energy']) * 1000} return json.dumps({'success': True, 'result': devices}) else: return json.dumps({'success': False, 'error': 'Fibaro plugin not enabled'}) @om_expose def get_config_description(self): return json.dumps(Fibaro.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Ventilation(OMPluginBase): """ A ventilation plugin, using statistical humidity or the dew point data to control the ventilation """ name = 'Ventilation' version = '1.1.8' interfaces = [('config', '1.0')] config_description = [{'name': 'low', 'type': 'section', 'description': 'Output configuration for "low" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'medium', 'type': 'section', 'description': 'Output configuration for "medium" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'high', 'type': 'section', 'description': 'Output configuration for "high" ventilation', 'repeat': True, 'min': 1, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'value', 'type': 'int'}]}, {'name': 'sensors', 'type': 'section', 'description': 'Sensors to use for ventilation control.', 'repeat': True, 'min': 1, 'content': [{'name': 'sensor_id', 'type': 'int'}]}, {'name': 'mode', 'type': 'nested_enum', 'description': 'The mode of the ventilation control', 'choices': [{'value': 'statistical', 'content': [{'name': 'samples', 'type': 'int'}, {'name': 'trigger', 'type': 'int'}]}, {'value': 'dew_point', 'content': [{'name': 'outside_sensor_id', 'type': 'int'}, {'name': 'target_lower', 'type': 'int'}, {'name': 'target_upper', 'type': 'int'}, {'name': 'offset', 'type': 'int'}, {'name': 'trigger', 'type': 'int'}]}]}] default_config = {} def __init__(self, webinterface, logger): super(Ventilation, self).__init__(webinterface, logger) self.logger('Starting Ventilation plugin...') self._config = self.read_config(Ventilation.default_config) self._config_checker = PluginConfigChecker(Ventilation.config_description) self._used_sensors = [] self._samples = {} self._sensors = {} self._runtime_data = {} self._settings = {} self._last_ventilation = None self._read_config() self._load_sensors() self._has_influxdb = False if self._enabled: thread = Thread(target=self._check_influxdb) thread.start() self.logger("Started Ventilation plugin") def _read_config(self): self._outputs = {1: self._config.get('low', []), 2: self._config.get('medium', []), 3: self._config.get('medium', [])} self._used_sensors = [sensor['sensor_id'] for sensor in self._config.get('sensors', [])] self._mode, self._settings = self._config.get('mode', ['disabled', {}]) self._enabled = len(self._used_sensors) > 0 and self._mode in ['dew_point', 'statistical'] self.logger('Ventilation is {0}'.format('enabled' if self._enabled else 'disabled')) def _load_sensors(self): try: configs = json.loads(self.webinterface.get_sensor_configurations(None)) if configs['success'] is False: self.logger('Failed to get sensor configurations') else: for sensor in configs['config']: sensor_id = sensor['id'] if sensor_id in self._used_sensors or sensor_id == self._settings['outside_sensor_id']: self._samples[sensor_id] = [] self._sensors[sensor_id] = sensor['name'] if sensor['name'] != '' else sensor_id except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error getting sensor status: {0}'.format(ex)) def _check_influxdb(self): time.sleep(10) self._has_influxdb = False try: response = requests.get(url='https://127.0.0.1/get_plugins', params={'token': 'None'}, verify=False) if response.status_code == 200: result = response.json() if result['success'] is True: for plugin in result['plugins']: if plugin['name'] == 'InfluxDB': version = plugin['version'] self._has_influxdb = version >= '0.5.1' break else: self.logger('Error loading plugin data: {0}'.format(result['msg'])) else: self.logger('Error loading plugin data: {0}'.format(response.status_code)) except Exception as ex: self.logger('Got unexpected error during plugin load: {0}'.format(ex)) self.logger('InfluxDB plugin {0}detected'.format('' if self._has_influxdb else 'not ')) @background_task def run(self): self._runtime_data = {} while True: if self._enabled: start = time.time() if self._mode == 'statistical': self._process_statistics() elif self._mode == 'dew_point': self._process_dew_point() # This loop should run approx. every minute. sleep = 60 - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) else: time.sleep(5) def _process_dew_point(self): try: dew_points = {} abs_humidities = {} humidities = {} sensor_temperatures = {} outdoor_abs_humidity = None outdoor_dew_point = None outdoor_sensor_id = self._settings['outside_sensor_id'] # Fetch data data_humidities = json.loads(self.webinterface.get_sensor_humidity_status(None)) data_temperatures = json.loads(self.webinterface.get_sensor_temperature_status(None)) if data_humidities['success'] is True and data_temperatures['success'] is True: for sensor_id in range(len(data_humidities['status'])): if sensor_id not in self._used_sensors + [outdoor_sensor_id]: continue humidity = data_humidities['status'][sensor_id] if humidity == 255: continue temperature = data_temperatures['status'][sensor_id] if temperature == 95.5: continue humidities[sensor_id] = humidity if sensor_id == outdoor_sensor_id: outdoor_dew_point = Ventilation._dew_point(temperature, humidity) outdoor_abs_humidity = Ventilation._abs_humidity(temperature, humidity) else: sensor_temperatures[sensor_id] = temperature dew_points[sensor_id] = Ventilation._dew_point(temperature, humidity) abs_humidities[sensor_id] = Ventilation._abs_humidity(temperature, humidity) if outdoor_abs_humidity is None or outdoor_dew_point is None: self.logger('Could not load outdoor humidity or temperature') return # Calculate required ventilation based on sensor information target_lower = self._settings['target_lower'] target_upper = self._settings['target_upper'] offset = self._settings['offset'] ventilation = 1 trigger_sensors = {1: [], 2: [], 3: []} for sensor_id in dew_points: if sensor_id not in self._runtime_data: self._runtime_data[sensor_id] = {'trigger': 0, 'ventilation': 1, 'candidate': 1, 'reason': '', 'name': self._sensors[sensor_id], 'stats': [0, 0, 0, 0]} humidity = humidities[sensor_id] dew_point = dew_points[sensor_id] abs_humidity = abs_humidities[sensor_id] temperature = sensor_temperatures[sensor_id] self._runtime_data[sensor_id]['stats'] = [temperature, dew_point, abs_humidity, outdoor_abs_humidity] reason = '{0:.2f} <= {1:.2f} <= {2:.2f}'.format(target_lower, humidity, target_upper) wanted_ventilation = 1 # First, try to get the dew point inside the target range - increasing comfort if humidity < target_lower or humidity > target_upper: if humidity < target_lower and outdoor_abs_humidity > abs_humidity: wanted_ventilation = 2 reason = '{0:.2f} < {1:.2f} and {2:.5f} > {3:.5f}'.format(humidity, target_lower, outdoor_abs_humidity, abs_humidity) if humidity > target_upper and outdoor_abs_humidity < abs_humidity: wanted_ventilation = 2 reason = '{0:.2f} > {1:.2f} and {2:.5f} < {3:.5f}'.format(humidity, target_lower, outdoor_abs_humidity, abs_humidity) # Second, prevent actual temperature from hitting the dew point - make sure we don't have condense if outdoor_abs_humidity < abs_humidity: if dew_point > temperature - offset: wanted_ventilation = 3 reason = '{0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - offset, temperature) elif dew_point > temperature - 2 * offset: wanted_ventilation = 2 reason = '{0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - 2 * offset, temperature) self._runtime_data[sensor_id]['candidate'] = wanted_ventilation current_ventilation = self._runtime_data[sensor_id]['ventilation'] if current_ventilation != wanted_ventilation: self._runtime_data[sensor_id]['trigger'] += 1 self._runtime_data[sensor_id]['reason'] = reason if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']: self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation self._runtime_data[sensor_id]['trigger'] = 0 current_ventilation = wanted_ventilation trigger_sensors[wanted_ventilation].append(sensor_id) else: self._runtime_data[sensor_id]['reason'] = '' self._runtime_data[sensor_id]['trigger'] = 0 ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation']) self._send_influxdb(tags={'id': sensor_id, 'name': self._sensors[sensor_id].replace(' ', '\ ')}, values={'dewpoint': float(dew_point), 'absolute\ humidity': float(abs_humidity), 'level': '{0}i'.format(current_ventilation)}) self._send_influxdb(tags={'id': outdoor_sensor_id, 'name': self._sensors[outdoor_sensor_id].replace(' ', '\ ')}, values={'dewpoint': float(outdoor_dew_point), 'absolute\ humidity': float(outdoor_abs_humidity), 'level': '0i'}) if ventilation != self._last_ventilation: if self._last_ventilation is None: self.logger('Updating ventilation to 1 (startup)') else: self.logger('Updating ventilation to {0} because of sensors: {1}'.format( ventilation, ', '.join(['{0} ({1})'.format(self._sensors[sensor_id], self._runtime_data[sensor_id]['reason']) for sensor_id in trigger_sensors[ventilation]]) )) self._set_ventilation(ventilation) self._last_ventilation = ventilation except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error calculating ventilation: {0}'.format(ex)) def _process_statistics(self): try: # Fetch data humidities = json.loads(self.webinterface.get_sensor_humidity_status(None)) if humidities['success'] is True: for sensor_id in range(len(humidities['status'])): if sensor_id not in self._samples: continue value = humidities['status'][sensor_id] if value == 255: continue self._samples[sensor_id].append(value) if len(self._samples[sensor_id]) > self._settings.get('samples', 1440): self._samples[sensor_id].pop(0) # Calculate required ventilation based on sensor information ventilation = 1 trigger_sensors = {1: [], 2: [], 3: []} for sensor_id in self._samples: if sensor_id not in self._runtime_data: self._runtime_data[sensor_id] = {'trigger': 0, 'ventilation': 1, 'candidate': 1, 'difference': '', 'name': self._sensors[sensor_id], 'stats': [0, 0, 0]} current = self._samples[sensor_id][-1] mean = Ventilation._mean(self._samples[sensor_id]) stddev = Ventilation._stddev(self._samples[sensor_id]) level_2 = mean + 2 * stddev level_3 = mean + 3 * stddev self._runtime_data[sensor_id]['stats'] = [current, level_2, level_3] if current > level_3: wanted_ventilation = 3 difference = '{0:.2f} > {1:.2f}'.format(current, level_3) elif current > level_2: wanted_ventilation = 2 difference = '{0:.2f} > {1:.2f}'.format(current, level_2) else: wanted_ventilation = 1 difference = '{0:.2f} <= {1:.2f}'.format(current, level_2) self._runtime_data[sensor_id]['candidate'] = wanted_ventilation current_ventilation = self._runtime_data[sensor_id]['ventilation'] if current_ventilation != wanted_ventilation: self._runtime_data[sensor_id]['trigger'] += 1 self._runtime_data[sensor_id]['difference'] = difference if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']: self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation self._runtime_data[sensor_id]['trigger'] = 0 current_ventilation = wanted_ventilation trigger_sensors[wanted_ventilation].append(sensor_id) else: self._runtime_data[sensor_id]['difference'] = '' self._runtime_data[sensor_id]['trigger'] = 0 ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation']) self._send_influxdb(tags={'id': sensor_id, 'name': self._sensors[sensor_id].replace(' ', '\ ')}, values={'medium': float(level_2), 'high': float(level_3), 'mean': float(mean), 'stddev': float(stddev), 'samples': '{0}i'.format(len(self._samples[sensor_id])), 'level': '{0}i'.format(current_ventilation)}) if ventilation != self._last_ventilation: if self._last_ventilation is None: self.logger('Updating ventilation to 1 (startup)') else: self.logger('Updating ventilation to {0} because of sensors: {1}'.format( ventilation, ', '.join(['{0} ({1})'.format(self._sensors[sensor_id], self._runtime_data[sensor_id]['difference']) for sensor_id in trigger_sensors[ventilation]]) )) self._set_ventilation(ventilation) self._last_ventilation = ventilation except CommunicationTimedOutException: self.logger('Error getting sensor status: CommunicationTimedOutException') except Exception as ex: self.logger('Error calculating ventilation: {0}'.format(ex)) def _set_ventilation(self, level): success = True for setting in self._outputs[level]: output_id = int(setting['output_id']) value = setting['value'] on = value > 0 if on is False: value = None result = json.loads(self.webinterface.set_output(None, output_id, is_on=str(on), dimmer=value, timer=None)) if result['success'] is False: self.logger('Error setting output {0} to {1}: {2}'.format(output_id, value, result['msg'])) success = False if success is True: self.logger('Ventilation set to {0}'.format(level)) else: self.logger('Could not set ventilation to {0}'.format(level)) success = False return success def _send_influxdb(self, tags, values): if self._has_influxdb is True: try: response = requests.get(url='https://127.0.0.1/plugins/InfluxDB/send_data', params={'token': 'None', 'key': 'ventilation', 'tags': json.dumps(tags), 'value': json.dumps(values)}, verify=False) if response.status_code == 200: result = response.json() if result['success'] is False: self.logger('Error sending data to InfluxDB plugin: {0}'.format(result['error'])) else: self.logger('Error sending data to InfluxDB plugin: {0}'.format(response.status_code)) except Exception as ex: self.logger('Got unexpected error while sending data to InfluxDB plugin: {0}'.format(ex)) @staticmethod def _abs_humidity(temperature, humidity): """ Calculate the absolute humidity (kg/m3) based on temperature and relative humidity. Formula was taken from http://www.aprweather.com/pages/calc.htm and should be good-enough for this purpose """ dew_point = Ventilation._dew_point(temperature, humidity) return ((6.11 * 10.0 ** (7.5 * dew_point / (237.7 + dew_point))) * 100) / ((temperature + 273.16) * 461.5) @staticmethod def _dew_point(temperature, humidity): """ Calculates the dew point for a given temperature and humidity """ a = 17.27 b = 237.7 def gamma(_temperature, _humidity): return ((a * _temperature) / (b + _temperature)) + math.log(_humidity / 100.0) return (b * gamma(temperature, humidity)) / (a - gamma(temperature, humidity)) @staticmethod def _mean(entries): """ Calculates mean """ if len(entries) > 0: return sum(entries) * 1.0 / len(entries) return 0 @staticmethod def _stddev(entries): """ Calculates standard deviation """ mean = Ventilation._mean(entries) variance = map(lambda e: (e - mean) ** 2, entries) return sqrt(Ventilation._mean(variance)) @om_expose def get_debug(self): return json.dumps({'runtime_data': self._runtime_data, 'ventilation': self._last_ventilation}, indent=4) @om_expose def get_config_description(self): return json.dumps(Ventilation.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class InfluxDB(OMPluginBase): """ An InfluxDB plugin, for sending statistics to InfluxDB """ name = 'InfluxDB' version = '2.0.56' interfaces = [('config', '1.0')] config_description = [{ 'name': 'url', 'type': 'str', 'description': 'The enpoint for the InfluxDB using HTTP. E.g. http://1.2.3.4:8086' }, { 'name': 'username', 'type': 'str', 'description': 'Optional username for InfluxDB authentication.' }, { 'name': 'password', 'type': 'str', 'description': 'Optional password for InfluxDB authentication.' }, { 'name': 'database', 'type': 'str', 'description': 'The InfluxDB database name to witch statistics need to be send.' }, { 'name': 'add_custom_tag', 'type': 'str', 'description': 'Add custom tag to statistics' }, { 'name': 'batch_size', 'type': 'int', 'description': 'The maximum batch size of grouped metrics to be send to InfluxDB.' }] default_config = {'url': '', 'database': 'openmotics'} def __init__(self, webinterface, logger): super(InfluxDB, self).__init__(webinterface, logger) self.logger('Starting InfluxDB plugin...') self._config = self.read_config(InfluxDB.default_config) self._config_checker = PluginConfigChecker(InfluxDB.config_description) self._pending_metrics = {} self._send_queue = deque() self._batch_sizes = [] self._queue_sizes = [] self._stats_time = 0 self._send_thread = Thread(target=self._sender) self._send_thread.setName('InfluxDB batch sender') self._send_thread.daemon = True self._send_thread.start() self._read_config() self.logger("Started InfluxDB plugin") def _read_config(self): self._url = self._config['url'] self._database = self._config['database'] self._batch_size = self._config.get('batch_size', 10) username = self._config.get('username', '') password = self._config.get('password', '') self._auth = None if username == '' else (username, password) self._add_custom_tag = self._config.get('add_custom_tag', '') self._endpoint = '{0}/write?db={1}'.format(self._url, self._database) self._query_endpoint = '{0}/query?db={1}&epoch=ns'.format( self._url, self._database) self._headers = {'X-Requested-With': 'OpenMotics plugin: InfluxDB'} self._enabled = self._url != '' and self._database != '' self.logger('InfluxDB is {0}'.format( 'enabled' if self._enabled else 'disabled')) @om_metric_receive(interval=10) def _receive_metric_data(self, metric): """ All metrics are collected, as filtering is done more finegraded when mapping to tables > example_metric = {"source": "OpenMotics", > "type": "energy", > "timestamp": 1497677091, > "tags": {"device": "OpenMotics energy ID1", > "id": 0}, > "values": {"power": 1234, > "power_counter": 1234567}} """ try: if self._enabled is False: return values = metric['values'] _values = {} for key in values.keys()[:]: value = values[key] if isinstance(value, basestring): value = '"{0}"'.format(value) if isinstance(value, bool): value = str(value) if isinstance(value, int): value = '{0}i'.format(value) _values[key] = value tags = {'source': metric['source'].lower()} if self._add_custom_tag: tags['custom_tag'] = self._add_custom_tag for tag, tvalue in metric['tags'].iteritems(): if isinstance(tvalue, basestring): tags[tag] = tvalue.replace(' ', '\ ').replace(',', '\,') else: tags[tag] = tvalue entry = self._build_entry(metric['type'], tags, _values, metric['timestamp'] * 1000000000) self._send_queue.appendleft(entry) except Exception as ex: self.logger('Error receiving metrics: {0}'.format(ex)) @staticmethod def _build_entry(key, tags, value, timestamp): if isinstance(value, dict): values = ','.join('{0}={1}'.format(vname, vvalue) for vname, vvalue in value.iteritems()) else: values = 'value={0}'.format(value) return '{0},{1} {2}{3}'.format( key, ','.join('{0}={1}'.format(tname, tvalue) for tname, tvalue in tags.iteritems()), values, '' if timestamp is None else ' {:.0f}'.format(timestamp)) def _sender(self): while True: try: data = [] try: while True: data.append(self._send_queue.pop()) if len(data) == self._batch_size: raise IndexError() except IndexError: pass if len(data) > 0: self._batch_sizes.append(len(data)) self._queue_sizes.append(len(self._send_queue)) response = requests.post(url=self._endpoint, data='\n'.join(data), headers=self._headers, auth=self._auth, verify=False) if response.status_code != 204: self.logger('Send failed, received: {0} ({1})'.format( response.text, response.status_code)) if self._stats_time < time.time() - 1800: self._stats_time = time.time() self.logger( 'Queue size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max' .format( min(self._queue_sizes), sum(self._queue_sizes) / float(len(self._queue_sizes)), max(self._queue_sizes))) self.logger( 'Batch size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max' .format( min(self._batch_sizes), sum(self._batch_sizes) / float(len(self._batch_sizes)), max(self._batch_sizes))) self._batch_sizes = [] self._queue_sizes = [] except Exception as ex: self.logger('Error sending from queue: {0}'.format(ex)) time.sleep(0.1) @om_expose def get_config_description(self): return json.dumps(InfluxDB.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})
class Hue(OMPluginBase): name = 'Hue' version = '1.0.0' interfaces = [('config', '1.0')] config_description = [{'name': 'api_url', 'type': 'str', 'description': 'The API URL of the Hue Bridge device. E.g. http://192.168.1.2/api'}, {'name': 'username', 'type': 'str', 'description': 'Hue Bridge generated username.'}, {'name': 'poll_frequency', 'type': 'int', 'description': 'The frequency used to pull the status of all lights from the Hue bridge in seconds (0 means never)'}, {'name': 'output_mapping', 'type': 'section', 'description': 'Mapping between OpenMotics Virtual Outputs/Dimmers and Hue Outputs', 'repeat': True, 'min': 0, 'content': [{'name': 'output_id', 'type': 'int'}, {'name': 'hue_output_id', 'type': 'int'}]}] default_config = {'api_url': 'http://hue/api', 'username': '', 'poll_frequency': 60} def __init__(self, webinterface, logger): super(Hue, self).__init__(webinterface, logger) self.logger('Starting Hue plugin...') self._config = self.read_config(Hue.default_config) self._config_checker = PluginConfigChecker(Hue.config_description) self._read_config() self._previous_output_state = {} self.logger("Hue plugin started") def _read_config(self): self._api_url = self._config['api_url'] self._output_mapping = self._config.get('output_mapping', []) self._output = self._create_output_object() self._hue = self._create_hue_object() self._username = self._config['username'] self._poll_frequency = self._config['poll_frequency'] self._endpoint = '{0}/{1}/{{0}}'.format(self._api_url, self._username) self._enabled = self._api_url != '' and self._username != '' self.logger('Hue is {0}'.format('enabled' if self._enabled else 'disabled')) def _create_output_object(self): # create an object with the OM output IDs as the keys and hue light IDs as the values output_object = {} for entry in self._output_mapping: output_object[entry['output_id']] = entry['hue_output_id'] return output_object def _create_hue_object(self): # create an object with the hue light IDs as the keys and OM output IDs as the values hue_object = {} for entry in self._output_mapping: hue_object[entry['hue_output_id']] = entry['output_id'] return hue_object @output_status def output_status(self, status): if self._enabled is True: try: current_output_state = {} for (output_id, dimmer_level) in status: hue_light_id = self._output.get(output_id) if hue_light_id is not None: key = '{0}_{1}'.format(output_id, hue_light_id) current_output_state[key] = dimmer_level previous_dimmer_level = self._previous_output_state.get(key, 0) if dimmer_level != previous_dimmer_level: self.logger('Dimming light {0} from {1} to {2}%'.format(key, previous_dimmer_level, dimmer_level)) thread = Thread(target=self._send, args=(hue_light_id, True, dimmer_level)) thread.start() else: self.logger('Light {0} unchanged at {1}%'.format(key, dimmer_level)) else: self.logger('Ignoring light {0}, because it is not a Hue light'.format(output_id)) for previous_key in self._previous_output_state.keys(): (output_id, hue_light_id) = previous_key.split('_') if current_output_state.get(previous_key) is None: self.logger('Switching light {0} OFF'.format(previous_key)) thread = Thread(target=self._send, args=(hue_light_id, False, self._previous_output_state.get(previous_key, 0))) thread.start() else: self.logger('Light {0} was already on'.format(previous_key)) self._previous_output_state = current_output_state except Exception as ex: self.logger('Error processing output_status event: {0}'.format(ex)) def _send(self, hue_light_id, state, dimmer_level): try: old_state = self._getLightState(hue_light_id) brightness = self._dimmerLevelToBrightness(dimmer_level) if old_state != False: if old_state['state'].get('on', False): if state: # light was on in Hue and is still on in OM -> send brightness command to Hue self._setLightState(hue_light_id, {'bri': brightness}) else: # light was on in Hue and is now off in OM -> send off command to Hue self._setLightState(hue_light_id, {'on': False}) else: if state: old_dimmer_level = self._brightnessToDimmerLevel(old_state['state']['bri']) if old_dimmer_level == dimmer_level: # light was off in Hue and is now on in OM with same dimmer level -> switch on command to Hue self._setLightState(hue_light_id, {'on': True}) else: # light was off in Hue and is now on in OM with different dimmer level -> switch on command to Hue and set brightness brightness = self._dimmerLevelToBrightness(dimmer_level) self._setLightState(hue_light_id, {'on': True, 'bri': brightness}) else: self.logger('Unable to read current state for Hue light {0}'.format(hue_light_id)) # sleep to avoid queueing the commands on the Hue bridge # time.sleep(1) except Exception as ex: self.logger('Error sending command to Hue light {0}: {1}'.format(hue_light_id, ex)) def _getLightState(self, hue_light_id): try: start = time.time() response = requests.get(url=self._endpoint.format('lights/{0}').format(hue_light_id)) if response.status_code is 200: hue_light = response.json() self.logger('Getting light state for Hue light {0} took {1}s'.format(hue_light_id, round(time.time() - start, 2))) return hue_light else: self.logger('Failed to pull state for light {0}'.format(hue_light_id)) return False except Exception as ex: self.logger('Error while getting light state for Hue light {0}: {1}'.format(hue_light_id, ex)) def _setLightState(self, hue_light_id, state): try: start = time.time() response = requests.put(url=self._endpoint.format('lights/{0}/state').format(hue_light_id), data=json.dumps(state)) if response.status_code is 200: result = response.json() if result[0].get('success')is None: self.logger('Setting light state for Hue light {0} returned unexpected result. Response: {1} ({2})'.format(hue_light_id, response.text, response.status_code)) return False self.logger('Setting light state for Hue light {0} took {1}s'.format(hue_light_id, round(time.time() - start, 2))) return True else: self.logger('Setting light state for Hue light {0} failed. Response: {1} ({2})'.format(response.text, response.status_code)) return False except Exception as ex: self.logger('Error while setting light state for Hue light {0} to {1}: {2}'.format(hue_light_id, json.dumps(state), ex)) def _getAllLightsState(self): self.logger('Pulling state for all lights from the Hue bridge') try: response = requests.get(url=self._endpoint.format('lights')) if response.status_code is 200: hue_lights = response.json() for output in self._output_mapping: output_id = output['output_id'] hue_light_id = str(output['hue_output_id']) hue_light = self._parseLightObject(hue_light_id, hue_lights[hue_light_id]) if hue_light.get('on', False): result = json.loads(self.webinterface.set_output(None, str(output_id), 'true', str(hue_light['dimmer_level']))) else: result = json.loads(self.webinterface.set_output(None, str(output_id), 'false')) if result['success'] is False: self.logger('--> Error when updating output {0}: {1}'.format(output_id, result['msg'])) else: self.logger('--> Failed to pull state for all lights') except Exception as ex: self.logger('--> Error while getting state for all Hue lights: {0}'.format(ex)) def _parseLightObject(self, hue_light_id, hue_light_object): try: light = {'id': hue_light_id, 'name': hue_light_object['name'], 'on': hue_light_object['state'].get('on', False), 'brightness': hue_light_object['state'].get('bri', 254)} light['dimmer_level'] = self._brightnessToDimmerLevel(light['brightness']) except Exception as ex: self.logger('--> Error while parsing Hue light {0}: {1}'.format(hue_light_object, ex)) return light def _brightnessToDimmerLevel(self, brightness): return int(round(brightness / 2.54)) def _dimmerLevelToBrightness(self, dimmer_level): return int(round(dimmer_level * 2.54)) @background_task def run(self): if self._enabled: while self._poll_frequency > 0: start = time.time() self._getAllLightsState() # This loop will run approx. every 'poll_frequency' seconds sleep = self._poll_frequency - (time.time() - start) if sleep < 0: sleep = 1 time.sleep(sleep) @om_expose def get_config_description(self): return json.dumps(Hue.config_description) @om_expose def get_config(self): return json.dumps(self._config) @om_expose def set_config(self, config): config = json.loads(config) for key in config: if isinstance(config[key], basestring): config[key] = str(config[key]) self._config_checker.check_config(config) self._config = config self._read_config() self.write_config(config) return json.dumps({'success': True})