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 __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._latitude = None self._longitude = None self._group_actions = {} self._bits = {} self._last_request_date = None self._execution_plan = {} self._sleeper = Event() self._sleep_until = 0 thread = Thread(target=self._sleep_manager) thread.start() self._read_config() self.logger("Started Astro plugin")
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 __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 __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)
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 __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 __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 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 __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 __init__(self, webinterface, logger): super(RTI, self).__init__(webinterface, logger) self.logger('Starting RTI plugin...') self._config = self.read_config(RTI.default_config) self._config_checker = PluginConfigChecker(RTI.config_description) self._command_queue = Queue() self._enabled = False self._serial = None self._read_config() self.logger("Started RTI plugin")
def __init__(self, webinterface, logger): super(HealthboxPlugin, self).__init__(webinterface, logger) self.__config = self.read_config(HealthboxPlugin.default_config) self.__config_checker = PluginConfigChecker(HealthboxPlugin.config_descr) self._enabled = True self.api_handler = ApiHandler(self.logger) self.discovered_devices = {} # dict of all the Healthbox3 drivers mapped with register key as key self.serial_key_to_gateway_id = {} # mapping of register key to gateway id (for api calls) self.logger("Started Healthbox 3 plugin") self.healtbox_manager = HealthBox3Manager() self.healtbox_manager.set_discovery_callback(self.discover_callback) self.healtbox_manager.start_discovery() # roomID is used as a placeholder for the room number, this is replaced through _define_sensors_with_rooms function self.sensorsGeneral = [ { 'sensor_id' :'roomID - indoor temperature[roomID]_HealthBox 3[Healthbox3] - temperature', 'sensor_name' :'Temperature Room roomID', 'physical_quantity':'temperature', 'unit' :'celcius', }, { 'sensor_id' :'roomID - indoor relative humidity[roomID]_HealthBox 3[Healthbox3] - humidity', 'sensor_name' :'Humidity Room roomID', 'physical_quantity':'humidity', 'unit' :'percent', }, { 'sensor_id' :'roomID - indoor air quality[roomID]_HealthBox 3[Healthbox3] - co2', 'sensor_name' :'CO2 Room roomID', 'physical_quantity':'co2', 'unit' :'parts_per_million', }, { 'sensor_id' :'roomID - indoor CO2[roomID]_HealthBox 3[Healthbox3] - concentration', 'sensor_name' :'CO2 Room roomID', 'physical_quantity':'co2', 'unit' :'parts_per_million', }, { 'sensor_id' :'roomID - indoor volatile organic compounds[roomID]_HealthBox 3[Healthbox3] - concentration', 'sensor_name' :'VOC Room roomID', 'physical_quantity':'voc', 'unit' :'parts_per_million', }, ]
def test_constructor_int(self): """ Test for the constructor for int. """ PluginConfigChecker([{ 'name': 'port', 'type': 'int', 'description': 'Port on the server.' }]) PluginConfigChecker([{'name': 'port', 'type': 'int'}]) try: PluginConfigChecker([{'type': 'int'}]) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('name' in str(exception))
def test_constructor_str(self): """ Test for the constructor for str. """ PluginConfigChecker([{ 'name': 'hostname', 'type': 'str', 'description': 'The hostname of the server.' }]) PluginConfigChecker([{'name': 'hostname', 'type': 'str'}]) try: PluginConfigChecker([{'type': 'str'}]) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('name' in str(exception))
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._read_config() self.logger("Started Astro plugin")
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")
def test_constructor_password(self): """ Test for the constructor for bool. """ PluginConfigChecker([{ 'name': 'password', 'type': 'password', 'description': 'A password.' }]) PluginConfigChecker([{'name': 'password', 'type': 'password'}]) try: PluginConfigChecker([{'type': 'password'}]) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('name' in str(exception))
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 __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 __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 __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")
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 __init__(self, webinterface, logger): super(SMAWebConnect, self).__init__(webinterface, logger) self.logger('Starting SMAWebConnect plugin...') self._config = self.read_config(SMAWebConnect.default_config) self._config_checker = PluginConfigChecker(SMAWebConnect.config_description) self._metrics_queue = deque() self._enabled = False self._sample_rate = 30 self._sma_devices = {} self._sma_sid = {} 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 test_constructor_bool(self): """ Test for the constructor for bool. """ PluginConfigChecker([{ 'name': 'use_auth', 'type': 'bool', 'description': 'Use authentication while connecting.' }]) PluginConfigChecker([{'name': 'use_auth', 'type': 'bool'}]) try: PluginConfigChecker([{'type': 'bool'}]) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('name' in str(exception))
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 __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 __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 __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 __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 test_constructor_error(self): """ Test with an invalid data type """ try: PluginConfigChecker({'test': 123}) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('list' in str(exception)) try: PluginConfigChecker([{'test': 123}]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('name' in str(exception)) try: PluginConfigChecker([{'name': 123}]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('name' in str(exception) and 'string' in str(exception)) try: PluginConfigChecker([{'name': 'test'}]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('type' in str(exception)) try: PluginConfigChecker([{'name': 'test', 'type': 123}]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('type' in str(exception) and 'string' in str(exception)) try: PluginConfigChecker([{'name': 'test', 'type': 'something_else'}]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('type' in str(exception) and 'something_else' in str(exception)) try: PluginConfigChecker([{ 'name': 'test', 'type': 'str', 'description': [] }]) self.fail("Expected PluginException") except PluginException as exception: self.assertTrue('description' in str(exception) and 'string' in str(exception))
def test_constructor_enum(self): """ Test for the constructor for enum. """ PluginConfigChecker([{ 'name': 'enumtest', 'type': 'enum', 'description': 'Test for enum', 'choices': ['First', 'Second'] }]) PluginConfigChecker([{ 'name': 'enumtest', 'type': 'enum', 'choices': ['First', 'Second'] }]) try: PluginConfigChecker([{ 'name': 'enumtest', 'type': 'enum', 'choices': 'First' }]) self.fail('Excepted exception') except PluginException as exception: self.assertTrue('choices' in str(exception) and 'list' in str(exception))
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._load_sensors() self.logger("Started Ventilation plugin")
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 __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")
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 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 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 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 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 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 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 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 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 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 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 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 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})