Example #1
0
    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")
Example #2
0
    def __init__(self, webinterface, logger):
        super(Astro, self).__init__(webinterface, logger)
        self.logger('Starting Astro plugin...')

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

        pytz_egg = '/opt/openmotics/python/plugins/Astro/pytz-2017.2-py2.7.egg'
        if pytz_egg not in sys.path:
            sys.path.insert(0, pytz_egg)

        self._latitude = None
        self._longitude = None

        self._group_actions = {}
        self._bits = {}

        self._last_request_date = None
        self._execution_plan = {}

        self._sleeper = Event()
        self._sleep_until = 0

        thread = Thread(target=self._sleep_manager)
        thread.start()

        self._read_config()

        self.logger("Started Astro plugin")
Example #3
0
    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")
Example #4
0
    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")
Example #5
0
    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)
Example #6
0
    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")
Example #7
0
    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")
Example #8
0
    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")
Example #9
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})
Example #10
0
    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")
Example #11
0
    def __init__(self, webinterface, logger):
        super(RTI, self).__init__(webinterface, logger)
        self.logger('Starting RTI plugin...')

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

        self._command_queue = Queue()
        self._enabled = False
        self._serial = None
        self._read_config()

        self.logger("Started RTI plugin")
Example #12
0
    def __init__(self, webinterface, logger):
        super(HealthboxPlugin, self).__init__(webinterface, logger)

        self.__config = self.read_config(HealthboxPlugin.default_config)
        self.__config_checker = PluginConfigChecker(HealthboxPlugin.config_descr)
        self._enabled = True

        self.api_handler = ApiHandler(self.logger)
        self.discovered_devices = {}  # dict of all the Healthbox3 drivers mapped with register key as key
        self.serial_key_to_gateway_id = {}  # mapping of register key to gateway id (for api calls)

        self.logger("Started Healthbox 3 plugin")

        self.healtbox_manager = HealthBox3Manager()
        self.healtbox_manager.set_discovery_callback(self.discover_callback)
        self.healtbox_manager.start_discovery()

        # roomID is used as a placeholder for the room number, this is replaced through _define_sensors_with_rooms function
        self.sensorsGeneral =   [
                {
                    'sensor_id'        :'roomID - indoor temperature[roomID]_HealthBox 3[Healthbox3] - temperature',
                    'sensor_name'      :'Temperature Room roomID',
                    'physical_quantity':'temperature',
                    'unit'             :'celcius',
                },
                {
                    'sensor_id'        :'roomID - indoor relative humidity[roomID]_HealthBox 3[Healthbox3] - humidity',
                    'sensor_name'      :'Humidity Room roomID',
                    'physical_quantity':'humidity',
                    'unit'             :'percent',
                },
                {
                    'sensor_id'        :'roomID - indoor air quality[roomID]_HealthBox 3[Healthbox3] - co2',
                    'sensor_name'      :'CO2 Room roomID',
                    'physical_quantity':'co2',
                    'unit'             :'parts_per_million',
                },
                {
                    'sensor_id'        :'roomID - indoor CO2[roomID]_HealthBox 3[Healthbox3] - concentration',
                    'sensor_name'      :'CO2 Room roomID',
                    'physical_quantity':'co2',
                    'unit'             :'parts_per_million',
                },
                {
                    'sensor_id'        :'roomID - indoor volatile organic compounds[roomID]_HealthBox 3[Healthbox3] - concentration',
                    'sensor_name'      :'VOC Room roomID',
                    'physical_quantity':'voc',
                    'unit'             :'parts_per_million',
                },

            ]
Example #13
0
    def test_constructor_int(self):
        """ Test for the constructor for int. """
        PluginConfigChecker([{
            'name': 'port',
            'type': 'int',
            'description': 'Port on the server.'
        }])
        PluginConfigChecker([{'name': 'port', 'type': 'int'}])

        try:
            PluginConfigChecker([{'type': 'int'}])
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('name' in str(exception))
Example #14
0
    def test_constructor_str(self):
        """ Test for the constructor for str. """
        PluginConfigChecker([{
            'name': 'hostname',
            'type': 'str',
            'description': 'The hostname of the server.'
        }])
        PluginConfigChecker([{'name': 'hostname', 'type': 'str'}])

        try:
            PluginConfigChecker([{'type': 'str'}])
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('name' in str(exception))
Example #15
0
    def __init__(self, webinterface, logger):
        super(Astro, self).__init__(webinterface, logger)
        self.logger('Starting Astro plugin...')

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

        pytz_egg = '/opt/openmotics/python/plugins/Astro/pytz-2017.2-py2.7.egg'
        if pytz_egg not in sys.path:
            sys.path.insert(0, pytz_egg)

        self._read_config()

        self.logger("Started Astro plugin")
Example #16
0
    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")
Example #17
0
    def test_constructor_password(self):
        """ Test for the constructor for bool. """
        PluginConfigChecker([{
            'name': 'password',
            'type': 'password',
            'description': 'A password.'
        }])
        PluginConfigChecker([{'name': 'password', 'type': 'password'}])

        try:
            PluginConfigChecker([{'type': 'password'}])
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('name' in str(exception))
Example #18
0
    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")
Example #19
0
    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")
Example #20
0
    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")
Example #21
0
    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")
Example #22
0
    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")
Example #23
0
    def __init__(self, webinterface, logger):
        super(SMAWebConnect, self).__init__(webinterface, logger)
        self.logger('Starting SMAWebConnect plugin...')
        self._config = self.read_config(SMAWebConnect.default_config)
        self._config_checker = PluginConfigChecker(SMAWebConnect.config_description)
        self._metrics_queue = deque()
        self._enabled = False
        self._sample_rate = 30
        self._sma_devices = {}
        self._sma_sid = {}
        self._read_config()

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

        self.logger("Started SMAWebConnect plugin")
Example #24
0
    def test_constructor_bool(self):
        """ Test for the constructor for bool. """
        PluginConfigChecker([{
            'name':
            'use_auth',
            'type':
            'bool',
            'description':
            'Use authentication while connecting.'
        }])
        PluginConfigChecker([{'name': 'use_auth', 'type': 'bool'}])

        try:
            PluginConfigChecker([{'type': 'bool'}])
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('name' in str(exception))
Example #25
0
    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")
Example #26
0
    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")
Example #27
0
    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")
Example #28
0
    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")
Example #29
0
    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")
Example #30
0
    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")
Example #31
0
    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")
Example #32
0
    def __init__(self, webinterface, logger):
        super(Astro, self).__init__(webinterface, logger)
        self.logger('Starting Astro plugin...')

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

        pytz_egg = '/opt/openmotics/python/plugins/Astro/pytz-2017.2-py2.7.egg'
        if pytz_egg not in sys.path:
            sys.path.insert(0, pytz_egg)

        self._read_config()

        self.logger("Started Astro plugin")
Example #33
0
    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")
Example #34
0
    def test_constructor_error(self):
        """ Test with an invalid data type """
        try:
            PluginConfigChecker({'test': 123})
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('list' in str(exception))

        try:
            PluginConfigChecker([{'test': 123}])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('name' in str(exception))

        try:
            PluginConfigChecker([{'name': 123}])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('name' in str(exception)
                            and 'string' in str(exception))

        try:
            PluginConfigChecker([{'name': 'test'}])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('type' in str(exception))

        try:
            PluginConfigChecker([{'name': 'test', 'type': 123}])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('type' in str(exception)
                            and 'string' in str(exception))

        try:
            PluginConfigChecker([{'name': 'test', 'type': 'something_else'}])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('type' in str(exception)
                            and 'something_else' in str(exception))

        try:
            PluginConfigChecker([{
                'name': 'test',
                'type': 'str',
                'description': []
            }])
            self.fail("Expected PluginException")
        except PluginException as exception:
            self.assertTrue('description' in str(exception)
                            and 'string' in str(exception))
Example #35
0
    def test_constructor_enum(self):
        """ Test for the constructor for enum. """
        PluginConfigChecker([{
            'name': 'enumtest',
            'type': 'enum',
            'description': 'Test for enum',
            'choices': ['First', 'Second']
        }])
        PluginConfigChecker([{
            'name': 'enumtest',
            'type': 'enum',
            'choices': ['First', 'Second']
        }])

        try:
            PluginConfigChecker([{
                'name': 'enumtest',
                'type': 'enum',
                'choices': 'First'
            }])
            self.fail('Excepted exception')
        except PluginException as exception:
            self.assertTrue('choices' in str(exception)
                            and 'list' in str(exception))
Example #36
0
    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")
Example #37
0
    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")
Example #38
0
    def __init__(self, webinterface, logger):
        super(Ventilation, self).__init__(webinterface, logger)
        self.logger('Starting Ventilation plugin...')

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

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

        self._read_config()
        self._load_sensors()

        self.logger("Started Ventilation plugin")
Example #39
0
    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")
Example #40
0
    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")
Example #41
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 = '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})
Example #42
0
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})
Example #43
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})
Example #44
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})
Example #45
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})
Example #46
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})
Example #47
0
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})
Example #48
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})
Example #49
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})
Example #50
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})
Example #51
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})
Example #52
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})
Example #53
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})