示例#1
0
    def test_check_config_section(self):
        """ Test check_config for section. """
        checker = PluginConfigChecker([{
            'name':
            'outputs',
            'type':
            'section',
            'repeat':
            True,
            'min':
            1,
            'content': [{
                'name': 'output',
                'type': 'int'
            }]
        }])

        checker.check_config({'outputs': []})
        checker.check_config({'outputs': [{'output': 2}]})
        checker.check_config({'outputs': [{'output': 2}, {'output': 4}]})

        try:
            checker.check_config({'outputs': 'test'})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('list' in str(exception))

        try:
            checker.check_config({'outputs': [{'test': 123}]})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('section' in str(exception)
                            and 'output' in str(exception))
示例#2
0
    def test_check_config_int(self):
        """ Test check_config for int. """
        checker = PluginConfigChecker([{'name': 'port', 'type': 'int'}])
        checker.check_config({'port': 123})

        try:
            checker.check_config({'port': "123"})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('int' in str(exception))
示例#3
0
    def test_check_config_bool(self):
        """ Test check_config for bool. """
        checker = PluginConfigChecker([{'name': 'use_auth', 'type': 'bool'}])
        checker.check_config({'use_auth': True})

        try:
            checker.check_config({'use_auth': 234543})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('bool' in str(exception))
示例#4
0
    def test_check_config_str(self):
        """ Test check_config for str. """
        checker = PluginConfigChecker([{'name': 'hostname', 'type': 'str'}])
        checker.check_config({'hostname': 'cloud.openmotics.com'})

        try:
            checker.check_config({'hostname': 123})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('str' in str(exception))
示例#5
0
    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})
示例#6
0
    def test_check_config_password(self):
        """ Test check_config for bool. """
        checker = PluginConfigChecker([{
            'name': 'password',
            'type': 'password'
        }])
        checker.check_config({'password': '******'})

        try:
            checker.check_config({'password': 123})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('str' in str(exception))
示例#7
0
    def test_check_config_error(self):
        """ Test check_config with an invalid data type """
        checker = PluginConfigChecker([{'name': 'hostname', 'type': 'str'}])

        try:
            checker.check_config('string')
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('dict' in str(exception))

        try:
            checker.check_config({})
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('hostname' in str(exception))
示例#8
0
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})
示例#9
0
class OpenWeatherMap(OMPluginBase):
    """
    An OpenWeatherMap plugin
    """

    name = 'OpenWeatherMap'
    version = '1.0.1'
    interfaces = [('config', '1.0')]

    config_description = [{'name': 'api_key',
                           'type': 'str',
                           'description': 'The API key from OpenWeatherMap.'},
                          {'name': 'lat',
                           'type': 'str',
                           'description': 'A location latitude which will be passed to OpenWeatherMap.'},
                          {'name': 'lng',
                           'type': 'str',
                           'description': 'A location longitude which will be passed to OpenWeatherMap.'},
                          {'name': 'main_mapping',
                           'type': 'section',
                           'description': 'Mapping betweet OpenMotics Virtual Sensors and OpenWeatherMap forecasts. See README.',
                           'repeat': True,
                           'min': 0,
                           'content': [{'name': 'sensor_id', 'type': 'int'},
                                       {'name': 'time_offset', 'type': 'int'}]},
                          {'name': 'uv_sensor_id',
                           'type': 'int',
                           'description': 'Sensor ID for storing the UV index (the UV index will be set as temperature). -1 if not needed.'}]

    default_config = {'api_key': ''}

    def __init__(self, webinterface, logger):
        super(OpenWeatherMap, self).__init__(webinterface, logger)
        self.logger('Starting OpenWeatherMap plugin...')

        self._config = self.read_config(OpenWeatherMap.default_config)
        self._config_checker = PluginConfigChecker(OpenWeatherMap.config_description)

        self._read_config()

        self._previous_output_state = {}

        self.logger("Started OpenWeatherMap plugin")

    def _read_config(self):
        self._api_key = self._config.get('api_key', '')

        main_mapping = self._config.get('main_mapping', [])
        self._current_mapping = [entry for entry in main_mapping if entry['time_offset'] == 0]
        self._forecast_mapping = [entry for entry in main_mapping if entry['time_offset'] > 0]
        self._uv_sensor_id = int(self._config.get('uv_sensor_id', -1))

        self._uv_endpoint = 'http://api.openweathermap.org/v3/uvi/{lat},{lon}/{date}Z.json?appid={api_key}'
        self._forecast_endpoint = 'http://api.openweathermap.org/data/2.5/forecast?lat={lat}&lon={lon}&units=metric&appid={api_key}'
        self._current_endpoint = 'http://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&units=metric&appid={api_key}'

        self._headers = {'X-Requested-With': 'OpenMotics plugin: OpenWeatherMap'}

        self._enabled = False
        if (self._config.get('lat', '') != '') and (self._config.get('lng', '') != ''):
            self._latitude = self._config.get('lat')
            self._longitude = self._config.get('lng')
            self.logger('Latitude: {0} - Longitude: {1}'.format(self._latitude, self._longitude))
            self._enabled = True

        self._enabled = self._enabled and self._api_key != ''
        self.logger('OpenWeatherMap is {0}'.format('enabled' if self._enabled else 'disabled'))

    @background_task
    def run(self):
        previous_values = {}
        accuracy = 5
        while True:
            if self._enabled:
                start = time.time()
                sensor_values = {}
                calls = 0
                if len(self._forecast_mapping) > 0:
                    try:
                        calls += 1
                        response = requests.get(url=self._forecast_endpoint.format(lat=self._latitude,
                                                                                   lon=self._longitude,
                                                                                   api_key=self._api_key),
                                                headers=self._headers)
                        if response.status_code != 200:
                            self.logger('Forecast call failed: {0}'.format(response.json()['message']))
                        else:
                            result = response.json()['list']
                            for sensor in self._forecast_mapping:
                                sensor_id = sensor['sensor_id']
                                wanted_time = start + (sensor['time_offset'] * 60)
                                selected_entry = None
                                for entry in result:
                                    if selected_entry is None or abs(entry['dt'] - wanted_time) < abs(selected_entry['dt'] - wanted_time):
                                        selected_entry = entry
                                if selected_entry is None:
                                    self.logger('Could not find forecast for virtual sensor {0}'.format(sensor_id))
                                    continue
                                sensor_values[sensor_id] = [selected_entry['main']['temp'], selected_entry['main']['humidity'], None]
                    except Exception as ex:
                        self.logger('Error while fetching forecast temperatures: {0}'.format(ex))
                if len(self._current_mapping) > 0:
                    try:
                        calls += 1
                        response = requests.get(url=self._current_endpoint.format(lat=self._latitude,
                                                                                  lon=self._longitude,
                                                                                  api_key=self._api_key),
                                                headers=self._headers)
                        if response.status_code != 200:
                            self.logger('Current weather call failed: {0}'.format(response.json()['message']))
                        else:
                            result = response.json()
                            for sensor in self._current_mapping:
                                sensor_id = sensor['sensor_id']
                                sensor_values[sensor_id] = [result['main']['temp'], result['main']['humidity'], None]
                    except Exception as ex:
                        self.logger('Error while fetching current temperatures: {0}'.format(ex))
                if 0 <= self._uv_sensor_id <= 31:
                    try:
                        execute = True
                        while execute is True:
                            calls += 1
                            lat, lon = round(self._latitude, accuracy), round(self._longitude, accuracy)
                            if accuracy == 0:
                                lat, lon = int(self._latitude), int(self._longitude)
                            response = requests.get(url=self._uv_endpoint.format(lat=lat, lon=lon,
                                                                                 date=time.strftime('%Y-%m-%d'),
                                                                                 api_key=self._api_key),
                                                    headers=self._headers)
                            result = response.json()
                            if response.status_code != 200:
                                self.logger('UV index call failed: {0}'.format(result['message']))
                                if result['message'] == 'not found':
                                    if accuracy > 0:
                                        accuracy = accuracy - 1
                                        execute = True
                                    else:
                                        execute = False
                                else:
                                    execute = False
                            else:
                                sensor_values[self._uv_sensor_id] = [result['data'], None, None]
                                execute = False
                    except Exception as ex:
                        self.logger('Error while fetching UV index: {0}'.format(ex))
                # Push all sensor data
                try:
                    for sensor_id, values in sensor_values.items():
                        if values != previous_values.get(sensor_id, []):
                            self.logger('Updating sensor {0} to temp: {1}, humidity: {2}'.format(sensor_id,
                                                                                                 values[0] if values[0] is not None else '-',
                                                                                                 values[1] if values[1] is not None else '-'))
                        previous_values[sensor_id] = values
                        result = json.loads(self.webinterface.set_virtual_sensor(None, sensor_id, *values))
                        if result['success'] is False:
                            self.logger('Error when updating virtual sensor {0}: {1}'.format(sensor_id, result['msg']))
                except Exception as ex:
                    self.logger('Error while setting virtual sensors: {0}'.format(ex))
                # Wait a given amount of seconds
                sleep = 60 * calls - (time.time() - start) + 1
                if sleep < 0:
                    sleep = 1
                time.sleep(sleep)
            else:
                time.sleep(5)

    @om_expose
    def get_config_description(self):
        return json.dumps(OpenWeatherMap.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})
示例#10
0
文件: main.py 项目: pvanlaet/plugins
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})
示例#11
0
文件: main.py 项目: wash34000/plugins
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})
示例#12
0
文件: main.py 项目: wash34000/plugins
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})
示例#13
0
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})
示例#14
0
class SMAWebConnect(OMPluginBase):
    """
    Reads out an SMA inverter using WebConnect
    """

    name = 'SMAWebConnect'
    version = '0.0.36'
    interfaces = [('config', '1.0'), ('metrics', '1.0')]

    counter_device_types = ['gas', 'heat', 'water', 'electricity']
    counter_unit_types = ['energy', 'volume', 'power', 'flow']
    types_mapping = {
        'energy': 'energy',
        'power': 'energy',
        'volume': 'volume',
        'flow': 'volume'
    }

    default_config = {}

    FIELD_MAPPING = {
        '6100_40263F00': {
            'name': 'grid_power',
            'description': 'Grid power',
            'unit': 'W',
            'type': 'gauge',
            'factor': 1.0
        },
        '6100_00465700': {
            'name': 'frequency',
            'description': 'Frequency',
            'unit': 'Hz',
            'type': 'gauge',
            'factor': 100.0
        },
        '6100_00464800': {
            'name': 'voltage_l1',
            'description': 'Voltage L1',
            'unit': 'V',
            'type': 'gauge',
            'factor': 100.0
        },
        '6100_00464900': {
            'name': 'voltage_l2',
            'description': 'Voltage L2',
            'unit': 'V',
            'type': 'gauge',
            'factor': 100.0
        },
        '6100_00464A00': {
            'name': 'voltage_l3',
            'description': 'Voltage L3',
            'unit': 'V',
            'type': 'gauge',
            'factor': 100.0
        },
        '6100_40465300': {
            'name': 'current_l1',
            'description': 'Current L1',
            'unit': 'A',
            'type': 'gauge',
            'factor': 1000.0
        },
        '6100_40465400': {
            'name': 'current_l2',
            'description': 'Current L2',
            'unit': 'A',
            'type': 'gauge',
            'factor': 1000.0
        },
        '6100_40465500': {
            'name': 'current_l3',
            'description': 'Current L3',
            'unit': 'A',
            'type': 'gauge',
            'factor': 1000.0
        },
        '6100_0046C200': {
            'name': 'pv_power',
            'description': 'PV power',
            'unit': 'W',
            'type': 'gauge',
            'factor': 1.0
        },
        '6380_40451F00': {
            'name': 'pv_voltage',
            'description': 'PV voltage (average of all PV channels)',
            'unit': 'V',
            'type': 'gauge',
            'factor': 100.0
        },
        '6380_40452100': {
            'name': 'pv_current',
            'description': 'PV current (average of all PV channels)',
            'unit': 'A',
            'type': 'gauge',
            'factor': 1000.0
        },
        '6400_0046C300': {
            'name': 'pv_gen_meter',
            'description': 'PV generation meter',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        },
        '6400_00260100': {
            'name': 'total_yield',
            'description': 'Total yield',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        },
        '6400_00262200': {
            'name': 'daily_yield',
            'description': 'Daily yield',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        },
        '6100_40463600': {
            'name': 'grid_power_supplied',
            'description': 'Grid power supplied',
            'unit': 'W',
            'type': 'gauge',
            'factor': 1.0
        },
        '6100_40463700': {
            'name': 'grid_power_absorbed',
            'description': 'Grid power absorbed',
            'unit': 'W',
            'type': 'gauge',
            'factor': 1.0
        },
        '6400_00462400': {
            'name': 'grid_total_yield',
            'description': 'Grid total yield',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        },
        '6400_00462500': {
            'name': 'grid_total_absorbed',
            'description': 'Grid total absorbed',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        },
        '6100_00543100': {
            'name': 'current_consumption',
            'description': 'Current consumption',
            'unit': 'W',
            'type': 'gauge',
            'factor': 1.0
        },
        '6400_00543A00': {
            'name': 'total_consumption',
            'description': 'Total consumption',
            'unit': 'Wh',
            'type': 'counter',
            'factor': 1.0
        }
    }

    metric_definitions = [{
        'type':
        'sma',
        'tags': ['device'],
        'metrics': [{
            'name': 'online',
            'description': 'Indicates if the SMA device is operating',
            'type': 'gauge',
            'unit': 'Boolean'
        }] + [{
            'name': entry['name'],
            'description': entry['description'],
            'unit': entry['unit'],
            'type': entry['type']
        } for entry in FIELD_MAPPING.values()]
    }]

    def __init__(self, webinterface, logger):
        super(SMAWebConnect, self).__init__(webinterface, logger)
        self.logger('Starting SMAWebConnect plugin...')
        self.config_description = self._create_config_description()
        self._config = self.read_config(SMAWebConnect.default_config)
        self._config_checker = PluginConfigChecker(self.config_description)
        self._metrics_queue = deque()
        self._enabled = False
        self._sample_rate = 30
        self._sma_devices = {}
        self._sma_sid = {}
        self._counter_rate_to_total = {}
        self._read_config()

        # Disable HTTPS warnings becasue of self-signed HTTPS certificate on the SMA inverter
        from requests.packages.urllib3.exceptions import InsecureRequestWarning
        requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

        self.logger("Started SMAWebConnect plugin")

    def _create_config_description(self):
        self.pc_config = json.loads(
            self.webinterface.get_pulse_counter_configurations())
        pc_names = [
            pc_config['name'] for pc_config in self.pc_config['config']
            if pc_config['name']
        ]
        config_description = [{
            'name':
            'sample_rate',
            'type':
            'int',
            'description':
            'How frequent (every x seconds) to fetch the sensor data, Default: 30'
        }, {
            'name':
            'debug',
            'type':
            'bool',
            'description':
            'Indicate whether debug logging should be enabled'
        }, {
            'name':
            'devices',
            'type':
            'section',
            'description':
            'List of all SMA devices.',
            'repeat':
            True,
            'min':
            1,
            'content': [{
                'name':
                'sma_inverter_ip',
                'type':
                'str',
                'description':
                'IP or hostname of the SMA inverter including the scheme (e.g. http:// or https://).'
            }, {
                'name': 'password',
                'type': 'str',
                'description': 'The password of the `User` account'
            }, {
                'name':
                'counter_mapping',
                'type':
                'section',
                'description':
                'Counter mapping',
                'repeat':
                True,
                'min':
                0,
                'content': [
                    {
                        'name':
                        'name',
                        'type':
                        'enum',
                        'description':
                        'Name of the pulse counter',
                        'choices': [
                            v['name']
                            for v in SMAWebConnect.FIELD_MAPPING.values()
                        ]
                    },
                    {
                        'name': 'pulsecounter_name',
                        'type': 'enum',
                        'description':
                        'Name of the counter to which the selected value needs to be mapped',
                        'choices': pc_names
                    },
                    {
                        'name': 'unit_type',
                        'type': 'enum',
                        'description': 'Unit type of measurement',
                        'choices': SMAWebConnect.counter_unit_types
                    },
                    {
                        'name': 'convert_to_counter',
                        'type': 'enum',
                        'description':
                        'The read value needs to be made into a counter (i.e. cumulative)',
                        'choices': ['NO', 'YES']
                    },
                    {
                        'name':
                        'multiplier',
                        'type':
                        'str',
                        'description':
                        'Multiplier for readout of values (default=1; required to ensure units of'
                        'kW(h) or m3(/h) for values given to pulse counters)'
                    },
                ]
            }]
        }]

        return config_description

    def _read_config(self):
        self._enabled = False
        self._sample_rate = int(self._config.get('sample_rate', 30))
        self._sma_devices = self._config.get('devices', [])
        self._debug = bool(self._config.get('debug', False))

        self._enabled = len(self._sma_devices) > 0 and self._sample_rate > 5
        self.logger('SMAWebConnect is {0}'.format(
            'enabled' if self._enabled else 'disabled'))

    def _log_debug(self, message):
        if self._debug:
            self.logger(message)

    @background_task
    def run(self):
        while True:
            if not self._enabled:
                time.sleep(5)
                continue
            for sma_device in self._sma_devices:
                try:
                    self._read_data(sma_device)
                except Exception as ex:
                    self.logger(
                        'Could not read SMA device values: {0}'.format(ex))
            time.sleep(self._sample_rate)

    def _read_data(self, sma_device):
        metrics_values = {}
        ip = sma_device['sma_inverter_ip']
        while True:
            sid = self._sma_sid.get(ip, '')
            endpoint = '{0}/dyn/getValues.json?sid={1}'.format(ip, sid)
            response = requests.post(endpoint,
                                     json={
                                         'destDev': [],
                                         'keys':
                                         SMAWebConnect.FIELD_MAPPING.keys()
                                     },
                                     timeout=10,
                                     verify=False).json()
            if response.get('err') == 401:
                self._login(sma_device)
                continue
            break
        if 'result' not in response or len(response['result']) != 1:
            raise RuntimeError('Unexpected response: {0}'.format(response))
        serial = response['result'].keys()[0]
        data = response['result'][serial]
        if data is None:
            raise RuntimeError('Unexpected response: {0}'.format(response))
        self._log_debug('Read values (ip: {0}, serial number: {1}):'.format(
            ip, serial))
        for key, info in SMAWebConnect.FIELD_MAPPING.items():
            name = info['name']
            unit = info['unit']
            if key in data:
                values = self._extract_values(key, data[key], info['factor'])
                if len(values) == 0:
                    self._log_debug('* {0}: No values'.format(name))
                elif len(values) == 1:
                    value = values[0]
                    self._log_debug('* {0}: {1}{2}'.format(
                        name, value, unit if value is not None else ''))
                    if value is not None:
                        metrics_values[name] = value
                else:
                    self._log_debug('* {0}:'.format(name))
                    for value in values:
                        self._log_debug('** {0}{1}'.format(
                            value, unit if value is not None else ''))
                    values = [value for value in values if value is not None]
                    if len(values) == 1:
                        metrics_values[name] = values[0]
                    elif len(values) > 1:
                        metrics_values[name] = sum(values) / len(values)
            else:
                self._log_debug('* Missing key: {0}'.format(key))
        for key in data:
            if key not in SMAWebConnect.FIELD_MAPPING.keys():
                self._log_debug('* Unknown key {0}: {1}'.format(
                    key, data[key]))
        offline = 'frequency' not in metrics_values or metrics_values[
            'frequency'] is None
        metrics_values['online'] = not offline
        self._enqueue_metrics(serial, metrics_values)
        self._update_pulsecounter(sma_device, metrics_values)

    def _extract_values(self, key, values, factor):
        if len(values) != 1 or '1' not in values:
            self.logger('* Unexpected structure for {0}: {1}'.format(
                key, values))
            return []
        values = values['1']
        if len(values) == 0:
            return []
        if len(values) == 1:
            return [self._clean_value(key, values[0], factor)]
        return_data = []
        for raw_value in values:
            value = self._clean_value(key, raw_value, factor)
            if value is not None:
                return_data.append(value)
        return return_data

    def _clean_value(self, key, value_container, factor):
        if 'val' not in value_container:
            self.logger('* Unexpected structure for {0}: {1}'.format(
                key, value_container))
            return None
        value = value_container['val']
        if value is None:
            return None
        return float(value) / factor

    def _login(self, sma_device):
        ip = sma_device['sma_inverter_ip']
        endpoint = '{0}/dyn/login.json'.format(ip)
        response = requests.post(endpoint,
                                 json={
                                     'right': 'usr',
                                     'pass': sma_device['password']
                                 },
                                 verify=False).json()
        if 'result' in response and 'sid' in response['result']:
            self._sma_sid[ip] = response['result']['sid']
        else:
            error_code = response.get('err', 'unknown')
            if error_code == 503:
                raise RuntimeError('Maximum amount of sessions')
            raise RuntimeError('Could not login: {0}'.format(error_code))

    def _enqueue_metrics(self, device_id, values):
        try:
            now = time.time()
            self._metrics_queue.appendleft({
                'type': 'sma',
                'timestamp': now,
                'tags': {
                    'device': device_id
                },
                'values': values
            })
        except Exception as ex:
            self.logger(
                'Got unexpected error while enqueueing metrics: {0}'.format(
                    ex))

    @om_metric_data(interval=15)
    def collect_metrics(self):
        try:
            while True:
                yield self._metrics_queue.pop()
        except IndexError:
            pass

    def _update_pulsecounter(self, device_config, values):
        """Add the values defined in counter_mapping not only to influxdb as metrics, but also create and update
        pulse counters for them. If configured, convert the measurement into a counter, i.e. make it cumulative."""
        for mapping in device_config.get('counter_mapping', []):
            value_name = str(mapping['name'])
            unit_type = SMAWebConnect.types_mapping.get(
                mapping.get('unit_type'))
            current_value = values[value_name] * float(
                mapping.get('multiplier', 1))
            pc_name = str(mapping['pulsecounter_name'])
            self.pc_config = json.loads(
                self.webinterface.get_pulse_counter_configurations())
            pc_id = [
                pc_config['id'] for pc_config in self.pc_config['config']
                if pc_config['name'] == pc_name
            ]
            if len(pc_id) == 0 or len(pc_id) > 1:
                self.logger(
                    "No or multiple pulsecounters found for name {0}. Skipping this pulsecounter"
                    .format(pc_name))
                continue
            pc_id = pc_id[0]

            if not self._counter_rate_to_total.get(pc_name):
                self._counter_rate_to_total[pc_name] = 0.0

            # If the measured value is a power measurement, it needs to be converted to energy.
            # Assume the value you are getting is in kW and needs to be converted to a kWh counter
            if mapping.get('unit_type') == 'power' or mapping.get(
                    'unit_type') == 'flow':
                self._log_debug('* Converting {0} to {1}...'.format(
                    mapping.get('unit_type'), unit_type))
                current_value_amount = current_value * self._sample_rate / 3600  # convert kW or m3/h to kWh or m3
                # You need an internal counter, because a pulse counter only works with integers, while a kWh-value
                # over 1 minute is often lower than 1
                self._counter_rate_to_total[pc_name] += current_value_amount
                if self._counter_rate_to_total[pc_name] < 1.0:
                    self._log_debug(
                        '* {0} for counter {1} has not reached cumulative value of 1 yet (currently {2}) - '
                        'adding 0 to pulse counter'.format(
                            mapping.get('unit_type'), pc_name,
                            self._counter_rate_to_total[pc_name]))
                    current_value = 0
                else:
                    current_value = self._counter_rate_to_total[pc_name]
                    self._counter_rate_to_total[
                        pc_name] = self._counter_rate_to_total[pc_name] - 1.0

            # If the measured values still need to be made cumulative
            if mapping.get('convert_to_counter', 'NO') == 'YES':
                result = json.loads(
                    self.webinterface.get_pulse_counter_status())
                previous_value = result['counters'][pc_id] if result[
                    'counters'][pc_id] else 0
                value = int(current_value + previous_value)
            else:
                value = int(current_value)

            self._log_debug(
                '* Processing ... Setting counter {0} with name {1} to {2}'.
                format(pc_id, pc_name, value))
            result = json.loads(
                self.webinterface.set_pulse_counter_status(
                    pulse_counter_id=pc_id, value=value))
            if result['success'] is False:
                self._log_debug(
                    '* Could not update counter value for {0} (ID {1}): {2}'.
                    format(pc_name, pc_id, result.get('msg')))

    @om_expose
    def get_config_description(self):
        return json.dumps(self.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self.write_config(config)
        self._config = config
        self._read_config()
        return json.dumps({'success': True})
示例#15
0
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})
示例#16
0
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})
示例#17
0
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})
示例#18
0
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})
示例#19
0
文件: main.py 项目: wash34000/plugins
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})
示例#20
0
文件: main.py 项目: wash34000/plugins
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})
示例#21
0
class Hue(OMPluginBase):

    name = 'Hue'
    version = '1.1.2'
    interfaces = [('config', '1.0')]

    config_description = [{
        'name':
        'api_url',
        'type':
        'str',
        'description':
        'The API URL of the Hue Bridge device. E.g. http://192.168.1.2/api'
    }, {
        'name': 'username',
        'type': 'str',
        'description': 'Hue Bridge generated username.'
    }, {
        'name':
        'poll_frequency',
        'type':
        'int',
        'description':
        'The frequency used to pull the status of all outputs from the Hue bridge in seconds (0 means never)'
    }, {
        'name':
        'output_mapping',
        'type':
        'section',
        'description':
        'Mapping between OpenMotics Virtual Outputs/Dimmers and Hue Outputs',
        'repeat':
        True,
        'min':
        0,
        'content': [{
            'name': 'output_id',
            'type': 'int'
        }, {
            'name': 'hue_output_id',
            'type': 'int'
        }]
    }]

    default_config = {
        'api_url': 'http://hue/api',
        'username': '',
        'poll_frequency': 60
    }

    def __init__(self, webinterface, gateway_logger):
        self.setup_logging(log_function=gateway_logger)
        super(Hue, self).__init__(webinterface, logger)
        logger.info('Starting Hue plugin %s ...', self.version)

        self.discover_hue_bridges()

        self._config = self.read_config(Hue.default_config)
        self._config_checker = PluginConfigChecker(Hue.config_description)

        self._read_config()

        self._io_lock = Lock()
        self._output_event_queue = Queue(maxsize=256)

        logger.info("Hue plugin started")

    @staticmethod
    def setup_logging(log_function):  # type: (Callable) -> None
        logger.setLevel(logging.INFO)
        log_handler = PluginLogHandler(log_function=log_function)
        # some elements like time and name are added by the plugin runtime already
        # formatter = logging.Formatter('%(asctime)s - %(name)s - %(threadName)s - %(levelname)s - %(message)s')
        formatter = logging.Formatter(
            '%(threadName)s - %(levelname)s - %(message)s')
        log_handler.setFormatter(formatter)
        logger.addHandler(log_handler)

    def _read_config(self):
        self._api_url = self._config['api_url']
        self._output_mapping = self._config.get('output_mapping', [])
        self._output = self._create_output_object()
        self._hue = self._create_hue_object()
        self._username = self._config['username']
        self._poll_frequency = self._config['poll_frequency']

        self._endpoint = '{0}/{1}/{{0}}'.format(self._api_url, self._username)

        self._enabled = self._api_url != '' and self._username != ''
        logger.info(
            'Hue is {0}'.format('enabled' if self._enabled else 'disabled'))

    def _create_output_object(self):
        # create an object with the OM output IDs as the keys and hue light IDs as the values
        output_object = {}
        for entry in self._output_mapping:
            output_object[entry['output_id']] = entry['hue_output_id']
        return output_object

    def _create_hue_object(self):
        # create an object with the hue light IDs as the keys and OM output IDs as the values
        hue_object = {}
        for entry in self._output_mapping:
            hue_object[entry['hue_output_id']] = entry['output_id']
        return hue_object

    @output_status(version=2)
    def output_status(self, output_event):
        if self._enabled is True:
            try:
                output_id = output_event['id']
                state = output_event['status'].get('on')
                dimmer_level = output_event['status'].get('value')
                hue_light_id = self._output.get(output_id)
                if hue_light_id is not None:
                    logger.info(
                        'Switching output %s (hue id: %s) %s (dimmer: %s)',
                        output_id, hue_light_id, 'ON' if state else 'OFF',
                        dimmer_level)
                    data = (hue_light_id, state, dimmer_level)
                    self._output_event_queue.put(data)
                else:
                    logger.debug(
                        'Ignoring output %s, because it is not Hue'.format(
                            output_id))
            except Exception as ex:
                logger.exception(
                    'Error processing output_status event: {0}'.format(ex))

    def _send(self, hue_light_id, state, dimmer_level):
        try:
            state = {'on': state}
            if dimmer_level is not None:
                state.update(
                    {'bri': self._dimmerLevelToBrightness(dimmer_level)})
            self._setLightState(hue_light_id, state)
        except Exception as ex:
            logger.exception('Error sending command to output with hue id: %s',
                             hue_light_id)

    def _getLightState(self, hue_light_id):
        try:
            start = time.time()
            response = requests.get(
                url=self._endpoint.format('lights/{0}').format(hue_light_id))
            if response.status_code is 200:
                hue_light = response.json()
                logger.info('Getting output state for hue id: %s took %ss',
                            hue_light_id, round(time.time() - start, 2))
                return hue_light
            else:
                logger.warning('Failed to pull state for hue id: %s',
                               hue_light_id)
                return False
        except Exception as ex:
            logger.exception('Error while getting output state for hue id: %s',
                             hue_light_id)

    def _setLightState(self, hue_light_id, state):
        try:
            start = time.time()
            response = requests.put(url=self._endpoint.format(
                'lights/{0}/state').format(hue_light_id),
                                    data=json.dumps(state))
            if response.status_code is 200:
                result = response.json()
                if result[0].get('success') is None:
                    logger.info(
                        'Setting output state for Hue output {0} returned unexpected result. Response: {1} ({2})'
                        .format(hue_light_id, response.text,
                                response.status_code))
                    return False
                logger.info('Setting output state for hue id: %s took %ss',
                            hue_light_id, round(time.time() - start, 2))
                return True
            else:
                logger.error(
                    'Setting output state for hue id: %s failed. Response: %s (%s)',
                    hue_light_id, response.text, response.status_code)
                return False
        except Exception as ex:
            logger.exception(
                'Error while setting output state for hue id: %s to %s',
                hue_light_id, json.dumps(state))

    def import_remote_state(self):
        if not self._output_event_queue.empty():
            logger.info(
                'Ignoring syncing remote state because we still need to process %s output events',
                self._output_event_queue.qsize())
        else:
            try:
                self._import_lights_state()
            except Exception as ex:
                logger.exception(
                    'Error while getting state for all Hue outputs')
            try:
                self._import_sensors_state()
            except Exception as ex:
                logger.exception(
                    'Error while getting state for all Hue sensors')

    def _import_lights_state(self):
        logger.debug(
            'Syncing remote state for all outputs from the Hue bridge')
        hue_lights = self._getAllLightsState()
        for output in self._output_mapping:
            output_id = output['output_id']
            hue_light_id = str(output['hue_output_id'])
            hue_light_state = hue_lights.get(hue_light_id)
            if hue_light_state is not None:
                if hue_light_state.get('on', False):
                    result = json.loads(
                        self.webinterface.set_output(
                            None, str(output_id), 'true',
                            str(hue_light_state['dimmer_level'])))
                else:
                    result = json.loads(
                        self.webinterface.set_output(None, str(output_id),
                                                     'false'))
                if result['success'] is False:
                    logger.error(
                        'Error when updating output %s (hue id: %s): %s',
                        output_id, hue_light_id, result['msg'])
            else:
                logger.warning(
                    'Output %s (hue id:  %s) not found on Hue bridge',
                    output_id, hue_light_id)

    def _import_sensors_state(self):
        logger.debug(
            'Syncing remote state for all sensors from the Hue bridge')
        known_sensors = self._get_known_sensors()
        hue_sensors = self._getAllSensorsState()

        for hue_sensor_id, sensor in hue_sensors.items():
            sensor_external_id = sensor['external_id']
            if sensor_external_id not in known_sensors.keys():
                name = 'Hue Sensor {}'.format(hue_sensor_id)
                om_sensor_id = self._register_sensor(name, sensor_external_id)
            else:
                om_sensor_id = known_sensors[sensor_external_id]
            value = float(sensor.get('value'))
            if om_sensor_id is not None:
                self._update_sensor(om_sensor_id, value)
            else:
                logger.error('Hue sensor %s (%s) not found', hue_sensor_id,
                             sensor_external_id)

    def _get_known_sensors(self):
        response = self.webinterface.get_sensor_configurations()
        data = json.loads(response)
        return {
            x['external_id']: x['id']
            for x in data['config']
            if x.get('source', {}).get('name') == Hue.name
            and x['external_id'] not in [None, '']
        }

    def _getAllSensorsState(self):
        hue_sensors = {}
        response = requests.get(url=self._endpoint.format('sensors'))
        if response.status_code is 200:
            for hue_sensor_id, data in response.json().items():
                if data.get('type') == 'ZLLTemperature':
                    hue_sensors[hue_sensor_id] = self._parseSensorObject(
                        hue_sensor_id, data, sensor_type='temperature')
        else:
            logger.error('Failed to pull state for all sensors (HTTP %s)',
                         response.status_code)
        return hue_sensors

    def _getAllLightsState(self):
        hue_lights = {}
        response = requests.get(url=self._endpoint.format('lights'))
        if response.status_code is 200:
            for hue_light_id, data in response.json().items():
                hue_lights[hue_light_id] = self._parseLightObject(
                    hue_light_id, data)
        else:
            logger.error('Failed to pull state for all outputs (HTTP %s)',
                         response.status_code)
        return hue_lights

    def _parseLightObject(self, hue_light_id, hue_light_object):
        light = {'id': hue_light_id}
        try:
            light.update({
                'name':
                hue_light_object['name'],
                'on':
                hue_light_object['state'].get('on', False),
                'brightness':
                hue_light_object['state'].get('bri', 254)
            })
            light['dimmer_level'] = self._brightnessToDimmerLevel(
                light['brightness'])
        except Exception as ex:
            logger.exception('Error while parsing Hue light %s',
                             hue_light_object)
        return light

    def _parseSensorObject(self,
                           hue_sensor_id,
                           hue_sensor_object,
                           sensor_type='temperature'):
        sensor = {'id': hue_sensor_id}
        try:
            value = hue_sensor_object['state'][sensor_type]
            if sensor_type == 'temperature':
                value /= 100.0
            sensor.update({
                'external_id': hue_sensor_object['uniqueid'],
                'name': hue_sensor_object['name'],
                'type': hue_sensor_object['type'],
                'value': value
            })

        except Exception as ex:
            logger.exception('Error while parsing Hue sensor %s',
                             hue_sensor_object)
        return sensor

    def _brightnessToDimmerLevel(self, brightness):
        return int(round(brightness / 2.54))

    def _dimmerLevelToBrightness(self, dimmer_level):
        return int(round(dimmer_level * 2.54))

    @background_task
    def run(self):
        if self._enabled:
            event_processor = Thread(target=self.output_event_processor,
                                     name='output_event_processor')
            event_processor.start()
            self.log_remote_asset_list()
            self.start_state_poller()

    def log_remote_asset_list(self):
        hue_lights = self._getAllLightsState()
        for hue_id, hue_light in hue_lights.items():
            logger.info('Discovered hue output %s (hue id: %s)',
                        hue_light.get('name'), hue_id)
        hue_sensors = self._getAllSensorsState()
        for hue_id, hue_sensor in hue_sensors.items():
            logger.info('Discovered hue sensor %s (hue id: %s)',
                        hue_sensor.get('name'), hue_id)

    def start_state_poller(self):
        while self._poll_frequency > 0:
            start = time.time()
            self.import_remote_state()
            # This loop will run approx. every 'poll_frequency' seconds
            sleep = self._poll_frequency - (time.time() - start)
            if sleep < 0:
                sleep = 1
            self.sleep(sleep)

    @om_expose
    def get_config_description(self):
        return json.dumps(Hue.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})

    def output_event_processor(self):
        while self._enabled:
            try:
                _latest_value_buffer = {}
                while True:
                    try:
                        hue_light_id, status, dimmer = self._output_event_queue.get(
                            block=True, timeout=1)
                        _latest_value_buffer[hue_light_id] = (
                            status, dimmer
                        )  # this will ensure only the latest value is taken
                    except Empty:
                        break
                for hue_light_id, (status,
                                   dimmer) in _latest_value_buffer.items():
                    self._send(hue_light_id, status, dimmer)
                    self.sleep(
                        0.1
                    )  # "throttle" requests to the bridge to avoid overloading
            except Exception as ex:
                if 'maintenance_mode' in ex.message:
                    logger.warning(
                        'System in maintenance mode. Processing paused for 1 minute.'
                    )
                    self.sleep(60)
                else:
                    logger.exception(
                        'Unexpected error processing output events')
                    self.sleep(10)

    def sleep(self, timer):
        now = time.time()
        expired = time.time() - now > timer
        while self._enabled and not expired:
            time.sleep(min(timer, 0.5))
            expired = time.time() - now > timer

            def _getLightState(self, hue_light_id):
                try:
                    start = time.time()
                    response = requests.get(url=self._endpoint.format(
                        'lights/{0}').format(hue_light_id))
                    if response.status_code is 200:
                        hue_light = response.json()
                        logger.info(
                            'Getting output state for hue id: %s took %ss',
                            hue_light_id, round(time.time() - start, 2))
                        return hue_light
                    else:
                        logger.warning('Failed to pull state for hue id: %s',
                                       hue_light_id)
                        return False
                except Exception as ex:
                    logger.exception(
                        'Error while getting output state for hue id: %s',
                        hue_light_id)

    def discover_hue_bridges(self):
        try:
            response = requests.get(url='https://discovery.meethue.com/')
            if response.status_code is 200:
                hue_bridge_data = response.json()
                for hue_bridge in hue_bridge_data:
                    logger.info('Discovered hue bridge %s @ %s',
                                hue_bridge.get('id'),
                                hue_bridge.get('internalipaddress'))
            else:
                logger.warning('Failed to discover bridges on this network')
                return False
        except Exception as ex:
            logger.exception(
                'Error while discovering hue bridges on this network')

    def _register_sensor(self, name, external_id):
        logger.debug('Registering sensor with name %s and external_id %s',
                     name, external_id)
        try:
            config = {
                'name': name,
            }
            response = self.webinterface.sensor.register(
                external_id=external_id,
                physical_quantity='temperature',
                unit='celcius',
                config=config)
            return response.id
        except Exception as e:
            logger.warning(
                'Failed registering sensor with name %s and external_id %s with exception %s',
                name, external_id, str(e.message))
            return None

    def _update_sensor(self, sensor_id, value):
        logger.debug('Updating sensor %s with status %s', sensor_id, value)
        response = self.webinterface.sensor.set_status(sensor_id=sensor_id,
                                                       value=value)
        if response is None:
            logger.warning('Could not set the updated sensor value')
            return False
        return True
示例#22
0
class MQTTClient(OMPluginBase):
    """
    An MQTT client plugin for sending/receiving data to/from an MQTT broker.
    For more info: https://github.com/openmotics/plugins/blob/master/mqtt-client/README.md
    """

    name = 'MQTTClient'
    version = '3.0.1'
    interfaces = [('config', '1.0')]

    energy_module_config = {
        1: 8,
        8: 8,
        12: 12
    }

    config_description = [
        {'name': 'hostname',
         'type': 'str',
         'description': 'MQTT broker hostname or IP address.'},
        {'name': 'port',
         'type': 'int',
         'description': 'MQTT broker port. Default: 1883'},
        {'name': 'username',
         'type': 'str',
         'description': 'MQTT broker username. Default: openmotics'},
        {'name': 'password',
         'type': 'password',
         'description': 'MQTT broker password'},
        # input status
        {'name': 'input_status_enabled',
         'type': 'bool',
         'description': 'Enable input status publishing of messages.'},
        {'name': 'input_status_topic_format',
         'type': 'str',
         'description': 'Input status topic format. Default: openmotics/input/{id}/state'},
        {'name': 'input_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Input status message quality of service. Default: 0'},
        {'name': 'input_status_retain',
         'type': 'bool',
         'description': 'Input status message retain.'},
        # output status
        {'name': 'output_status_enabled',
         'type': 'bool',
         'description': 'Enable output status publishing of messages.'},
        {'name': 'output_status_topic_format',
         'type': 'str',
         'description': 'Output status topic format. Default: openmotics/output/{id}/state'},
        {'name': 'output_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Output status message quality of service. Default: 0'},
        {'name': 'output_status_retain',
         'type': 'bool',
         'description': 'Output status message retain.'},
        # event status
        {'name': 'event_status_enabled',
         'type': 'bool',
         'description': 'Enable event status publishing of messages.'},
        {'name': 'event_status_topic_format',
         'type': 'str',
         'description': 'Event status topic format. Default: openmotics/event/{id}/state'},
        {'name': 'event_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Event status message quality of service. Default: 0'},
        {'name': 'event_status_retain',
         'type': 'bool',
         'description': 'Event status message retain.'},
        # sensor status
        {'name': 'sensor_status_enabled',
         'type': 'bool',
         'description': 'Enable sensor status publishing of messages.'},
        {'name': 'sensor_status_topic_format',
         'type': 'str',
         'description': 'Sensor status topic format. Default: openmotics/sensor/{id}/state'},
        {'name': 'sensor_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Sensor status message quality of service. Default: 0'},
        {'name': 'sensor_status_retain',
         'type': 'bool',
         'description': 'Sensor status message retain.'},
        {'name': 'sensor_status_poll_frequency',
         'type': 'int',
         'description': 'Polling frequency for sensor status in seconds. Default: 300, minimum: 10'},
        # power status
        {'name': 'power_status_enabled',
         'type': 'bool',
         'description': 'Enable power status publishing of messages.'},
        {'name': 'power_status_topic_format',
         'type': 'str',
         'description': 'Power status topic format. Default: openmotics/power/{module_id}/{sensor_id}/state'},
        {'name': 'power_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Power status quality of Service. Default: 0'},
        {'name': 'power_status_retain',
         'type': 'bool',
         'description': 'Power status retain.'},
        {'name': 'power_status_poll_frequency',
        'type': 'int',
        'description': 'Polling frequency for power status in seconds. Default: 60, minimum: 10'},
        # energy status
        {'name': 'energy_status_enabled',
         'type': 'bool',
         'description': 'Enable energy status publishing of messages.'},
        {'name': 'energy_status_topic_format',
         'type': 'str',
         'description': 'Energy status topic format. Default: openmotics/energy/{module_id}/{sensor_id}/state'},
        {'name': 'energy_status_qos',
         'type': 'enum',
         'choices': ['0', '1', '2'],
         'description': 'Energy status quality of Service. Default: 0'},
        {'name': 'energy_status_retain',
         'type': 'bool',
         'description': 'Energy status retain.'},
        {'name': 'energy_status_poll_frequency',
        'type': 'int',
        'description': 'Polling frequency for energy status in seconds. Default: 3600 (1 hour), minimum: 10'},
        # output command
        {'name': 'output_command_topic',
         'type': 'str',
         'description': 'Topic to subscribe to for output command messages. Leave empty to turn off.'},
        # logging
        {'name': 'logging_topic',
         'type': 'str',
         'description': 'Topic for logging messages. Leave empty to turn off.'},
        # timestamp timezone
        {'name': 'timezone',
         'type': 'str',
         'description': 'Timezone. Default: UTC. Example: Europe/Brussels'}
    ]

    default_config = {
        'port': 1883,
        'username': '******',
        'input_status_topic_format': 'openmotics/input/{id}/state',
        'input_status_qos': 0,
        'output_status_topic_format': 'openmotics/output/{id}/state',
        'output_status_qos': 0,
        'event_status_topic_format': 'openmotics/event/{id}/state',
        'event_status_qos': 0,
        'sensor_status_topic_format': 'openmotics/sensor/{id}/state',
        'sensor_status_qos': 0,
        'sensor_status_poll_frequency': 300,
        'power_status_topic_format': 'openmotics/power/{module_id}/{sensor_id}/state',
        'power_status_qos': 0,
        'power_status_poll_frequency': 60,
        'energy_status_topic_format': 'openmotics/energy/{module_id}/{sensor_id}/state',
        'energy_status_qos': 0,
        'energy_status_poll_frequency': 3600,
        'output_command_topic': 'openmotics/output/+/set',
        'logging_topic': 'openmotics/logging',
        'timezone': 'UTC'
    }

    def __init__(self, webinterface, logger):
        super(MQTTClient, self).__init__(webinterface, logger)
        self.logger('Starting MQTTClient plugin...')

        self._config = self.read_config(MQTTClient.default_config)
        #self.logger("Default configuration '{0}'".format(self._config))
        self._config_checker = PluginConfigChecker(MQTTClient.config_description)

        paho_mqtt_wheel = '/opt/openmotics/python/plugins/MQTTClient/paho_mqtt-1.5.0-py2-none-any.whl'
        if paho_mqtt_wheel not in sys.path:
            sys.path.insert(0, paho_mqtt_wheel)

        self.client = None
        self._sensor_config = {}
        self._inputs = {}
        self._outputs = {}
        self._sensors = {}
        self._power_modules = {}

        self._read_config()
        self._try_connect()

        self._load_configuration()

        self.logger("Started MQTTClient plugin")

    def _read_config(self):
        # broker
        self._hostname = self._config.get('hostname')
        self._port     = self._config.get('port')
        self._username = self._config.get('username')
        self._password = self._config.get('password')
        # inputs
        self._input_enabled = self._config.get('input_status_enabled')
        self._input_topic   = self._config.get('input_status_topic_format')
        self._input_qos     = int(self._config.get('input_status_qos'))
        self._input_retain  = self._config.get('input_status_retain')
        # outputs
        self._output_enabled = self._config.get('output_status_enabled')
        self._output_topic   = self._config.get('output_status_topic_format')
        self._output_qos     = int(self._config.get('output_status_qos'))
        self._output_retain  = self._config.get('output_status_retain')
        # events
        self._event_enabled = self._config.get('event_status_enabled')
        self._event_topic   = self._config.get('event_status_topic_format')
        self._event_qos     = int(self._config.get('event_status_qos'))
        self._event_retain  = self._config.get('event_status_retain')
        # sensors
        self._sensor_config = {
            'sensor': {
                'enabled':        self._config.get('sensor_status_enabled'),
                'topic':          self._config.get('sensor_status_topic_format'),
                'qos':            int(self._config.get('sensor_status_qos')),
                'retain':         self._config.get('sensor_status_retain'),
                'poll_frequency': int(self._config.get('sensor_status_poll_frequency'))
            },
            'power': {
                'enabled':        self._config.get('power_status_enabled'),
                'topic':          self._config.get('power_status_topic_format'),
                'qos':            int(self._config.get('power_status_qos')),
                'retain':         self._config.get('power_status_retain'),
                'poll_frequency': int(self._config.get('power_status_poll_frequency'))
            },
            'energy': {
                'enabled':        self._config.get('energy_status_enabled'),
                'topic':          self._config.get('energy_status_topic_format'),
                'qos':            int(self._config.get('energy_status_qos')),
                'retain':         self._config.get('energy_status_retain'),
                'poll_frequency': int(self._config.get('energy_status_poll_frequency'))
            }
        }
        self._sensor_enabled = self._sensor_config.get('sensor').get('enabled')
        self._power_enabled = (self._sensor_config.get('power').get('enabled') or self._sensor_config.get('energy').get('enabled'))
        # output command
        self._output_command_topic = self._config.get('output_command_topic')
        # logging topic
        self._logging_topic = self._config.get('logging_topic')
        # timezone
        self._timezone = self._config.get('timezone')
        self._enabled = self._hostname is not None and self._port is not None
        self.logger('MQTTClient is {0}'.format('enabled' if self._enabled else 'disabled'))

    def _load_configuration(self):
        inputs_loaded  = False
        outputs_loaded = False
        sensors_loaded = False
        power_loaded   = False
        should_load = True

        while should_load:
            if not inputs_loaded:
                inputs_loaded = self._load_input_configuration()
            if not outputs_loaded:
                outputs_loaded = self._load_output_configuration()
            if not sensors_loaded:
                sensors_loaded = self._load_sensor_configuration()
            if not power_loaded:
                power_loaded = self._load_power_configuration()
            should_load = not all([inputs_loaded, outputs_loaded, sensors_loaded, power_loaded])
            if should_load:
                time.sleep(15)

    def _load_input_configuration(self):
        input_config_loaded = True
        if self._input_enabled:
            try:
                result = json.loads(self.webinterface.get_input_configurations())
                if result['success'] is False:
                    self.logger('Failed to load input configurations')
                    input_config_loaded = False
                else:
                    ids = []
                    for config in result['config']:
                        input_id = config['id']
                        ids.append(input_id)
                        self._inputs[input_id] = config
                    for input_id in self._inputs.keys():
                        if input_id not in ids:
                            del self._inputs[input_id]
                    self.logger('Configuring {0} inputs'.format(len(ids)))
            except Exception as ex:
                self.logger('Error while loading input configurations: {0}'.format(ex))
                input_config_loaded = False
            try:
                result = json.loads(self.webinterface.get_input_status())
                if result['success'] is False:
                    self.logger('Failed to get input status')
                    input_config_loaded = False
                else:
                    for input_data in result['status']:
                        input_id = input_data['id']
                        if input_id not in self._inputs:
                            continue
                        self._inputs[input_id]['status'] = input_data['status']
            except Exception as ex:
                self.logger('Error getting input status: {0}'.format(ex))
                input_config_loaded = False
        return input_config_loaded

    def _load_output_configuration(self):
        output_config_loaded = True
        if self._output_enabled:
            try:
                result = json.loads(self.webinterface.get_output_configurations())
                if result['success'] is False:
                    output_config_loaded = False
                    self.logger('Failed to load output configurations')
                else:
                    ids = []
                    for config in result['config']:
                        if config['module_type'] not in ['o', 'O', 'd', 'D']:
                            continue
                        output_id = config['id']
                        ids.append(output_id)
                        self._outputs[output_id] = {'name': config['name'],
                                                    'module_type': {'o': 'output',
                                                                    'O': 'output',
                                                                    'd': 'dimmer',
                                                                    'D': 'dimmer'}[config['module_type']],
                                                    'type': 'relay' if config['type'] == 0 else 'light'}
                    for output_id in self._outputs.keys():
                        if output_id not in ids:
                            del self._outputs[output_id]
                    self.logger('Configuring {0} outputs'.format(len(ids)))
            except Exception as ex:
                output_config_loaded = False
                self.logger('Error while loading output configurations: {0}'.format(ex))
            try:
                result = json.loads(self.webinterface.get_output_status())
                if result['success'] is False:
                    output_config_loaded = False
                    self.logger('Failed to get output status')
                else:
                    for output in result['status']:
                        output_id = output['id']
                        if output_id not in self._outputs:
                            continue
                        self._outputs[output_id]['status'] = output['status']
                        self._outputs[output_id]['dimmer'] = output['dimmer']
            except Exception as ex:
                output_config_loaded = False
                self.logger('Error getting output status: {0}'.format(ex))
        return output_config_loaded

    def _load_sensor_configuration(self):
        sensor_config_loaded = True
        if self._sensor_enabled:
            try:
                result = json.loads(self.webinterface.get_sensor_configurations())
                if result['success'] is False:
                    sensor_config_loaded = False
                    self.logger('Failed to load sensor configurations: {0}'.format(result.get('msg')))
                else:
                    ids = []
                    for config in result['config']:
                        sensor_id = config['id']
                        ids.append(sensor_id)
                        self._sensors[sensor_id] = {'name': config['name'],
                                                    'external_id': str(config['external_id']),
                                                    'physical_quantity': str(config['physical_quantity']),
                                                    'source': config.get('source'),
                                                    'unit': config.get('unit')}
                    for sensor_id in self._sensors.keys():
                        if sensor_id not in ids:
                            del self._sensors[sensor_id]
                    self.logger('Configuring {0} sensors'.format(len(ids)))
            except Exception as ex:
                sensor_config_loaded = False
                self.logger('Error while loading sensor configurations: {0}'.format(ex))
        return sensor_config_loaded

    def _load_power_configuration(self):
        power_config_loaded = True
        if self._power_enabled:
            try:
                result = json.loads(self.webinterface.get_power_modules())
                if result['success'] is False:
                    power_config_loaded = False
                    self.logger('Failed to load power configurations: {0}'.format(result.get('msg')))
                else:
                    ids = []
                    for module in result['modules']:
                        module_id = int(module['id'])
                        ids.append(module_id)
                        version = int(module['version'])
                        input_count = MQTTClient.energy_module_config.get(version, 0)
                        module_config = {}
                        if input_count == 0:
                            self.logger('Warning: Skipping energy module {0}, version {1} is currently not supported by this plugin. Only versions: {2}'.format(
                                module_id,
                                version,
                                ', '.join(MQTTClient.energy_module_config.keys())))
                            continue
                        else:
    	                    self.logger('Configuring energy module {0} (version {1}) with {2} inputs'.format(module_id, version, input_count))
                        for input_id in range(0, input_count):
                            module_config[input_id] = {'name':     module['input{0}'.format(input_id)],
                                                       'sensor':   module['sensor{0}'.format(input_id)],
                                                       'times':    module['times{0}'.format(input_id)],
                                                       'inverted': module['inverted{0}'.format(input_id)]}
                        self._power_modules[module_id] = module_config
                    for module_id in self._power_modules.keys():
                        if module_id not in ids:
                            del self._power_modules[module_id]
            except Exception as ex:
                power_config_loaded = False
                self.logger('Error while loading power configurations: {0}'.format(ex))
        return power_config_loaded

    def _try_connect(self):
        if self._enabled is True:
            try:
                import paho.mqtt.client as client
                self.client = client.Client()
                if self._username is not None:
                    self.logger("MQTTClient is using username '{0}' and password".format(self._username))
                    self.client.username_pw_set(self._username, self._password)
                self.client.on_message = self.on_message
                self.client.on_connect = self.on_connect
                self.client.connect(self._hostname, self._port, 5)
                self.client.loop_start()
            except Exception as ex:
                self.logger('Error connecting to MQTT broker: {0}'.format(ex))

    def _log(self, info):
        # for log messages QoS = 0 and retain = False
        thread = Thread(target=self._send, args=(self._logging_topic, info, 0, False))
        thread.start()

    def _send(self, topic, data, qos, retain):
        try:
            self.client.publish(topic, payload=json.dumps(data), qos=qos, retain=retain)
        except Exception as ex:
            self.logger('Error sending data to broker: {0}'.format(ex))

    def _timestamp2isoformat(self, timestamp=None):
        # start with UTC
        dt = datetime.utcnow()
        if (timestamp is not None):
            dt.utcfromtimestamp(float(timestamp))
        # localize the UTC date/time, make it "aware" instead of naive
        dt = pytz.timezone('UTC').localize(dt)
        # convert to timezone from configuration
        if self._timezone is not None and self._timezone is not 'UTC':
            dt = dt.astimezone(pytz.timezone(self._timezone))
        return dt.isoformat()

    @input_status(version=2)
    def input_status(self, data):
        if self._enabled and self._input_enabled:
            input_id = data.get('input_id')
            status = 'ON' if data.get('status') else 'OFF'
            try:
                if input_id in self._inputs:
                    name = self._inputs[input_id].get('name')
                    self._log('Input {0} ({1}) switched {2}'.format(input_id, name, status))
                    self.logger('Input {0} ({1}) switched {2}'.format(input_id, name,  status))
                    data = {'id': input_id,
                            'name': name,
                            'status': status,
                            'timestamp': self._timestamp2isoformat()}
                    thread = Thread(
                        target=self._send,
                        args=(self._input_topic.format(id=input_id), data, self._input_qos, self._input_retain)
                    )
                    thread.start()
                else:
                    self.logger('Got event for unknown input {0}'.format(input_id))
            except Exception as ex:
                self.logger('Error processing input {0}: {1}'.format(input_id, ex))

    @output_status
    def output_status(self, status):
        if self._enabled and self._output_enabled:
            try:
                new_output_status = {}
                for entry in status:
                    new_output_status[entry[0]] = entry[1]
                current_output_status = self._outputs
                for output_id in current_output_status:
                    status = current_output_status[output_id].get('status')
                    dimmer = current_output_status[output_id].get('dimmer')
                    name = current_output_status[output_id].get('name')
                    if status is None or dimmer is None:
                        continue
                    changed = False
                    if output_id in new_output_status:
                        if status != 1:
                            changed = True
                            current_output_status[output_id]['status'] = 1
                            self._log('Output {0} ({1}) changed to ON'.format(output_id, name))
                            self.logger('Output {0} ({1}) changed to ON'.format(output_id, name))
                        if dimmer != new_output_status[output_id]:
                            changed = True
                            current_output_status[output_id]['dimmer'] = new_output_status[output_id]
                            self._log('Output {0} ({1}) changed to level {2}'.format(output_id, name, new_output_status[output_id]))
                            self.logger('Output {0} ({1}) changed to level {2}'.format(output_id, name, new_output_status[output_id]))
                    elif status != 0:
                        changed = True
                        current_output_status[output_id]['status'] = 0
                        self._log('Output {0} ({1}) changed to OFF'.format(output_id, name))
                        self.logger('Output {0} ({1}) changed to OFF'.format(output_id, name))
                    if changed is True:
                        if current_output_status[output_id]['module_type'] == 'output':
                            level = 100
                        else:
                            level = dimmer
                        if current_output_status[output_id]['status'] == 0:
                            level = 0
                        data = {'id': output_id,
                                'name': name,
                                'value': level,
                                'timestamp': self._timestamp2isoformat()}
                        thread = Thread(
                            target=self._send,
                            args=(self._output_topic.format(id=output_id), data, self._output_qos, self._output_retain)
                        )
                        thread.start()
            except Exception as ex:
                self.logger('Error processing outputs: {0}'.format(ex))

    @receive_events
    def receive_events(self, event_id):
        if self._enabled and self._event_enabled:
            try:
                self._log('Got event {0}'.format(event_id))
                self.logger('Got event {0}'.format(event_id))
                data = {'id': event_id,
                        'timestamp': self._timestamp2isoformat()}
                thread = Thread(
                    target=self._send,
                    args=(self._event_topic.format(id=event_id), data, self._event_qos, self._event_retain)
                )
                thread.start()
            except Exception as ex:
                self.logger('Error processing event: {0}'.format(ex))

    @background_task
    def background_task_sensor_status(self):
        self._create_background_task(
            'sensor',
            self.webinterface.get_sensor_status,
            self._process_sensor_status
        )()

    @background_task
    def background_task_realtime_power(self):
        self._create_background_task(
            'power',
            self.webinterface.get_realtime_power,
            self._process_realtime_power
        )()

    @background_task
    def background_task_total_energy(self):
        self._create_background_task(
            'energy',
            self.webinterface.get_total_energy,
            self._process_total_energy
        )()

    def _process_sensor_status(self, sensor_config, json_data):
        mqtt_messages = []
        data_list = list(filter(None, json_data.get('status', [])))
        for sensor_id, sensor_value in enumerate(data_list):
            sensor = self._sensors.get(sensor_id)
            if sensor:
                sensor_data = {'id': sensor_id,
                               'source': sensor.get('source'),
                               'external_id': sensor.get('external_id'),
                               'physical_quantity': sensor.get('physical_quantity'),
                               'unit': sensor.get('unit'),
                               'name': sensor.get('name'),
                               'value': float(sensor_value),
                               'timestamp': self._timestamp2isoformat()}
                mqtt_messages.append({'topic': sensor_config.get('topic').format(id=sensor_id),
                                      'message': sensor_data})
        return mqtt_messages

    def _process_realtime_power(self, sensor_config, json_data):
        mqtt_messages = []
        json_data.pop('success')
        for module_id, values in json_data.items():
            module = self._power_modules.get(int(module_id))
            if module:
                for input_id, sensor_values in enumerate(values):
                    power_input = module.get(int(input_id))
                    if power_input:
                        sensor_data = {'sensor_id': input_id,
                                       'module_id': module_id,
                                       'name': power_input.get('name'),
                                       'voltage': sensor_values[0],
                                       'frequency': sensor_values[1],
                                       'current': sensor_values[2],
                                       'power': sensor_values[3],
                                       'timestamp': self._timestamp2isoformat()}
                        mqtt_messages.append({'topic': sensor_config.get('topic').format(module_id=module_id, sensor_id=input_id),
                                              'message': sensor_data })
        return mqtt_messages

    def _process_total_energy(self, sensor_config, json_data):
        mqtt_messages = []
        json_data.pop('success')
        for module_id, values in json_data.items():
            module = self._power_modules.get(int(module_id))
            if module:
                for input_id, sensor_values in enumerate(values):
                    power_input = module.get(int(input_id))
                    if power_input:
                        sensor_data = {'sensor_id': input_id,
                                       'module_id': module_id,
                                       'name': power_input.get('name'),
                                       'day': sensor_values[0],
                                       'night': sensor_values[1],
                                       'timestamp': self._timestamp2isoformat()}
                    mqtt_messages.append({'topic': sensor_config.get('topic').format(module_id=module_id, sensor_id=input_id),
                                          'message': sensor_data})
        return mqtt_messages

    def _create_background_task(self, sensor_type, data_retriever, data_processor):
        def background_function():
            while True:
                if self._enabled:
                    sensor_config = self._sensor_config.get(sensor_type)
                    frequency = sensor_config.get('poll_frequency')
                    self.logger('Background task to retrieve {0} sensor data started, will run every {1} seconds.'.format(sensor_type, frequency))
                    # highest frequency is every 10s
                    while frequency >= 10:
                        start = time.time()
                        try:
                            if sensor_config.get('enabled'):
                                result = json.loads(data_retriever())
                                if result['success'] is False:
                                    self.logger('Failed to load {0} sensor data: {1}'.format(sensor_type, result.get('msg')))
                                else:
                                    mqtt_messages = data_processor(sensor_config, result)
                                    for mqtt_message in mqtt_messages:
                                        thread = Thread(target=self._send,
                                                        args=(mqtt_message.get('topic'),
                                                              mqtt_message.get('message'),
                                                              sensor_config.get('qos'),
                                                              sensor_config.get('retain')))
                                        thread.start()
                        except Exception as ex:
                            self.logger('Error processing {0} sensor status: {1}'.format(sensor_type, ex))
                        # This loop will run approx. every 'frequency' seconds
                        sleep = frequency - (time.time() - start)
                        if sleep < 0:
                            sleep = 1
                        time.sleep(sleep)
                else:
                    time.sleep(15)
        return background_function

    def on_connect(self, client, userdata, flags, rc):
        if rc != 0:
            self.logger('Error connecting: rc={0}', rc)
            return

        self.logger('Connected to MQTT broker {0}:{1}'.format(self._hostname, self._port))
        # subscribe to output command topic if provided
        if self._output_command_topic:
            try:
                self.client.subscribe(self._output_command_topic)
                self.logger('Subscribed to {0}'.format(self._output_command_topic))
            except Exception as ex:
                self.logger('Could not subscribe: {0}'.format(ex))

    def on_message(self, client, userdata, msg):
        if self._output_command_topic:
            regexp = self._output_command_topic.replace('+', '(\d+)')
            if re.search(regexp, msg.topic) is not None:
                try:
                    # the output_id is the first match of the regular expression
                    output_id = int(re.findall(regexp, msg.topic)[0])
                    if output_id in self._outputs:
                        output = self._outputs[output_id]
                        value = int(msg.payload)
                        if value > 0:
                            is_on = 'true'
                        else:
                            is_on = 'false'
                        dimmer = None
                        if output['module_type'] == 'dimmer':
                            dimmer = None if value == 0 else max(0, min(100, value))
                            if value > 0:
                                log_value = 'ON ({0}%)'.format(value)
                        result = json.loads(self.webinterface.set_output(id=output_id, is_on=is_on, dimmer=dimmer))
                        if result['success'] is False:
                            log_message = 'Failed to set output {0} to {1}: {2}'.format(output_id, value, result.get('msg', 'Unknown error'))
                            self._log(log_message)
                            self.logger(log_message)
                        else:
                            log_message = 'Message for output {0} with payload {1}'.format(output_id, value)
                            self._log(log_message)
                            self.logger(log_message)
                    else:
                        self._log('Unknown output: {0}'.format(output_id))
                except Exception as ex:
                    self._log('Failed to process message: {0}'.format(ex))
            else:
                self._log('Message with topic {0} ignored'.format(msg.topic))
                self.logger('Message with topic {0} ignored'.format(msg.topic))

    @om_expose
    def get_config_description(self):
        return json.dumps(MQTTClient.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        try:
            config = json.loads(config)
            for key in config:
                if isinstance(config[key], six.string_types):
                    config[key] = str(config[key])
            self._config_checker.check_config(config)
            self._config = config
            self._read_config()
            self.write_config(config)
            if self._enabled:
                thread = Thread(target=self._load_configuration)
                thread.start()
        except Exception as ex:
            self.logger('Error saving configuration: {0}'.format(ex))

        self._try_connect()
        return json.dumps({'success': True})
示例#23
0
class TasmotaHTTP(OMPluginBase):
    """
    An Tasmota HTTP plugin
    """

    name = 'tasmotaHTTP'
    version = '1.0.1'
    interfaces = [('config', '1.0')]

    config_description = [{
        'name':
        'refresh_interval',
        'type':
        'int',
        'description':
        'Refresh interval (in seconds) to fetch values from outputs and push to tasmota devices'
    }, {
        'name':
        'tasmota_mapping',
        'type':
        'section',
        'description':
        'Mapping betweet OpenMotics Virtual Sensors and Tasmota devices. See README.',
        'repeat':
        True,
        'min':
        0,
        'content': [{
            'name':
            'label',
            'type':
            'str',
            'description':
            'Name to identify pair (ip_address, output_id)'
        }, {
            'name': 'ip_address',
            'type': 'str',
            'description': 'Device IP Address.'
        }, {
            'name':
            'username',
            'type':
            'str',
            'description':
            'Device username, fill only if authentication is enabled.'
        }, {
            'name':
            'password',
            'type':
            'password',
            'description':
            'Device password, fill only if authentication is enabled.'
        }, {
            'name': 'output_id',
            'type': 'int',
            'description': 'OpenMotics output id to sync with Tasmota'
        }]
    }]

    default_config = {'refresh_interval': 2}
    tasmota_http_endpoint = 'http://{ip_address}/cm?user={user}&password={password}&cmnd=Power%20{action}'

    def __init__(self, webinterface, logger):
        super(TasmotaHTTP, self).__init__(webinterface, logger)
        self.logger('Starting Tasmota HTTP plugin...')

        self._config = self.read_config(TasmotaHTTP.default_config)
        self._config_checker = PluginConfigChecker(
            TasmotaHTTP.config_description)

        self._read_config()

        self._previous_output_state = {}

        self.logger("Started Tasmota HTTP plugin")

    def _read_config(self):
        self._refresh_interval = self._config.get('refresh_interval', 5)

        tasmota_mapping = self._config.get('tasmota_mapping', [])
        self._tasmota_mapping = tasmota_mapping
        self._headers = {'X-Requested-With': 'OpenMotics plugin: Tasmota HTTP'}
        self._enabled = bool(self._tasmota_mapping)

        self.logger('Tasmota HTTP is {0}'.format(
            'enabled' if self._enabled else 'disabled'))

    @background_task
    def run(self):
        previous_values = {}
        while True:
            if self._enabled:
                try:
                    result = json.loads(self.webinterface.get_output_status())
                    if 'status' in result:
                        for device in self._tasmota_mapping:
                            if not isinstance(device['output_id'], int):
                                continue
                            device_output_id = device['output_id']

                            for output in result['status']:
                                output_id = output['id']
                                if output_id != device_output_id:
                                    continue
                                if device[
                                        'label'] in previous_values and previous_values[
                                            device['label']] == output[
                                                'status']:
                                    continue
                                previous_values[
                                    device['label']] = self.update_tasmota(
                                        device, output)
                                self.logger('Tasmota device {0} is {1}'.format(
                                    device['label'],
                                    'on' if output['status'] == 1 else 'off'))
                except Exception as ex:
                    self.logger('Failed to get output status: {0}'.format(ex))

                # Wait a given amount of seconds
                time.sleep(self._refresh_interval)
            else:
                time.sleep(5)

    @om_expose
    def get_config_description(self):
        return json.dumps(TasmotaHTTP.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})

    def update_tasmota(self, device, output):
        response = requests.get(url=self.tasmota_http_endpoint.format(
            ip_address=device['ip_address'],
            user=device['username'],
            password=device['password'],
            action=output['status']),
                                headers=self._headers)
        if response.status_code == 200:
            if response.json()['POWER'] == 'ON':
                return 1
        else:
            self.logger('Failed to update Tasmota device {0}: {1}'.format(
                device['ip_address'], response.status_code))

        return 0
示例#24
0
文件: main.py 项目: pvanlaet/plugins
class Ventilation(OMPluginBase):
    """
    A ventilation plugin, using statistical humidity or the dew point data to control the ventilation
    """

    name = 'Ventilation'
    version = '2.0.14'
    interfaces = [('config', '1.0'),
                  ('metrics', '1.0')]

    config_description = [{'name': 'low',
                           'type': 'section',
                           'description': 'Output configuration for "low" ventilation',
                           'repeat': True,
                           'min': 1,
                           'content': [{'name': 'output_id', 'type': 'int'},
                                       {'name': 'value', 'type': 'int'}]},
                          {'name': 'medium',
                           'type': 'section',
                           'description': 'Output configuration for "medium" ventilation',
                           'repeat': True,
                           'min': 1,
                           'content': [{'name': 'output_id', 'type': 'int'},
                                       {'name': 'value', 'type': 'int'}]},
                          {'name': 'high',
                           'type': 'section',
                           'description': 'Output configuration for "high" ventilation',
                           'repeat': True,
                           'min': 1,
                           'content': [{'name': 'output_id', 'type': 'int'},
                                       {'name': 'value', 'type': 'int'}]},
                          {'name': 'sensors',
                           'type': 'section',
                           'description': 'Sensors to use for ventilation control.',
                           'repeat': True,
                           'min': 1,
                           'content': [{'name': 'sensor_id', 'type': 'int'}]},
                          {'name': 'mode',
                           'type': 'nested_enum',
                           'description': 'The mode of the ventilation control',
                           'choices': [{'value': 'statistical', 'content': [{'name': 'samples',
                                                                             'type': 'int'},
                                                                            {'name': 'trigger',
                                                                             'type': 'int'}]},
                                       {'value': 'dew_point', 'content': [{'name': 'outside_sensor_id',
                                                                           'type': 'int'},
                                                                          {'name': 'target_lower',
                                                                           'type': 'int'},
                                                                          {'name': 'target_upper',
                                                                           'type': 'int'},
                                                                          {'name': 'offset',
                                                                           'type': 'int'},
                                                                          {'name': 'trigger',
                                                                           'type': 'int'}]}]}]
    metric_definitions = [{'type': 'ventilation',
                           'tags': ['name', 'id'],
                           'metrics': [{'name': 'dewpoint',
                                        'description': 'Dew point',
                                        'type': 'gauge', 'unit': 'degree C'},
                                       {'name': 'absolute_humidity',
                                        'description': 'Absolute humidity',
                                        'type': 'gauge', 'unit': 'g/m3'},
                                       {'name': 'level',
                                        'description': 'Ventilation level',
                                        'type': 'gauge', 'unit': ''},
                                       {'name': 'medium',
                                        'description': 'Medium ventilation limit',
                                        'type': 'gauge', 'unit': '%'},
                                       {'name': 'high',
                                        'description': 'High ventilation limit',
                                        'type': 'gauge', 'unit': '%'},
                                       {'name': 'mean',
                                        'description': 'Average relative humidity level',
                                        'type': 'gauge', 'unit': '%'},
                                       {'name': 'stddev',
                                        'description': 'Stddev relative humidity level',
                                        'type': 'gauge', 'unit': '%'},
                                       {'name': 'samples',
                                        'description': 'Amount of samples',
                                        'type': 'gauge', 'unit': ''}]}]

    default_config = {}

    def __init__(self, webinterface, logger):
        super(Ventilation, self).__init__(webinterface, logger)
        self.logger('Starting Ventilation plugin...')

        self._config = self.read_config(Ventilation.default_config)
        self._config_checker = PluginConfigChecker(Ventilation.config_description)
        self._used_sensors = []

        self._samples = {}
        self._sensors = {}
        self._runtime_data = {}
        self._settings = {}
        self._last_ventilation = None
        self._metrics_queue = deque()

        self._read_config()

        self.logger("Started Ventilation plugin")

    def _read_config(self):
        self._outputs = {1: self._config.get('low', []),
                         2: self._config.get('medium', []),
                         3: self._config.get('medium', [])}
        self._used_sensors = [sensor['sensor_id'] for sensor in self._config.get('sensors', [])]
        self._mode, self._settings = self._config.get('mode', ['disabled', {}])
        self._enabled = len(self._used_sensors) > 0 and self._mode in ['dew_point', 'statistical']
        self.logger('Ventilation is {0}'.format('enabled' if self._enabled else 'disabled'))

    def _load_sensors(self):
        try:
            configs = json.loads(self.webinterface.get_sensor_configurations())
            if configs['success'] is False:
                self.logger('Failed to get sensor configurations: {0}'.format(configs.get('msg', 'Unknown error')))
            else:
                sensor_ids = []
                for sensor in configs['config']:
                    sensor_id = sensor['id']
                    if sensor_id in self._used_sensors or sensor_id == self._settings['outside_sensor_id']:
                        sensor_ids.append(sensor_id)
                        self._samples[sensor_id] = []
                        self._sensors[sensor_id] = sensor['name'] if sensor['name'] != '' else sensor_id
                for sensor_id in self._sensors.keys():
                    if sensor_id not in sensor_ids:
                        self._sensors.pop(sensor_id, None)
                        self._samples.pop(sensor_id, None)
        except CommunicationTimedOutException:
            self.logger('Error getting sensor status: CommunicationTimedOutException')
        except Exception as ex:
            self.logger('Error getting sensor status: {0}'.format(ex))

    @background_task
    def run(self):
        self._runtime_data = {}
        while True:
            if self._enabled:
                start = time.time()
                self._load_sensors()
                if self._mode == 'statistical':
                    self._process_statistics()
                elif self._mode == 'dew_point':
                    self._process_dew_point()
                # This loop should run approx. every minute.
                sleep = 60 - (time.time() - start)
                if sleep < 0:
                    sleep = 1
                time.sleep(sleep)
            else:
                time.sleep(5)

    def _process_dew_point(self):
        try:
            dew_points = {}
            abs_humidities = {}
            humidities = {}
            sensor_temperatures = {}
            outdoor_abs_humidity = None
            outdoor_dew_point = None
            outdoor_sensor_id = self._settings['outside_sensor_id']
            # Fetch data
            data_humidities = json.loads(self.webinterface.get_sensor_humidity_status())
            if data_humidities['success'] is False:
                self.logger('Failed to read humidities: {0}'.format(data_humidities.get('msg', 'Unknown error')))
                return
            data_temperatures = json.loads(self.webinterface.get_sensor_temperature_status())
            if data_temperatures['success'] is False:
                self.logger('Failed to read temperatures: {0}'.format(data_temperatures.get('msg', 'Unknown error')))
                return
            for sensor_id in range(len(data_humidities['status'])):
                if sensor_id not in self._used_sensors + [outdoor_sensor_id]:
                    continue
                humidity = data_humidities['status'][sensor_id]
                if humidity is None:
                    continue
                temperature = data_temperatures['status'][sensor_id]
                if temperature is None:
                    continue
                humidities[sensor_id] = humidity
                if sensor_id == outdoor_sensor_id:
                    outdoor_dew_point = Ventilation._dew_point(temperature, humidity)
                    outdoor_abs_humidity = Ventilation._abs_humidity(temperature, humidity)
                else:
                    sensor_temperatures[sensor_id] = temperature
                    dew_points[sensor_id] = Ventilation._dew_point(temperature, humidity)
                    abs_humidities[sensor_id] = Ventilation._abs_humidity(temperature, humidity)
            if outdoor_abs_humidity is None or outdoor_dew_point is None:
                self.logger('Could not load outdoor humidity or temperature')
                return
            # Calculate required ventilation based on sensor information
            target_lower = self._settings['target_lower']
            target_upper = self._settings['target_upper']
            offset = self._settings['offset']
            ventilation = 1
            trigger_sensors = {1: [],
                               2: [],
                               3: []}
            for sensor_id in dew_points:
                if sensor_id not in self._sensors:
                    continue
                if sensor_id not in self._runtime_data:
                    self._runtime_data[sensor_id] = {'trigger': 0,
                                                     'ventilation': 1,
                                                     'candidate': 1,
                                                     'reason': '',
                                                     'name': self._sensors[sensor_id],
                                                     'stats': [0, 0, 0, 0]}
                humidity = humidities[sensor_id]
                dew_point = dew_points[sensor_id]
                abs_humidity = abs_humidities[sensor_id]
                temperature = sensor_temperatures[sensor_id]
                self._runtime_data[sensor_id]['stats'] = [temperature, dew_point, abs_humidity, outdoor_abs_humidity]
                reason = 'RH {0:.2f} <= {1:.2f} <= {2:.2f}'.format(target_lower, humidity, target_upper)
                wanted_ventilation = 1
                # First, try to get the dew point inside the target range - increasing comfort
                if humidity < target_lower or humidity > target_upper:
                    if humidity < target_lower and outdoor_abs_humidity > abs_humidity:
                        wanted_ventilation = 2
                        reason = 'RH {0:.2f} < {1:.2f} and AH {2:.5f} > {3:.5f}'.format(humidity, target_lower, outdoor_abs_humidity, abs_humidity)
                    if humidity > target_upper and outdoor_abs_humidity < abs_humidity:
                        wanted_ventilation = 2
                        reason = 'RH {0:.2f} > {1:.2f} and AH {2:.5f} < {3:.5f}'.format(humidity, target_upper, outdoor_abs_humidity, abs_humidity)
                # Second, prevent actual temperature from hitting the dew point - make sure we don't have condense
                if outdoor_abs_humidity < abs_humidity:
                    if dew_point > temperature - offset:
                        wanted_ventilation = 3
                        reason = 'DP {0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - offset, temperature)
                    elif dew_point > temperature - 2 * offset:
                        wanted_ventilation = 2
                        reason = 'DP {0:.2f} > {1:.2f} - ({2:.2f})'.format(dew_point, temperature - 2 * offset, temperature)
                self._runtime_data[sensor_id]['candidate'] = wanted_ventilation
                current_ventilation = self._runtime_data[sensor_id]['ventilation']
                if current_ventilation != wanted_ventilation:
                    self._runtime_data[sensor_id]['trigger'] += 1
                    self._runtime_data[sensor_id]['reason'] = reason
                    if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']:
                        self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation
                        self._runtime_data[sensor_id]['trigger'] = 0
                        current_ventilation = wanted_ventilation
                        trigger_sensors[wanted_ventilation].append(sensor_id)
                else:
                    self._runtime_data[sensor_id]['reason'] = ''
                    self._runtime_data[sensor_id]['trigger'] = 0
                ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation'])
                self._enqueue_metrics(tags={'id': sensor_id,
                                            'name': self._sensors[sensor_id]},
                                      values={'dewpoint': float(dew_point),
                                              'absolute_humidity': float(abs_humidity),
                                              'level': int(current_ventilation)})
            self._enqueue_metrics(tags={'id': outdoor_sensor_id,
                                        'name': self._sensors[outdoor_sensor_id]},
                                  values={'dewpoint': float(outdoor_dew_point),
                                          'absolute_humidity': float(outdoor_abs_humidity),
                                          'level': 0})
            if ventilation != self._last_ventilation:
                if self._last_ventilation is None:
                    self.logger('Updating ventilation to 1 (startup)')
                else:
                    self.logger('Updating ventilation to {0} because of sensors: {1}'.format(
                        ventilation,
                        ', '.join(['{0} ({1})'.format(self._sensors[sensor_id],
                                                      self._runtime_data[sensor_id]['reason'])
                                   for sensor_id in trigger_sensors[ventilation]])
                    ))
                self._set_ventilation(ventilation)
                self._last_ventilation = ventilation
        except CommunicationTimedOutException:
            self.logger('Error getting sensor status: CommunicationTimedOutException')
        except Exception as ex:
            self.logger('Error calculating ventilation: {0}'.format(ex))
            self.logger('Stacktrace: {0}'.format(traceback.format_exc()))

    def _process_statistics(self):
        try:
            # Fetch data
            humidities = json.loads(self.webinterface.get_sensor_humidity_status())
            if humidities['success'] is True:
                for sensor_id in range(len(humidities['status'])):
                    if sensor_id not in self._samples:
                        continue
                    value = humidities['status'][sensor_id]
                    if value == 255:
                        continue
                    self._samples[sensor_id].append(value)
                    if len(self._samples[sensor_id]) > self._settings.get('samples', 1440):
                        self._samples[sensor_id].pop(0)
            # Calculate required ventilation based on sensor information
            ventilation = 1
            trigger_sensors = {1: [],
                               2: [],
                               3: []}
            for sensor_id in self._samples:
                if sensor_id not in self._sensors:
                    continue
                if sensor_id not in self._runtime_data:
                    self._runtime_data[sensor_id] = {'trigger': 0,
                                                     'ventilation': 1,
                                                     'candidate': 1,
                                                     'difference': '',
                                                     'name': self._sensors[sensor_id],
                                                     'stats': [0, 0, 0]}
                current = self._samples[sensor_id][-1]
                mean = Ventilation._mean(self._samples[sensor_id])
                stddev = Ventilation._stddev(self._samples[sensor_id])
                level_2 = mean + 2 * stddev
                level_3 = mean + 3 * stddev
                self._runtime_data[sensor_id]['stats'] = [current, level_2, level_3]
                if current > level_3:
                    wanted_ventilation = 3
                    difference = '{0:.2f} > {1:.2f}'.format(current, level_3)
                elif current > level_2:
                    wanted_ventilation = 2
                    difference = '{0:.2f} > {1:.2f}'.format(current, level_2)
                else:
                    wanted_ventilation = 1
                    difference = '{0:.2f} <= {1:.2f}'.format(current, level_2)
                self._runtime_data[sensor_id]['candidate'] = wanted_ventilation
                current_ventilation = self._runtime_data[sensor_id]['ventilation']
                if current_ventilation != wanted_ventilation:
                    self._runtime_data[sensor_id]['trigger'] += 1
                    self._runtime_data[sensor_id]['difference'] = difference
                    if self._runtime_data[sensor_id]['trigger'] >= self._settings['trigger']:
                        self._runtime_data[sensor_id]['ventilation'] = wanted_ventilation
                        self._runtime_data[sensor_id]['trigger'] = 0
                        current_ventilation = wanted_ventilation
                        trigger_sensors[wanted_ventilation].append(sensor_id)
                else:
                    self._runtime_data[sensor_id]['difference'] = ''
                    self._runtime_data[sensor_id]['trigger'] = 0
                ventilation = max(ventilation, self._runtime_data[sensor_id]['ventilation'])
                self._enqueue_metrics(tags={'id': sensor_id,
                                            'name': self._sensors[sensor_id]},
                                      values={'medium': float(level_2),
                                              'high': float(level_3),
                                              'mean': float(mean),
                                              'stddev': float(stddev),
                                              'samples': len(self._samples[sensor_id]),
                                              'level': int(current_ventilation)})
            if ventilation != self._last_ventilation:
                if self._last_ventilation is None:
                    self.logger('Updating ventilation to 1 (startup)')
                else:
                    self.logger('Updating ventilation to {0} because of sensors: {1}'.format(
                        ventilation,
                        ', '.join(['{0} ({1})'.format(self._sensors[sensor_id],
                                                      self._runtime_data[sensor_id]['difference'])
                                   for sensor_id in trigger_sensors[ventilation]])
                    ))
                self._set_ventilation(ventilation)
                self._last_ventilation = ventilation
        except CommunicationTimedOutException:
            self.logger('Error getting sensor status: CommunicationTimedOutException')
        except Exception as ex:
            self.logger('Error calculating ventilation: {0}'.format(ex))

    def _set_ventilation(self, level):
        success = True
        for setting in self._outputs[level]:
            output_id = int(setting['output_id'])
            value = setting['value']
            on = value > 0
            if on is False:
                value = None
            result = json.loads(self.webinterface.set_output(id=output_id, is_on=str(on), dimmer=value, timer=None))
            if result['success'] is False:
                self.logger('Error setting output {0} to {1}: {2}'.format(output_id, 'OFF' if value is None else value, result['msg']))
                success = False
        if success is True:
            self.logger('Ventilation set to {0}'.format(level))
        else:
            self.logger('Could not set ventilation to {0}'.format(level))
            success = False
        return success

    def _enqueue_metrics(self, tags, values):
        try:
            now = time.time()
            self._metrics_queue.appendleft({'type': 'ventilation',
                                            'timestamp': now,
                                            'tags': tags,
                                            'values': values})
        except Exception as ex:
            self.logger('Got unexpected error while enqueing metrics: {0}'.format(ex))

    @om_metric_data(interval=5)
    def collect_metrics(self):
        # Yield all metrics in the Queue
        try:
            while True:
                yield self._metrics_queue.pop()
        except IndexError:
            pass

    @staticmethod
    def _abs_humidity(temperature, humidity):
        """
        Calculate the absolute humidity (kg/m3) based on temperature and relative humidity.
        Formula was taken from http://www.aprweather.com/pages/calc.htm and should be good-enough for this purpose
        """
        dew_point = Ventilation._dew_point(temperature, humidity)
        return ((6.11 * 10.0 ** (7.5 * dew_point / (237.7 + dew_point))) * 100) / ((temperature + 273.16) * 461.5)

    @staticmethod
    def _dew_point(temperature, humidity):
        """
        Calculates the dew point for a given temperature and humidity
        """
        a = 17.27
        b = 237.7

        def gamma(_temperature, _humidity):
            return ((a * _temperature) / (b + _temperature)) + math.log(_humidity / 100.0)
        return (b * gamma(temperature, humidity)) / (a - gamma(temperature, humidity))

    @staticmethod
    def _mean(entries):
        """
        Calculates mean
        """
        if len(entries) > 0:
            return sum(entries) * 1.0 / len(entries)
        return 0

    @staticmethod
    def _stddev(entries):
        """
        Calculates standard deviation
        """
        mean = Ventilation._mean(entries)
        variance = map(lambda e: (e - mean) ** 2, entries)
        return sqrt(Ventilation._mean(variance))

    @om_expose
    def get_debug(self):
        return json.dumps({'runtime_data': self._runtime_data,
                           'ventilation': self._last_ventilation}, indent=4)

    @om_expose
    def get_config_description(self):
        return json.dumps(Ventilation.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], basestring):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})
示例#25
0
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})
示例#26
0
文件: main.py 项目: pdcleyn/plugins
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})
示例#27
0
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})
示例#28
0
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})
示例#29
0
    def test_check_config_nested_enum(self):
        """ Test check_config for nested_enum. """
        checker = PluginConfigChecker([{
            'name':
            'network',
            'type':
            'nested_enum',
            'choices': [{
                'value': 'Facebook',
                'content': [{
                    'name': 'likes',
                    'type': 'int'
                }]
            }, {
                'value': 'Twitter',
                'content': [{
                    'name': 'followers',
                    'type': 'int'
                }]
            }]
        }])

        checker.check_config({'network': ['Twitter', {'followers': 3}]})
        checker.check_config({'network': ['Facebook', {'likes': 3}]})

        try:
            checker.check_config({'network': 'test'})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('list' in str(exception))

        try:
            checker.check_config({'network': []})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('list' in str(exception) and '2' in str(exception))

        try:
            checker.check_config({'network': ['something else', {}]})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('choices' in str(exception))

        try:
            checker.check_config({'network': ['Twitter', {}]})
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('nested_enum dict' in str(exception)
                            and 'followers' in str(exception))
示例#30
0
文件: main.py 项目: wash34000/plugins
class Statful(OMPluginBase):
    """
    An Statful plugin, for sending metrics to Statful (adapted from InfluxDB plugin)
    """

    name = 'Statful'
    version = '1.0.0'
    url = 'https://api.statful.com/tel/v2.0/metrics'
    interfaces = [('config', '1.0')]

    config_description = [{'name': 'token',
                           'type': 'str',
                           'description': 'Statful API token for authentication.'},
                          {'name': 'add_custom_tag',
                           'type': 'str',
                           'description': 'Add custom tag to statistics'},
                          {'name': 'batch_size',
                           'type': 'int',
                           'description': 'The maximum batch size of grouped metrics to be send to Statful.'}]

    default_config = {}

    def __init__(self, webinterface, logger):
        super(Statful, self).__init__(webinterface, logger)
        self.logger('Starting Statful plugin...')

        self._config = self.read_config(Statful.default_config)
        self._config_checker = PluginConfigChecker(Statful.config_description)
        self._pending_metrics = {}
        self._send_queue = deque()

        self._send_thread = Thread(target=self._sender)
        self._send_thread.setName('Statful batch sender')
        self._send_thread.daemon = True
        self._send_thread.start()

        self._read_config()
        self.logger("Started Statful plugin")

    def _read_config(self):
        self._batch_size = self._config.get('batch_size', 10)
        self._add_custom_tag = self._config.get('add_custom_tag', '')

        token = self._config.get('token', '')
        self._headers = {'M-Api-Token': token, 'X-Requested-With': 'OpenMotics plugin: Statful'}

        self._enabled = token != ''
        self.logger('Statful is {0}'.format('enabled' if self._enabled else 'disabled'))

    @om_metric_receive(interval=30)
    def _receive_metric_data(self, metric):
        """
        All metrics are collected, as filtering is done more finegraded when mapping to tables
        > example_metric = {"source": "OpenMotics",
        >                   "type": "energy",
        >                   "timestamp": 1497677091,
        >                   "tags": {"device": "OpenMotics energy ID1",
        >                            "id": 0},
        >                   "values": {"power": 1234,
        >                              "power_counter": 1234567}}
        """
        try:
            if self._enabled is False:
                return

            values = metric['values']
            _values = {}
            for key in values.keys()[:]:
                value = values[key]
                if isinstance(value, basestring):
                    value = '"{0}"'.format(value)
                if isinstance(value, bool):
                    value = int(value == True)
                if isinstance(value, int) or isinstance(value, long):
                    value = '{0}'.format(value)
                _values[key] = value

            tags = {'source': metric['source'].lower()}
            if self._add_custom_tag:
                tags['custom_tag'] = self._add_custom_tag
            for tag, tvalue in metric['tags'].iteritems():
                if isinstance(tvalue, basestring):
                    # send tag values as ascii. specification details at https://www.statful.com/docs/metrics-ingestion-protocol.html#Metrics-Ingestion-Protocol
                    tags[tag] = tvalue.decode("utf-8").encode('ascii', 'ignore').replace(' ', '_').replace(',', '.')
                else:
                    tags[tag] = tvalue

            entries = self._build_entries(metric['type'], tags, _values, metric['timestamp'])
            for entry in entries:
                self._send_queue.appendleft(entry)

        except Exception as ex:
            self.logger('Error receiving metrics: {0}'.format(ex))

    @staticmethod
    def _build_entries(key, tags, value, timestamp):
        if isinstance(value, dict):
            _entries = []
            for vname, vvalue in value.iteritems():
                _entries.append(Statful._build_entry(key, tags, vname, vvalue, timestamp))
            return _entries

        return [Statful._build_entry(key, tags, None, value, timestamp)]

    @staticmethod
    def _build_entry(metric, tags, key, value, timestamp):
        return 'openmotics.{0},{1} {2}{3}'.format(metric if key is None else '{0}.{1}'.format(metric, key),
                                       ','.join('{0}={1}'.format(tname, tvalue)
                                                for tname, tvalue in tags.iteritems()),
                                       value,
                                       '' if timestamp is None else ' {:.0f}'.format(timestamp))

    def _sender(self):
        _stats_time = 0
        _batch_sizes = []
        _queue_sizes = []
        _run_amount = 0
        _batch_amount = 0
        while True:
            try:
                data = []
                try:
                    while True:
                        data.append(self._send_queue.pop())
                        if len(data) == self._batch_size:
                            raise IndexError()
                except IndexError:
                    pass
                if len(data) > 0:
                    _batch_sizes.append(len(data))
                    _run_amount += len(data)
                    _batch_amount += 1
                    _queue_sizes.append(len(self._send_queue))
                    response = requests.put(url=Statful.url,
                                            data='\n'.join(data),
                                            headers=self._headers,
                                            verify=False)
                    if response.status_code != 201:
                        self.logger('Send failed, received: {0} ({1})'.format(response.text, response.status_code))
                    if _stats_time < time.time() - 1800:
                        _stats_time = time.time()
                        self.logger('Queue size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format(
                            min(_queue_sizes),
                            sum(_queue_sizes) / float(len(_queue_sizes)),
                            max(_queue_sizes)
                        ))
                        self.logger('Batch size stats: {0:.2f} min, {1:.2f} avg, {2:.2f} max'.format(
                            min(_batch_sizes),
                            sum(_batch_sizes) / float(len(_batch_sizes)),
                            max(_batch_sizes)
                        ))
                        self.logger('Total {0} metric(s) over {1} batche(s)'.format(_run_amount, _batch_amount))
                        _batch_sizes = []
                        _queue_sizes = []
                        _run_amount = 0
                        _batch_amount = 0
            except Exception as ex:
                self.logger('Error sending from queue: {0}'.format(ex))
            time.sleep(0.1)

    @om_expose
    def get_config_description(self):
        return json.dumps(Statful.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], basestring):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})
示例#31
0
文件: main.py 项目: rubengr/gateway
class EventForwarder(OMPluginBase):
    name = 'EventForwarder'
    version = '0.0.7'
    interfaces = [('config', '1.0')]

    config_description = [
        {
            'name': 'port',
            'type': 'int',
            'description': 'Internal port for this process to listen on',
        },
    ]

    default_config = {
        'port': 6666,
    }

    def __init__(self, webinterface, logger):
        super(EventForwarder, self).__init__(webinterface, logger)
        self.config = self.read_config(self.default_config)
        self.config_checker = PluginConfigChecker(self.config_description)
        self.connection = None
        self._connected = False

    @background_task
    def listen(self):
        port = self.config['port']
        listener = Listener(('localhost', port))
        while True:
            if self._connected:
                time.sleep(1)
            else:
                if port != self.config['port']:
                    listener.close()
                    port = self.config['port']
                    listener = Listener('localhost', port)
                self.connection = listener.accept()
                # Hangs until a client connects
                self._connected = True

    @receive_events
    def forward_master_events(self, event):
        """
        Forward events of the master. You can set events e.g. as advanced
        action when configuring inputs.
        :param event: Event number sent by the master
        :type event: int
        """
        self.logger('Received event: %s' % event)
        if not self._connected:
            return
        try:
            self.connection.send(event)
        except IOError:
            self.logger('Failed to forward event')
            self.connection.close()
            self._connected = False

    @om_expose
    def get_config_description(self):
        return json.dumps(EventForwarder.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self.config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        self.config_checker.check_config(config)
        self.config = config
        self.write_config(config)
        return json.dumps({'success': True})
示例#32
0
class ModbusTCPSensor(OMPluginBase):
    """
    Get sensor values form modbus
    """

    name = 'modbusTCPSensor'
    version = '1.0.18'
    interfaces = [('config', '1.0')]

    config_description = [{
        'name': 'modbus_server_ip',
        'type': 'str',
        'description': 'IP or hostname of the ModBus server.'
    }, {
        'name':
        'modbus_port',
        'type':
        'int',
        'description':
        'Port of the ModBus server. Default: 502'
    }, {
        'name': 'debug',
        'type': 'int',
        'description': 'Turn on debugging (0 = off, 1 = on)'
    }, {
        'name':
        'sample_rate',
        'type':
        'int',
        'description':
        'How frequent (every x seconds) to fetch the sensor data, Default: 60'
    }, {
        'name':
        'sensors',
        'type':
        'section',
        'description':
        'OM sensor ID (e.g. 4), a sensor type and a Modbus Address',
        'repeat':
        True,
        'min':
        0,
        'content': [{
            'name': 'sensor_id',
            'type': 'int'
        }, {
            'name':
            'sensor_type',
            'type':
            'enum',
            'choices':
            ['temperature', 'humidity', 'brightness', 'validation_bit']
        }, {
            'name': 'modbus_address',
            'type': 'int'
        }, {
            'name': 'modbus_register_length',
            'type': 'int'
        }]
    }, {
        'name':
        'bits',
        'type':
        'section',
        'description':
        'OM validation bit ID (e.g. 4), and a Modbus Coil Address',
        'repeat':
        True,
        'min':
        0,
        'content': [{
            'name': 'validation_bit_id',
            'type': 'int'
        }, {
            'name': 'modbus_coil_address',
            'type': 'int'
        }]
    }]

    default_config = {'modbus_port': 502, 'sample_rate': 60}

    def __init__(self, webinterface, logger):
        super(ModbusTCPSensor, self).__init__(webinterface, logger)
        self.logger('Starting ModbusTCPSensor plugin...')

        self._config = self.read_config(ModbusTCPSensor.default_config)
        self._config_checker = PluginConfigChecker(
            ModbusTCPSensor.config_description)

        py_modbus_tcp_egg = '/opt/openmotics/python/plugins/modbusTCPSensor/pyModbusTCP-0.1.7-py2.7.egg'
        if py_modbus_tcp_egg not in sys.path:
            sys.path.insert(0, py_modbus_tcp_egg)

        self._client = None
        self._samples = []
        self._save_times = {}
        self._read_config()

        self.logger("Started ModbusTCPSensor plugin")

    def _read_config(self):
        self._ip = self._config.get('modbus_server_ip')
        self._port = self._config.get(
            'modbus_port', ModbusTCPSensor.default_config['modbus_port'])
        self._debug = self._config.get('debug', 0) == 1
        self._sample_rate = self._config.get(
            'sample_rate', ModbusTCPSensor.default_config['sample_rate'])
        self._sensors = []
        # Load Sensors
        for sensor in self._config.get('sensors', []):
            if 0 <= sensor['sensor_id'] < 32:
                self._sensors.append(sensor)
        # Load Validation bits
        self._validation_bits = []
        for validation_bit in self._config.get('bits', []):
            if 0 <= validation_bit['validation_bit_id'] < 256:
                self._validation_bits.append(validation_bit)
        self._enabled = len(self._sensors) > 0

        try:
            from pyModbusTCP.client import ModbusClient
            self._client = ModbusClient(self._ip,
                                        self._port,
                                        auto_open=True,
                                        auto_close=True)
            self._client.open()
            self._enabled = self._enabled & True
        except Exception as ex:
            self.logger('Error connecting to Modbus server: {0}'.format(ex))

        self.logger('ModbusTCPSensor is {0}'.format(
            'enabled' if self._enabled else 'disabled'))

    @background_task
    def run(self):
        while True:
            try:
                if not self._enabled or self._client is None:
                    time.sleep(5)
                    continue
                # Process all configured sensors
                self.process_sensors()
                # Process all validation bits
                self.process_validation_bits()

                time.sleep(self._sample_rate)
            except Exception as ex:
                self.logger('Could not process sensor values: {0}'.format(ex))
                time.sleep(15)

    def clamp_sensor(self, value, sensor_type):
        clamping = {
            'temperature': [-32, 95.5, 1],
            'humidity': [0, 100, 1],
            'brightness': [0, 100, 0]
        }
        return round(
            max(clamping[sensor_type][0], min(value,
                                              clamping[sensor_type][1])),
            clamping[sensor_type][2])

    def process_sensors(self):
        om_sensors = {}
        for sensor in self._sensors:
            registers = self._client.read_holding_registers(
                sensor['modbus_address'], sensor['modbus_register_length'])
            if registers is None:
                continue
            sensor_value = struct.unpack(
                '>f',
                struct.pack('BBBB', registers[1] >> 8, registers[1] & 255,
                            registers[0] >> 8, registers[0] & 255))[0]
            if not om_sensors.get(sensor['sensor_id']):
                om_sensors[sensor['sensor_id']] = {
                    'temperature': None,
                    'humidity': None,
                    'brightness': None
                }

            sensor_value = self.clamp_sensor(sensor_value,
                                             sensor['sensor_type'])

            om_sensors[sensor['sensor_id']][
                sensor['sensor_type']] = sensor_value
        if self._debug == 1:
            self.logger('The sensors values are: {0}'.format(om_sensors))

        for sensor_id, values in om_sensors.iteritems():
            result = json.loads(
                self.webinterface.set_virtual_sensor(sensor_id, **values))
            if result['success'] is False:
                self.logger(
                    'Error when updating virtual sensor {0}: {1}'.format(
                        sensor_id, result['msg']))

    def process_validation_bits(self):
        for validation_bit in self._validation_bits:
            bit = self._client.read_coils(
                validation_bit['modbus_coil_address'], 1)

            if bit is None or len(bit) != 1:
                if self._debug == 1:
                    self.logger('Failed to read bit {0}, bit is {1}'.format(
                        validation_bit['validation_bit_id'], bit))
                continue
            result = json.loads(
                self.webinterface.do_basic_action(
                    None, 237 if bit[0] else 238,
                    validation_bit['validation_bit_id']))
            if result['success'] is False:
                self.logger('Failed to set bit {0} to {1}'.format(
                    validation_bit['validation_bit_id'], 1 if bit[0] else 0))
            else:
                if self._debug == 1:
                    self.logger('Successfully set bit {0} to {1}'.format(
                        validation_bit['validation_bit_id'],
                        1 if bit[0] else 0))

    @om_expose
    def get_config_description(self):
        return json.dumps(ModbusTCPSensor.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self.write_config(config)
        self._config = config
        self._read_config()
        return json.dumps({'success': True})
示例#33
0
文件: main.py 项目: dengxw/plugins
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})
示例#34
0
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})
示例#35
0
class SensorDotCommunity(OMPluginBase):

    name = 'SensorDotCommunity'
    version = '1.0.1'
    interfaces = [('config', '1.0')]

    config_description = []
    default_config = []

    def __init__(self, webinterface, gateway_logger):
        self.setup_logging(log_function=gateway_logger)
        super(SensorDotCommunity, self).__init__(webinterface, logger)
        logger.info('Starting %s plugin %s ...', self.name, self.version)

        self._config = self.default_config
        self._config_checker = PluginConfigChecker(
            SensorDotCommunity.config_description)

        logger.info("%s plugin started", self.name)

    @staticmethod
    def setup_logging(log_function):  # type: (Callable) -> None
        logger.setLevel(logging.INFO)
        log_handler = PluginLogHandler(log_function=log_function)
        # some elements like time and name are added by the plugin runtime already
        # formatter = logging.Formatter('%(asctime)s - %(name)s - %(threadName)s - %(levelname)s - %(message)s')
        formatter = logging.Formatter(
            '%(threadName)s - %(levelname)s - %(message)s')
        log_handler.setFormatter(formatter)
        logger.addHandler(log_handler)

    @om_expose(version=2, auth=False)
    def api(self, plugin_web_request):
        """
{
  "esp8266id": "12345678",
  "software_version": "NRZ-2020-133",
  "sensordatavalues": [
    {
      "value_type": "SDS_P1",
      "value": "7.18"
    },
    {
      "value_type": "SDS_P2",
      "value": "2.58"
    },
    {
      "value_type": "temperature",
      "value": "23.20"
    },
    {
      "value_type": "humidity",
      "value": "46.40"
    },
    {
      "value_type": "samples",
      "value": "5029246"
    },
    {
      "value_type": "min_micro",
      "value": "28"
    },
    {
      "value_type": "max_micro",
      "value": "20132"
    },
    {
      "value_type": "interval",
      "value": "145000"
    },
    {
      "value_type": "signal",
      "value": "-60"
    }
  ]
}        """
        method = plugin_web_request.method
        path = plugin_web_request.path
        body = plugin_web_request.body
        params = plugin_web_request.params
        headers = plugin_web_request.headers
        errors = []

        logger.debug('%s %s %s', method, path, params)
        logger.debug('%s', headers)
        logger.debug('%s', body)

        data = json.loads(body)
        known_sensors = self._get_known_sensors()

        device_id = data["esp8266id"]
        for entry in data.get("sensordatavalues", []):
            value_type = entry["value_type"]
            value = float(entry["value"])
            sensor_external_id = "{}/{}".format(device_id, value_type)
            if value_type == "temperature":
                name = "Temperature"
                physical_quantity = "temperature"
                unit = "celcius"
            elif value_type == "humidity":
                name = "Humidity"
                physical_quantity = "humidity"
                unit = "percent"
            elif value_type == "SDS_P1":
                name = "PM10"
                physical_quantity = "dust"
                unit = "micro_gram_per_cubic_meter"
            elif value_type == "SDS_P2":
                name = "PM2.5"
                physical_quantity = "dust"
                unit = "micro_gram_per_cubic_meter"
            else:
                logger.debug('unsupported sensor value %s', value_type)
                continue
            if sensor_external_id not in known_sensors.keys():
                logger.info('Registering new sensor %s with external id %s',
                            name, sensor_external_id)
                om_sensor_id = self._register_sensor(name, sensor_external_id,
                                                     physical_quantity, unit)
            else:
                om_sensor_id = known_sensors[sensor_external_id]
            if om_sensor_id is not None:
                logger.info('Updating sensor %s (%s) with %s (%s)', name,
                            om_sensor_id, value, unit)
                self._update_sensor(om_sensor_id, value)
            else:
                msg = 'Sensor.community sensor {} ({}) not found'.format(
                    name, sensor_external_id)
                logger.error(msg)
                errors.append(msg)
        if errors:
            return PluginWebResponse(status_code=500,
                                     body='\n'.join(errors),
                                     path=plugin_web_request.path)
        else:
            return PluginWebResponse(status_code=200,
                                     body='success',
                                     path=plugin_web_request.path)

    def _get_known_sensors(self):
        response = self.webinterface.get_sensor_configurations()
        data = json.loads(response)
        return {
            x['external_id']: x['id']
            for x in data['config']
            if x.get('source', {}).get('name') == SensorDotCommunity.name
            and x['external_id'] not in [None, '']
        }

    @om_expose
    def get_config_description(self):
        return json.dumps(SensorDotCommunity.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self.write_config(config)
        return json.dumps({'success': True})

    def _register_sensor(self, name, external_id, physical_quantity, unit):
        logger.info('Registering sensor with name %s and external_id %s', name,
                    external_id)
        try:
            config = {
                'name': name,
            }
            response = self.webinterface.sensor.register(
                external_id=external_id,
                physical_quantity=physical_quantity,
                unit=unit,
                config=config)
            logger.info(
                'Registered new sensor with name %s and external_id %s', name,
                external_id)
            return response.id
        except Exception as e:
            logger.warning(
                'Failed registering sensor with name %s and external_id %s with exception %s',
                name, external_id, str(e.message))
            return None

    def _update_sensor(self, sensor_id, value):
        logger.debug('Updating sensor %s with status %s', sensor_id, value)
        response = self.webinterface.sensor.set_status(sensor_id=sensor_id,
                                                       value=value)
        if response is None:
            logger.warning('Could not set the updated sensor value')
            return False
        return True
示例#36
0
class RTD10(OMPluginBase):
    """
    RTD10 plugin
    """

    name = 'RTD10'
    version = '0.1.4'
    interfaces = [('config', '1.0')]

    config_description = [{
        'name':
        'thermostats',
        'type':
        'section',
        'description':
        'Thermostats',
        'repeat':
        True,
        'min':
        1,
        'content': [{
            'name': 'thermostat_id',
            'type': 'int',
            'description': 'OpenMotics thermostat ID'
        }, {
            'name':
            's1_output_id',
            'type':
            'int',
            'description':
            'Output connected to the S1 port (setpoint control)'
        }, {
            'name': 's1_temperature_curve',
            'type': 'str',
            'description': 'Setpoint control temperature curve'
        }, {
            'name':
            's2_output_id',
            'type':
            'int',
            'description':
            'Output connected to the S2 port (ventilation control)'
        }, {
            'name':
            's2_value',
            'type':
            'int',
            'description':
            'Ventilation output value (0-100, in percent of 0-10V)'
        }, {
            'name': 's3_output_id',
            'type': 'int',
            'description': 'Output connected to the S3 port (mode)'
        }, {
            'name':
            's4_output_id',
            'type':
            'int',
            'description':
            'Output connected to the S4 port (air direction)'
        }, {
            'name':
            's4_value',
            'type':
            'int',
            'description':
            'Air direction output value (0-100, in percent of 0-10V)'
        }, {
            'name': 's5_output_id',
            'type': 'int',
            'description': 'Output connected to the S5 port (state)'
        }]
    }]
    default_config = {}

    def __init__(self, webinterface, logger):
        super(RTD10, self).__init__(webinterface, logger)
        self.logger('Starting RTD10 plugin...')

        self._config = self.read_config(RTD10.default_config)
        self._config_checker = PluginConfigChecker(RTD10.config_description)

        self._enabled = False
        self._syncing = False
        self._thermostats = {}
        self._s_values = {}

        self._read_config()

        self.logger("Started RTD10 plugin")

    def _read_config(self):
        self._enabled = False

        try:
            used_ids = []
            for thermostat in self._config.get('thermostats', []):
                thermostat_id = int(thermostat.get('thermostat_id'))
                config = {
                    's{0}_output_id'.format(i):
                    int(thermostat.get('s{0}_output_id'.format(i)))
                    for i in range(1, 6)
                }
                temperature_curve = {
                    float(key): int(value)
                    for key, value in json.loads(
                        thermostat.get('s1_temperature_curve')).items()
                }
                config.update({
                    's1_temperature_curve': temperature_curve,
                    's2_value': int(thermostat.get('s2_value')),
                    's4_value': int(thermostat.get('s4_value'))
                })
                self._thermostats[thermostat_id] = config
                used_ids.append(thermostat_id)
            for thermostat_id in list(self._thermostats.keys()):
                if thermostat_id not in used_ids:
                    self._thermostats.pop(thermostat_id, None)
            self._enabled = True
        except Exception as ex:
            self.logger('Could not read/process configuration: {0}'.format(ex))

        self.logger(
            'RTD10 is {0}'.format('enabled' if self._enabled else 'disabled'))
        if self._enabled:
            thread = Thread(target=self._sync)
            thread.start()

    def _sync(self):
        if self._syncing:
            return
        self.logger('Performing initial sync...')
        try:
            self._syncing = True
            while True:
                try:
                    result = json.loads(
                        self.webinterface.get_thermostat_group_status())
                    if result.get('success', False) is False:
                        raise RuntimeError(result.get('msg', 'Unknown error'))
                    for thermostat_group_status in result.get('status', []):
                        mode = thermostat_group_status['mode'].upper(
                        )  # COOLING / HEATING
                        for thermostat_status in thermostat_group_status[
                                'thermostats']:
                            thermostat_id = thermostat_status['id']
                            state = thermostat_status['state'].upper(
                            )  # ON / OFF
                            setpoint = thermostat_status[
                                'setpoint_temperature']
                            self._drive_device(thermostat_id=thermostat_id,
                                               mode=mode,
                                               state=state,
                                               setpoint=setpoint)
                    return
                except Exception as ex:
                    self.logger(
                        'Could not load thermostat group states: {0}'.format(
                            ex))
                    time.sleep(30)
        finally:
            self.logger('Performing initial sync... Done')
            self._syncing = False

    @thermostat_status(version=1)
    def thermostat_status(self, status):
        thermostat_id = status['id']
        mode = status['status']['mode'].upper()  # COOLING / HEATING
        state = status['status']['state'].upper()  # ON / OFF
        setpoint = status['status']['current_setpoint']

        self._drive_device(thermostat_id=thermostat_id,
                           mode=mode,
                           state=state,
                           setpoint=setpoint)

    def _drive_device(self, thermostat_id, mode, state, setpoint):
        configuration = self._thermostats.get(thermostat_id)
        if configuration is None:
            return

        # S1 - Temperature curve: Map the current setpoint to a valve output value
        s1_value = 0
        output_id = configuration['s1_output_id']
        for temperature in sorted(
                configuration['s1_temperature_curve'].keys()):
            if setpoint >= temperature:
                s1_value = configuration['s1_temperature_curve'][temperature]
        self._set_output(output_id=output_id,
                         output_value=s1_value,
                         s_number=1,
                         thermostat_id=thermostat_id)

        # S2 - Ventilation control: Set the ventilation value (hardcoded configured value)
        output_id = configuration['s2_output_id']
        s2_value = configuration['s2_value']
        self._set_output(output_id=output_id,
                         output_value=s2_value,
                         s_number=2,
                         thermostat_id=thermostat_id)

        # S3 - Mode: Sets the system mode
        output_id = configuration['s3_output_id']
        s3_value = 32 if mode == 'HEATING' else 62
        self._set_output(output_id=output_id,
                         output_value=s3_value,
                         s_number=3,
                         thermostat_id=thermostat_id)

        # S4 - Air direction: Sets the RTD10 air direction
        output_id = configuration['s4_output_id']
        s4_value = configuration['s4_value']
        self._set_output(output_id=output_id,
                         output_value=s4_value,
                         s_number=4,
                         thermostat_id=thermostat_id)

        # S5 - State: Sets the RTD10 state
        output_id = configuration['s5_output_id']
        s5_value = 100 if state == 'ON' else 0
        self._set_output(output_id=output_id,
                         output_value=s5_value,
                         s_number=5,
                         thermostat_id=thermostat_id)

        new_s_values = [s1_value, s2_value, s3_value, s4_value, s5_value]
        if new_s_values != self._s_values.get(thermostat_id):
            self.logger('New S-values for thermostat {0}: {1}'.format(
                thermostat_id, ', '.join([
                    'S{0}={1:.1f}V'.format(i + 1,
                                           float(new_s_values[i]) / 10.0)
                    for i in range(5)
                ])))
            self._s_values[thermostat_id] = new_s_values

    def _set_output(self, output_id, output_value, s_number, thermostat_id):
        try:
            result = json.loads(
                self.webinterface.set_output(id=output_id,
                                             is_on=output_value > 0,
                                             dimmer=output_value))
            if result.get('success', False) is False:
                raise RuntimeError(result.get('msg', 'Unknown error'))
        except Exception as ex:
            self.logger(
                'Could not set output {0} (S{1} for thermostat {2}) to {3}: {4}'
                .format(output_id, s_number, thermostat_id, output_value, ex))

    @om_expose
    def get_config_description(self):
        return json.dumps(RTD10.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})
示例#37
0
class Polysun(OMPluginBase):
    """
    A Polysun plugin
    """
    class State(object):
        # In sync with Gateway implementation
        GOING_UP = 'going_up'
        GOING_DOWN = 'going_down'
        STOPPED = 'stopped'
        UP = 'up'
        DOWN = 'down'

    name = 'Polysun'
    version = '0.1.6'
    interfaces = [('config', '1.0')]

    config_description = [{
        'name':
        'mapping',
        'type':
        'section',
        'description':
        'Shutter to Output mapping',
        'repeat':
        True,
        'min':
        1,
        'content': [{
            'name': 'shutter_id',
            'type': 'int'
        }, {
            'name': 'output_id_up',
            'type': 'int'
        }, {
            'name': 'output_id_down',
            'type': 'int'
        }, {
            'name': 'inputs',
            'type': 'section',
            'description': 'Inputs that directly manipulate both Outputs',
            'repeat': True,
            'min': 0,
            'content': [{
                'name': 'input_id',
                'type': 'int'
            }]
        }]
    }]

    default_config = {}

    def __init__(self, webinterface, logger):
        super(Polysun, self).__init__(webinterface, logger)
        self.logger('Starting Polysun plugin...')

        self._config = self.read_config(Polysun.default_config)
        self._config_checker = PluginConfigChecker(Polysun.config_description)

        self._states = {}
        self._mapping = {}
        self._input_shutter_mapping = {}
        self._lost_shutters = {}
        self._action_queue = deque()
        self._input_enabled = None

        self._read_config()
        self.logger("Started Polysun plugin")

    def _read_config(self):
        new_mapping = {}
        new_input_mapping = {}
        for entry in self._config.get('mapping', []):
            shutter_id = entry['shutter_id']
            try:
                shutter_id = int(shutter_id)
                output_id_up = entry['output_id_up']
                output_id_down = entry['output_id_down']
                if not 0 <= shutter_id <= 240 or not 0 <= output_id_up <= 240 or not 0 <= output_id_down <= 240:
                    continue
                new_mapping[shutter_id] = {
                    'up': output_id_up,
                    'down': output_id_down
                }
                for input_entry in entry.get('inputs', []):
                    input_id = int(input_entry['input_id'])
                    new_input_mapping.setdefault(input_id,
                                                 set()).add(shutter_id)
            except ValueError:
                self.logger(
                    'Skipped entry with shutter_id {0}'.format(shutter_id))
        self._mapping = new_mapping
        self._input_shutter_mapping = new_input_mapping
        self._enabled = len(self._mapping) > 0
        self.logger('Polysun is {0}'.format(
            'enabled' if self._enabled else 'disabled'))

    @shutter_status
    def shutter_status(self, status, detail):
        _ = status  # We need the details
        for shutter_id in detail:
            new_state = detail[shutter_id]['state']
            shutter_id = int(shutter_id)
            if shutter_id not in self._mapping:
                continue
            old_state = self._states.get(shutter_id, Polysun.State.STOPPED)
            if new_state != old_state:
                self._states[shutter_id] = new_state
                self._action_queue.appendleft(
                    [shutter_id, new_state, old_state])
                self.logger(
                    'Shutter {0}: Received state transition from {1} to {2}'.
                    format(shutter_id, old_state, new_state))

    @input_status(version=2)
    def input_status(self, data):
        if self._enabled and self._input_enabled:
            input_id = data.get('input_id')
            shutter_ids = self._input_shutter_mapping.get(input_id, set())
            for shutter_id in shutter_ids:
                self.logger(
                    'Shutter {0}: Lost position due to Input {1}'.format(
                        shutter_id, input_id))
                self._lost_shutters[shutter_id] = time.time()
                self.webinterface.shutter_report_lost_position(id=shutter_id)

    @background_task
    def runner(self):
        while True:
            try:
                if len(self._input_shutter_mapping
                       ) > 0 and self._input_enabled is None:
                    result = json.loads(self.webinterface.get_features())
                    if not result.get('success', False):
                        self.logger('Could not load features: {0}'.format(
                            result.get('msg', 'Unknown')))
                    else:
                        features = result.get('features', [])
                        self._input_enabled = 'shutter_positions' in features
                        self.logger(
                            'Gateway {0} support reporting lost positions'.
                            format(
                                'does' if self._input_enabled else 'does not'))
            except Exception as ex:
                self.logger(
                    'Unexpected exception loading Gateway features: {0}'.
                    format(ex))
            try:
                shutter_id, new_state, old_state = self._action_queue.pop()
                mapping = self._mapping.get(shutter_id)
                if mapping is None:
                    continue

                if new_state == Polysun.State.STOPPED:
                    lost_time = self._lost_shutters.get(shutter_id, 0)
                    if lost_time > (
                            time.time() - 5
                    ):  # Ignore STOPPED state within `x` seconds of lost position
                        self.logger(
                            'Shutter {0}: Ignore stopped state due to lost position'
                            .format(shutter_id))
                        continue

                output_id_up = mapping['up']
                output_id_down = mapping['down']

                # If there's an immediate direction change (the shutter is going up and is suddenly going down or vice versa,
                # the "button" are first released and the shutter considered to be stopped for further logic
                if old_state in [
                        Polysun.State.GOING_DOWN, Polysun.State.GOING_UP
                ] and new_state in [
                        Polysun.State.GOING_DOWN, Polysun.State.GOING_UP
                ]:
                    self.logger(
                        'Shutter {0}: Immediate direction change'.format(
                            shutter_id))
                    self._turn_output(output_id_up, False)
                    self._turn_output(output_id_down, False)
                    old_state = Polysun.State.STOPPED
                    self.logger(
                        'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off'
                        .format(shutter_id, output_id_up, output_id_down))

                # If the old state was in some stopped state (either stopped, up or down) a movement is started. This means one of the
                # "buttons" needs to be pressed until the shutter timeout is elapsed.
                if old_state in [
                        Polysun.State.DOWN, Polysun.State.UP,
                        Polysun.State.STOPPED
                ]:
                    self.logger(
                        'Shutter {0}: Started moving'.format(shutter_id))
                    if new_state == Polysun.State.GOING_DOWN:
                        self._turn_output(
                            output_id_up, False
                        )  # Make sure only one "button" is pressed at a time
                        self._turn_output(output_id_down, True)
                        self.logger(
                            'Shutter {0}: Connected output {1} (down) is turned on'
                            .format(shutter_id, output_id_down))
                    if new_state == Polysun.State.GOING_UP:
                        self._turn_output(
                            output_id_down, False
                        )  # Make sure only one "button" is pressed at a time
                        self._turn_output(output_id_up, True)
                        self.logger(
                            'Shutter {0}: Connected output {1} (up) is turned on'
                            .format(shutter_id, output_id_up))

                # If the shutter is currently moving and reached the UP/DOWN position, it means the configured timeout is elapsed
                # and the the blinds are assumed to be moving to the correct location. The "buttons" can be released
                if old_state in [
                        Polysun.State.GOING_UP, Polysun.State.GOING_DOWN
                ] and new_state in [Polysun.State.UP, Polysun.State.DOWN]:
                    self.logger('Shutter {0}: Shutter is now up/down'.format(
                        shutter_id))
                    self._turn_output(output_id_up, False)
                    self._turn_output(output_id_down, False)
                    self.logger(
                        'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off'
                        .format(shutter_id, output_id_up, output_id_down))

                # If the new state is STOPPED, it (should) mean that an explicit stop action was executed. This is emulated by
                # briefly pressing the "button" again.
                if new_state == Polysun.State.STOPPED:
                    output_id = output_id_down
                    direction = 'down'
                    if old_state == Polysun.State.GOING_UP:
                        output_id = output_id_up
                        direction = 'up'
                    self.logger(
                        'Shutter {0}: Shutter is stopped'.format(shutter_id))
                    self._turn_output(output_id_down, False)
                    self._turn_output(output_id_up, False)
                    self.logger(
                        'Shutter {0}: Connected outputs {1} (up) and {2} (down) are turned off'
                        .format(shutter_id, output_id_up, output_id_down))
                    self._turn_output(output_id, True)
                    self._turn_output(output_id, False)
                    self.logger(
                        'Shutter {0}: Connected output {1} ({2}) turned on & off'
                        .format(shutter_id, output_id, direction))

            except IndexError:
                time.sleep(1)
            except Exception as ex:
                self.logger(
                    'Unexpected exception processing workload: {0}'.format(ex))
                time.sleep(1)

    def _turn_output(self, output_id, on):
        try:
            result = json.loads(
                self.webinterface.set_output(id=output_id, is_on=on))
            if not result.get('success', False):
                self.logger('Could not turn {0} output {1}: {2}'.format(
                    'on' if on else 'off', output_id,
                    result.get('msg', 'Unknown')))
        except Exception as ex:
            self.logger(
                'Unexpected exception turning {0} output {1}: {2}'.format(
                    'on' if on else 'off', output_id, ex))

    @om_expose
    def get_config_description(self):
        return json.dumps(Polysun.config_description)

    @om_expose
    def get_config(self):
        return json.dumps(self._config)

    @om_expose
    def set_config(self, config):
        config = json.loads(config)
        for key in config:
            if isinstance(config[key], six.string_types):
                config[key] = str(config[key])
        self._config_checker.check_config(config)
        self._config = config
        self._read_config()
        self.write_config(config)
        return json.dumps({'success': True})
示例#38
0
文件: main.py 项目: pdcleyn/plugins
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})
示例#39
0
文件: main.py 项目: pdcleyn/plugins
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})
示例#40
0
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})
示例#41
0
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})