コード例 #1
0
ファイル: observer.py プロジェクト: rubengr/gateway
    def __init__(self,
                 master_communicator=INJECTED,
                 master_controller=INJECTED,
                 message_client=INJECTED,
                 shutter_controller=INJECTED):
        """
        :param master_communicator: Master communicator
        :type master_communicator: master.master_communicator.MasterCommunicator
        :param master_controller: Master controller
        :type master_controller: gateway.master_controller.MasterController
        :param message_client: MessageClient instance
        :type message_client: bus.om_bus_client.MessageClient
        :param shutter_controller: Shutter Controller
        :type shutter_controller: gateway.shutters.ShutterController
        """
        self._master_communicator = master_communicator
        self._master_controller = master_controller
        self._message_client = message_client
        self._gateway_api = None

        self._master_subscriptions = {
            Observer.LegacyMasterEvents.ON_SHUTTER_UPDATE: [],
            Observer.LegacyMasterEvents.ON_INPUT_CHANGE: [],
            Observer.LegacyMasterEvents.ONLINE: []
        }
        self._event_subscriptions = []

        self._input_status = InputStatus(on_input_change=self._input_changed)
        self._thermostat_status = ThermostatStatus(
            on_thermostat_change=self._thermostat_changed,
            on_thermostat_group_change=self._thermostat_group_changed)
        self._shutter_controller = shutter_controller
        self._shutter_controller.set_shutter_changed_callback(
            self._shutter_changed)

        self._master_controller.subscribe_event(self._master_event)

        self._input_interval = 300
        self._input_last_updated = 0
        self._input_config = {}
        self._thermostats_original_interval = 30
        self._thermostats_interval = self._thermostats_original_interval
        self._thermostats_last_updated = 0
        self._thermostats_restore = 0
        self._thermostats_config = {}
        self._shutters_interval = 600
        self._shutters_last_updated = 0
        self._master_online = False
        self._background_consumers_registered = False
        self._master_version = None

        self._thread = Thread(target=self._monitor)
        self._thread.daemon = True
コード例 #2
0
    def test_timeout(self):
        """ Test timeout of InputStatus data. """
        inps = InputStatus(5, 1)
        inps.set_input({'input': 1, 'status': 1})
        self.assertEquals([1], inps.get_recent())

        time.sleep(0.8)

        inps.set_input({'input': 2, 'status': 1})
        self.assertEquals([1, 2], inps.get_recent())

        time.sleep(0.3)

        self.assertEquals([2], inps.get_recent())
コード例 #3
0
ファイル: inputs_tests.py プロジェクト: wash34000/gateway
    def test_timeout(self):
        """ Test timeout of InputStatus data. """
        inps = InputStatus(5, 1)
        inps.add_data(1)
        self.assertEquals([1], inps.get_status())

        time.sleep(0.8)

        inps.add_data(2)
        self.assertEquals([1, 2], inps.get_status())

        time.sleep(0.3)

        self.assertEquals([2], inps.get_status())
コード例 #4
0
ファイル: inputs_tests.py プロジェクト: openmotics/gateway
    def test_timeout(self):
        """ Test timeout of InputStatus data. """
        inps = InputStatus(5, 1)
        inps.add_data(1)
        self.assertEquals([1], inps.get_status())

        time.sleep(0.8)

        inps.add_data(2)
        self.assertEquals([1, 2], inps.get_status())

        time.sleep(0.3)

        self.assertEquals([2], inps.get_status())
コード例 #5
0
    def test_set_input_without_status(self):
        changed = []

        def on_input_change(input_id, status):
            changed.append(input_id)

        inps = InputStatus(on_input_change=on_input_change)
        inps.set_input({'input': 6, 'status': 1})
        current_status = inps.get_input(6)
        self.assertEquals(current_status['status'], True)
        inps.set_input({'input': 6})
        current_status = inps.get_input(6)
        self.assertEquals(current_status['status'], None)
        self.assertEquals(len(changed), 2)
コード例 #6
0
    def test_on_changed(self):
        changed = []

        def on_input_change(input_id, status):
            changed.append(input_id)

        inps = InputStatus(on_input_change=on_input_change)
        inps.set_input({'input': 6, 'status': 0})
        inps.set_input({'input': 6, 'status': 1})
        inps.set_input({'input': 6, 'status': 1})
        inps.set_input({'input': 6, 'status': 0})
        inps.set_input({'input': 6, 'status': 0})
        inps.set_input({'input': 6, 'status': 1})
        self.assertEquals(len(changed), 4)
コード例 #7
0
ファイル: observer.py プロジェクト: openmotics/gateway
    def __init__(self, master_communicator, dbus_service):
        """
        :param master_communicator: Master communicator
        :type master_communicator: master.master_communicator.MasterCommunicator
        :param dbus_service: DBusService instance
        :type dbus_service: bus.dbus_service.DBusService
        """
        self._master_communicator = master_communicator
        self._dbus_service = dbus_service
        self._gateway_api = None

        self._master_subscriptions = {Observer.MasterEvents.ON_OUTPUTS: [],
                                      Observer.MasterEvents.ON_SHUTTER_UPDATE: [],
                                      Observer.MasterEvents.INPUT_TRIGGER: []}
        self._event_subscriptions = []

        self._input_status = InputStatus()
        self._output_status = OutputStatus(on_output_change=self._output_changed)
        self._thermostat_status = ThermostatStatus(on_thermostat_change=self._thermostat_changed,
                                                   on_thermostat_group_change=self._thermostat_group_changed)
        self._shutter_status = ShutterStatus(on_shutter_change=self._shutter_changed)

        self._output_interval = 600
        self._output_last_updated = 0
        self._output_config = {}
        self._thermostats_original_interval = 30
        self._thermostats_interval = self._thermostats_original_interval
        self._thermostats_last_updated = 0
        self._thermostats_restore = 0
        self._thermostats_config = {}
        self._shutters_interval = 600
        self._shutters_last_updated = 0

        self._thread = Thread(target=self._monitor)
        self._thread.daemon = True

        self._master_communicator.register_consumer(BackgroundConsumer(master_api.output_list(), 0, self._on_output, True))
        self._master_communicator.register_consumer(BackgroundConsumer(master_api.input_list(), 0, self._on_input))
        self._master_communicator.register_consumer(BackgroundConsumer(master_api.shutter_status(), 0, self._on_shutter_update))
コード例 #8
0
ファイル: inputs_tests.py プロジェクト: openmotics/gateway
    def test_add(self):
        """ Test adding data to the InputStatus. """
        inps = InputStatus(5, 300)
        inps.add_data(1)
        self.assertEquals([1], inps.get_status())

        inps.add_data(2)
        self.assertEquals([1, 2], inps.get_status())

        inps.add_data(3)
        self.assertEquals([1, 2, 3], inps.get_status())

        inps.add_data(4)
        self.assertEquals([1, 2, 3, 4], inps.get_status())

        inps.add_data(5)
        self.assertEquals([1, 2, 3, 4, 5], inps.get_status())

        inps.add_data(6)
        self.assertEquals([2, 3, 4, 5, 6], inps.get_status())

        inps.add_data(7)
        self.assertEquals([3, 4, 5, 6, 7], inps.get_status())
コード例 #9
0
ファイル: observer.py プロジェクト: rubengr/gateway
class Observer(object):
    """
    The Observer gets various (change) events and will also monitor certain datasets to manually detect changes
    """

    # TODO: Needs to be removed and replace by MasterEvents from the MasterController
    class LegacyMasterEvents(object):
        ON_SHUTTER_UPDATE = 'ON_SHUTTER_UPDATE'
        ON_INPUT_CHANGE = 'INPUT_CHANGE'
        ONLINE = 'ONLINE'

    class Types(object):
        THERMOSTATS = 'THERMOSTATS'
        SHUTTERS = 'SHUTTERS'

    @Inject
    def __init__(self,
                 master_communicator=INJECTED,
                 master_controller=INJECTED,
                 message_client=INJECTED,
                 shutter_controller=INJECTED):
        """
        :param master_communicator: Master communicator
        :type master_communicator: master.master_communicator.MasterCommunicator
        :param master_controller: Master controller
        :type master_controller: gateway.master_controller.MasterController
        :param message_client: MessageClient instance
        :type message_client: bus.om_bus_client.MessageClient
        :param shutter_controller: Shutter Controller
        :type shutter_controller: gateway.shutters.ShutterController
        """
        self._master_communicator = master_communicator
        self._master_controller = master_controller
        self._message_client = message_client
        self._gateway_api = None

        self._master_subscriptions = {
            Observer.LegacyMasterEvents.ON_SHUTTER_UPDATE: [],
            Observer.LegacyMasterEvents.ON_INPUT_CHANGE: [],
            Observer.LegacyMasterEvents.ONLINE: []
        }
        self._event_subscriptions = []

        self._input_status = InputStatus(on_input_change=self._input_changed)
        self._thermostat_status = ThermostatStatus(
            on_thermostat_change=self._thermostat_changed,
            on_thermostat_group_change=self._thermostat_group_changed)
        self._shutter_controller = shutter_controller
        self._shutter_controller.set_shutter_changed_callback(
            self._shutter_changed)

        self._master_controller.subscribe_event(self._master_event)

        self._input_interval = 300
        self._input_last_updated = 0
        self._input_config = {}
        self._thermostats_original_interval = 30
        self._thermostats_interval = self._thermostats_original_interval
        self._thermostats_last_updated = 0
        self._thermostats_restore = 0
        self._thermostats_config = {}
        self._shutters_interval = 600
        self._shutters_last_updated = 0
        self._master_online = False
        self._background_consumers_registered = False
        self._master_version = None

        self._thread = Thread(target=self._monitor)
        self._thread.daemon = True

    def set_gateway_api(self, gateway_api):
        """
        Sets the Gateway API instance
        :param gateway_api: Gateway API (business logic)
        :type gateway_api: gateway.gateway_api.GatewayApi
        """
        self._gateway_api = gateway_api

    def subscribe_master(self, event, callback):
        """
        Subscribes a callback to a certain event
        :param event: The event on which to call the callback
        :param callback: The callback to call
        """
        self._master_subscriptions[event].append(callback)

    def subscribe_events(self, callback):
        """
        Subscribes a callback to generic events
        :param callback: the callback to call
        """
        self._event_subscriptions.append(callback)

    def start(self):
        """ Starts the monitoring thread """
        self._ensure_gateway_api()
        if Platform.get_platform() == Platform.Type.CLASSIC:
            self._thread.start()

    def invalidate_cache(self, object_type=None):
        """
        Triggered when an external service knows certain settings might be changed in the background.
        For example: maintenance mode or module discovery
        """
        if object_type is None or object_type == Observer.Types.THERMOSTATS:
            self._thermostats_last_updated = 0
        if object_type is None or object_type == Observer.Types.SHUTTERS:
            self._shutters_last_updated = 0
        self._master_controller.invalidate_caches()

    def increase_interval(self, object_type, interval, window):
        """ Increases a certain interval to a new setting for a given amount of time """
        if object_type == Observer.Types.THERMOSTATS:
            self._thermostats_interval = interval
            self._thermostats_restore = time.time() + window

    def _monitor(self):
        """ Monitors certain system states to detect changes without events """
        while True:
            try:
                self._check_master_version()
                # Refresh if required
                if self._thermostats_last_updated + self._thermostats_interval < time.time(
                ):
                    self._refresh_thermostats()
                    self._set_master_state(True)
                if self._shutters_last_updated + self._shutters_interval < time.time(
                ):
                    self._refresh_shutters()
                    self._set_master_state(True)
                if self._input_last_updated + self._input_interval < time.time(
                ):
                    self._refresh_inputs()
                    self._set_master_state(True)
                # Restore interval if required
                if self._thermostats_restore < time.time():
                    self._thermostats_interval = self._thermostats_original_interval
                self._register_background_consumers()
                time.sleep(1)
            except CommunicationTimedOutException:
                logger.error(
                    'Got communication timeout during monitoring, waiting 10 seconds.'
                )
                self._set_master_state(False)
                time.sleep(10)
            except InMaintenanceModeException:
                # This is an expected situation
                time.sleep(10)
            except Exception as ex:
                logger.exception(
                    'Unexpected error during monitoring: {0}'.format(ex))
                time.sleep(10)

    def _ensure_gateway_api(self):
        if self._gateway_api is None:
            raise RuntimeError(
                'The observer has no access to the Gateway API yet')

    def _check_master_version(self):
        if self._master_version is None:
            self._master_version = self._gateway_api.get_master_version()
            self._set_master_state(True)

    def _set_master_state(self, online):
        if online != self._master_online:
            self._master_online = online
            # Notify subscribers
            for callback in self._master_subscriptions[
                    Observer.LegacyMasterEvents.ONLINE]:
                callback(online)

    # Handle master "events"

    def _register_background_consumers(self):
        if self._master_version and not self._background_consumers_registered:
            if Platform.get_platform() == Platform.Type.CLASSIC:
                # This import/code will eventually be migrated away to MasterControllers
                from master.master_communicator import BackgroundConsumer
                self._master_communicator.register_consumer(
                    BackgroundConsumer(
                        master_api.input_list(self._master_version), 0,
                        self._on_input))
                self._master_communicator.register_consumer(
                    BackgroundConsumer(
                        master_api.shutter_status(self._master_version), 0,
                        self._on_shutter_update))
                self._background_consumers_registered = True

    # Handle master "events"

    def _on_input(self, data):
        """ Triggers when the master informs us of an Input state change """
        # Update status tracker
        self._input_status.set_input(data)
        # Notify subscribers
        for callback in self._master_subscriptions[
                Observer.LegacyMasterEvents.ON_INPUT_CHANGE]:
            callback(data)

    def _on_shutter_update(self, data):
        """ Triggers when the master informs us of an Shutter state change """
        # Update status tracker
        self._shutter_controller.update_from_master_state(data)
        # Notify subscribers
        for callback in self._master_subscriptions[
                Observer.LegacyMasterEvents.ON_SHUTTER_UPDATE]:
            callback(self._shutter_controller.get_states())

    def _master_event(self, master_event):
        """
        Triggers when the MasterController generates events
        :type master_event: gateway.hal.master_controller.MasterEvent
        """
        if master_event.type == MasterEvent.Types.OUTPUT_CHANGE:
            self._message_client.send_event(OMBusEvents.OUTPUT_CHANGE,
                                            {'id': master_event.data['id']})
            for callback in self._event_subscriptions:
                callback(
                    Event(event_type=Event.Types.OUTPUT_CHANGE,
                          data=master_event.data))

    # Outputs

    def get_outputs(self):
        """ Returns a list of Outputs with their status """
        return self._master_controller.get_output_statuses()

    # Inputs

    def get_inputs(self):
        """ Returns a list of Inputs with their status """
        self._ensure_gateway_api()
        return self._input_status.get_inputs()

    def get_recent(self):
        """ Returns a list of recently changed inputs """
        self._ensure_gateway_api()
        return self._input_status.get_recent()

    def _input_changed(self, input_id, status):
        """ Executed by the Input Status tracker when an input changed state """
        for callback in self._event_subscriptions:
            resp_data = {
                'id': input_id,
                'status': status,
                'location': {
                    'room_id': self._input_config[input_id]['room']
                }
            }
            callback(Event(event_type=Event.Types.INPUT_CHANGE,
                           data=resp_data))

    def _refresh_inputs(self):
        """ Refreshes the Input Status tracker """
        # 1. refresh input configuration
        self._input_config = {
            input_configuration['id']: input_configuration
            for input_configuration in
            self._gateway_api.get_input_configurations()
        }
        # 2. poll for latest input status
        try:
            number_of_input_modules = self._master_communicator.do_command(
                master_api.number_of_io_modules())['in']
            inputs = []
            for i in xrange(number_of_input_modules):
                # we could be dealing with e.g. a temperature module, skip those
                module_type = self._gateway_api.get_input_module_type(
                    input_module_id=i)
                if module_type not in ['i', 'I']:
                    continue
                result = self._master_communicator.do_command(
                    master_api.read_input_module(self._master_version),
                    {'input_module_nr': i})
                module_status = result['input_status']
                # module_status byte contains bits for each individual input, use mask and bitshift to get status
                for n in xrange(8):
                    input_nr = i * 8 + n
                    input_status = module_status & (1 << n) != 0
                    data = {'input': input_nr, 'status': input_status}
                    inputs.append(data)
            self._input_status.full_update(inputs)
        except NotImplementedError as e:
            logger.error('Cannot refresh inputs: {}'.format(e))
        self._input_last_updated = time.time()

    # Shutters

    def get_shutter_status(self):
        return self._shutter_controller.get_states()

    def _shutter_changed(self, shutter_id, shutter_data, shutter_state):
        """ Executed by the Shutter Status tracker when a shutter changed state """
        for callback in self._event_subscriptions:
            callback(
                Event(event_type=Event.Types.SHUTTER_CHANGE,
                      data={
                          'id': shutter_id,
                          'status': {
                              'state': shutter_state
                          },
                          'location': {
                              'room_id': shutter_data['room']
                          }
                      }))

    def _refresh_shutters(self):
        """ Refreshes the Shutter status tracker """
        number_of_shutter_modules = self._master_communicator.do_command(
            master_api.number_of_io_modules())['shutter']
        self._shutter_controller.update_config(
            self._gateway_api.get_shutter_configurations())
        for module_id in xrange(number_of_shutter_modules):
            self._shutter_controller.update_from_master_state({
                'module_nr':
                module_id,
                'status':
                self._master_communicator.do_command(
                    master_api.shutter_status(self._master_version),
                    {'module_nr': module_id})['status']
            })
        self._shutters_last_updated = time.time()

    # Thermostats

    def get_thermostats(self):
        """ Returns thermostat information """
        self._ensure_gateway_api()
        self._refresh_thermostats()  # Always return the latest information
        return self._thermostat_status.get_thermostats()

    def _thermostat_changed(self, thermostat_id, status):
        """ Executed by the Thermostat Status tracker when an output changed state """
        self._message_client.send_event(OMBusEvents.THERMOSTAT_CHANGE,
                                        {'id': thermostat_id})
        location = {'room_id': self._thermostats_config[thermostat_id]['room']}
        for callback in self._event_subscriptions:
            callback(
                Event(event_type=Event.Types.THERMOSTAT_CHANGE,
                      data={
                          'id': thermostat_id,
                          'status': {
                              'preset': status['preset'],
                              'current_setpoint': status['current_setpoint'],
                              'actual_temperature':
                              status['actual_temperature'],
                              'output_0': status['output_0'],
                              'output_1': status['output_1']
                          },
                          'location': location
                      }))

    def _thermostat_group_changed(self, status):
        self._message_client.send_event(OMBusEvents.THERMOSTAT_CHANGE,
                                        {'id': None})
        for callback in self._event_subscriptions:
            callback(
                Event(event_type=Event.Types.THERMOSTAT_GROUP_CHANGE,
                      data={
                          'id': 0,
                          'status': {
                              'state': status['state'],
                              'mode': status['mode']
                          },
                          'location': {}
                      }))

    def _refresh_thermostats(self):
        """
        Get basic information about all thermostats and pushes it in to the Thermostat Status tracker
        """
        def get_automatic_setpoint(_mode):
            _automatic = bool(_mode & 1 << 3)
            return _automatic, 0 if _automatic else (_mode & 0b00000111)

        thermostat_info = self._master_communicator.do_command(
            master_api.thermostat_list())
        thermostat_mode = self._master_communicator.do_command(
            master_api.thermostat_mode_list())
        aircos = self._master_communicator.do_command(
            master_api.read_airco_status_bits())
        outputs = self.get_outputs()

        mode = thermostat_info['mode']
        thermostats_on = bool(mode & 1 << 7)
        cooling = bool(mode & 1 << 4)
        automatic, setpoint = get_automatic_setpoint(thermostat_mode['mode0'])

        fields = ['sensor', 'output0', 'output1', 'name', 'room']
        if cooling:
            self._thermostats_config = self._gateway_api.get_cooling_configurations(
                fields=fields)
        else:
            self._thermostats_config = self._gateway_api.get_thermostat_configurations(
                fields=fields)

        thermostats = []
        for thermostat_id in xrange(32):
            config = self._thermostats_config[thermostat_id]
            if (config['sensor'] <= 31
                    or config['sensor'] == 240) and config['output0'] <= 240:
                t_mode = thermostat_mode['mode{0}'.format(thermostat_id)]
                t_automatic, t_setpoint = get_automatic_setpoint(t_mode)
                thermostat = {
                    'id':
                    thermostat_id,
                    'act':
                    thermostat_info['tmp{0}'.format(
                        thermostat_id)].get_temperature(),
                    'csetp':
                    thermostat_info['setp{0}'.format(
                        thermostat_id)].get_temperature(),
                    'outside':
                    thermostat_info['outside'].get_temperature(),
                    'mode':
                    t_mode,
                    'automatic':
                    t_automatic,
                    'setpoint':
                    t_setpoint,
                    'name':
                    config['name'],
                    'sensor_nr':
                    config['sensor'],
                    'airco':
                    aircos['ASB{0}'.format(thermostat_id)]
                }
                for output in [0, 1]:
                    output_nr = config['output{0}'.format(output)]
                    if output_nr < len(
                            outputs) and outputs[output_nr]['status']:
                        thermostat['output{0}'.format(
                            output)] = outputs[output_nr]['dimmer']
                    else:
                        thermostat['output{0}'.format(output)] = 0
                thermostats.append(thermostat)

        self._thermostat_status.full_update({
            'thermostats_on': thermostats_on,
            'automatic': automatic,
            'setpoint': setpoint,
            'cooling': cooling,
            'status': thermostats
        })
        self._thermostats_last_updated = time.time()
コード例 #10
0
    def test_get_recent(self):
        """ Test adding data to the InputStatus. """
        inps = InputStatus()
        with mock.patch.object(time, 'time', return_value=10):
            inps.set_input({'input': 1, 'status': 1})
            self.assertEqual([1], inps.get_recent())

        with mock.patch.object(time, 'time', return_value=30):
            for i in xrange(2, 10):
                inps.set_input({'input': i, 'status': 1})
            self.assertEqual(5, len(inps.get_recent()))

        with mock.patch.object(time, 'time', return_value=60):
            self.assertEqual(0, len(inps.get_recent()))

        with mock.patch.object(time, 'time', return_value=35):
            inps.set_input({'input': 1, 'status': 0})
            inps.set_input({'input': 2, 'status': 1})
            self.assertIn(1, inps.get_recent())
            self.assertNotIn(2, inps.get_recent())
コード例 #11
0
    def test_add(self):
        """ Test adding data to the InputStatus. """
        inps = InputStatus()
        inps.set_input({'input': 1, 'status': 1})
        states = [{k: v
                   for k, v in x.items() if k in ('id', 'status')}
                  for x in inps.get_inputs()]
        self.assertEqual(len(states), 1)
        self.assertIn({'id': 1, 'status': True}, states)

        inps.set_input({'input': 2, 'status': 1})
        states = [{k: v
                   for k, v in x.items() if k in ('id', 'status')}
                  for x in inps.get_inputs()]
        self.assertEqual(len(states), 2)
        self.assertIn({'id': 2, 'status': True}, states)
        self.assertIn({'id': 1, 'status': True}, states)

        inps.set_input({'input': 3, 'status': 0})
        states = [{k: v
                   for k, v in x.items() if k in ('id', 'status')}
                  for x in inps.get_inputs()]
        self.assertEqual(len(states), 3)
        self.assertIn({'id': 3, 'status': False}, states)
        self.assertIn({'id': 1, 'status': True}, states)
コード例 #12
0
ファイル: inputs_tests.py プロジェクト: rubengr/gateway
    def test_add(self):
        """ Test adding data to the InputStatus. """
        inps = InputStatus(5, 300)
        inps.set_input({'input': 1, 'status': 1})
        self.assertEquals([1], inps.get_recent())

        inps.set_input({'input': 2, 'status': 1})
        self.assertEquals([1, 2], inps.get_recent())

        inps.set_input({'input': 3, 'status': 1})
        self.assertEquals([1, 2, 3], inps.get_recent())

        inps.set_input({'input': 4, 'status': 1})
        self.assertEquals([1, 2, 3, 4], inps.get_recent())

        inps.set_input({'input': 5, 'status': 1})
        self.assertEquals([1, 2, 3, 4, 5], inps.get_recent())

        inps.set_input({'input': 6, 'status': 1})
        self.assertEquals([2, 3, 4, 5, 6], inps.get_recent())

        inps.set_input({'input': 7, 'status': 1})
        self.assertEquals([3, 4, 5, 6, 7], inps.get_recent())
コード例 #13
0
ファイル: inputs_tests.py プロジェクト: wash34000/gateway
    def test_add(self):
        """ Test adding data to the InputStatus. """
        inps = InputStatus(5, 300)
        inps.add_data(1)
        self.assertEquals([1], inps.get_status())

        inps.add_data(2)
        self.assertEquals([1, 2], inps.get_status())

        inps.add_data(3)
        self.assertEquals([1, 2, 3], inps.get_status())

        inps.add_data(4)
        self.assertEquals([1, 2, 3, 4], inps.get_status())

        inps.add_data(5)
        self.assertEquals([1, 2, 3, 4, 5], inps.get_status())

        inps.add_data(6)
        self.assertEquals([2, 3, 4, 5, 6], inps.get_status())

        inps.add_data(7)
        self.assertEquals([3, 4, 5, 6, 7], inps.get_status())
コード例 #14
0
ファイル: observer.py プロジェクト: openmotics/gateway
class Observer(object):
    """
    The Observer gets various (change) events and will also monitor certain datasets to manually detect changes
    """

    class MasterEvents(object):
        ON_OUTPUTS = 'ON_OUTPUTS'
        ON_SHUTTER_UPDATE = 'ON_SHUTTER_UPDATE'
        INPUT_TRIGGER = 'INPUT_TRIGGER'

    class Types(object):
        OUTPUTS = 'OUTPUTS'
        THERMOSTATS = 'THERMOSTATS'
        SHUTTERS = 'SHUTTERS'

    def __init__(self, master_communicator, dbus_service):
        """
        :param master_communicator: Master communicator
        :type master_communicator: master.master_communicator.MasterCommunicator
        :param dbus_service: DBusService instance
        :type dbus_service: bus.dbus_service.DBusService
        """
        self._master_communicator = master_communicator
        self._dbus_service = dbus_service
        self._gateway_api = None

        self._master_subscriptions = {Observer.MasterEvents.ON_OUTPUTS: [],
                                      Observer.MasterEvents.ON_SHUTTER_UPDATE: [],
                                      Observer.MasterEvents.INPUT_TRIGGER: []}
        self._event_subscriptions = []

        self._input_status = InputStatus()
        self._output_status = OutputStatus(on_output_change=self._output_changed)
        self._thermostat_status = ThermostatStatus(on_thermostat_change=self._thermostat_changed,
                                                   on_thermostat_group_change=self._thermostat_group_changed)
        self._shutter_status = ShutterStatus(on_shutter_change=self._shutter_changed)

        self._output_interval = 600
        self._output_last_updated = 0
        self._output_config = {}
        self._thermostats_original_interval = 30
        self._thermostats_interval = self._thermostats_original_interval
        self._thermostats_last_updated = 0
        self._thermostats_restore = 0
        self._thermostats_config = {}
        self._shutters_interval = 600
        self._shutters_last_updated = 0

        self._thread = Thread(target=self._monitor)
        self._thread.daemon = True

        self._master_communicator.register_consumer(BackgroundConsumer(master_api.output_list(), 0, self._on_output, True))
        self._master_communicator.register_consumer(BackgroundConsumer(master_api.input_list(), 0, self._on_input))
        self._master_communicator.register_consumer(BackgroundConsumer(master_api.shutter_status(), 0, self._on_shutter_update))

    def set_gateway_api(self, gateway_api):
        """
        Sets the Gateway API instance
        :param gateway_api: Gateway API (business logic)
        :type gateway_api: gateway.gateway_api.GatewayApi
        """
        self._gateway_api = gateway_api

    def subscribe_master(self, event, callback):
        """
        Subscribes a callback to a certain event
        :param event: The event on which to call the callback
        :param callback: The callback to call
        """
        self._master_subscriptions[event].append(callback)

    def subscribe_events(self, callback):
        """
        Subscribes a callback to generic events
        :param callback: the callback to call
        """
        self._event_subscriptions.append(callback)

    def start(self):
        """ Starts the monitoring thread """
        self._ensure_gateway_api()
        self._thread.start()

    def invalidate_cache(self, object_type=None):
        """
        Triggered when an external service knows certain settings might be changed in the background.
        For example: maintenance mode or module discovery
        """
        if object_type is None or object_type == Observer.Types.OUTPUTS:
            self._output_last_updated = 0
        if object_type is None or object_type == Observer.Types.THERMOSTATS:
            self._thermostats_last_updated = 0
        if object_type is None or object_type == Observer.Types.SHUTTERS:
            self._shutters_last_updated = 0

    def increase_interval(self, object_type, interval, window):
        """ Increases a certain interval to a new setting for a given amount of time """
        if object_type == Observer.Types.THERMOSTATS:
            self._thermostats_interval = interval
            self._thermostats_restore = time.time() + window

    def _monitor(self):
        """ Monitors certain system states to detect changes without events """
        while True:
            try:
                # Refresh if required
                if self._thermostats_last_updated + self._thermostats_interval < time.time():
                    self._refresh_thermostats()
                if self._output_last_updated + self._output_interval < time.time():
                    self._refresh_outputs()
                if self._shutters_last_updated + self._shutters_interval < time.time():
                    self._refresh_shutters()
                # Restore interval if required
                if self._thermostats_restore < time.time():
                    self._thermostats_interval = self._thermostats_original_interval
                time.sleep(1)
            except CommunicationTimedOutException:
                LOGGER.error('Got communication timeout during monitoring, waiting 10 seconds.')
                time.sleep(10)
            except Exception as ex:
                LOGGER.exception('Unexpected error during monitoring: {0}'.format(ex))
                time.sleep(10)

    def _ensure_gateway_api(self):
        if self._gateway_api is None:
            raise RuntimeError('The observer has no access to the Gateway API yet')

    # Handle master "events"

    def _on_output(self, data):
        """ Triggers when the master informs us of an Output state change """
        on_outputs = data['outputs']
        # Notify subscribers
        for callback in self._master_subscriptions[Observer.MasterEvents.ON_OUTPUTS]:
            callback(on_outputs)
        # Update status tracker
        self._output_status.partial_update(on_outputs)

    def _on_input(self, data):
        """ Triggers when the master informs us of an Input press """
        # Notify subscribers
        for callback in self._master_subscriptions[Observer.MasterEvents.INPUT_TRIGGER]:
            callback(data)
        for callback in self._event_subscriptions:
            callback(Event(event_type=Event.Types.INPUT_TRIGGER,
                           data={'id': data['input'],
                                 'location': {}}))
        # Update status tracker
        self._input_status.add_data((data['input'], data['output']))

    def _on_shutter_update(self, data):
        """ Triggers when the master informs us of an Shutter state change """
        # Update status tracker
        self._shutter_status.update_states(data)
        # Notify subscribers
        for callback in self._master_subscriptions[Observer.MasterEvents.ON_SHUTTER_UPDATE]:
            callback(self._shutter_status.get_states())

    # Outputs

    def get_outputs(self):
        """ Returns a list of Outputs with their status """
        self._ensure_gateway_api()
        return self._output_status.get_outputs()

    def _output_changed(self, output_id, status):
        """ Executed by the Output Status tracker when an output changed state """
        self._dbus_service.send_event(DBusEvents.OUTPUT_CHANGE, {'id': output_id})
        for callback in self._event_subscriptions:
            resp_status = {'on': status['on']}
            # 1. only add value to status when handling dimmers
            if self._output_config[output_id]['module_type'] in ['d', 'D']:
                resp_status['value'] = status['value']
            # 2. format response data
            resp_data = {'id': output_id,
                         'status': resp_status,
                         'location': {'room_id': self._output_config[output_id]['room']}}
            callback(Event(event_type=Event.Types.OUTPUT_CHANGE, data=resp_data))

    def _refresh_outputs(self):
        """ Refreshes the Output Status tracker """
        self._output_config = self._gateway_api.get_output_configurations()
        number_of_outputs = self._master_communicator.do_command(master_api.number_of_io_modules())['out'] * 8
        outputs = []
        for i in xrange(number_of_outputs):
            outputs.append(self._master_communicator.do_command(master_api.read_output(), {'id': i}))
        self._output_status.full_update(outputs)
        self._output_last_updated = time.time()

    # Inputs

    def get_input_status(self):
        self._ensure_gateway_api()
        return self._input_status.get_status()

    # Shutters

    def get_shutter_status(self):
        return self._shutter_status.get_states()

    def _shutter_changed(self, shutter_id, shutter_data, shutter_state):
        """ Executed by the Shutter Status tracker when a shutter changed state """
        for callback in self._event_subscriptions:
            callback(Event(event_type=Event.Types.SHUTTER_CHANGE,
                           data={'id': shutter_id,
                                 'status': {'state': shutter_state},
                                 'location': {'room_id': shutter_data['room']}}))

    def _refresh_shutters(self):
        """ Refreshes the Shutter status tracker """
        number_of_shutter_modules = self._master_communicator.do_command(master_api.number_of_io_modules())['shutter']
        self._shutter_status.update_config(self._gateway_api.get_shutter_configurations())
        for module_id in xrange(number_of_shutter_modules):
            self._shutter_status.update_states(
                {'module_nr': module_id,
                 'status': self._master_communicator.do_command(master_api.shutter_status(),
                                                                {'module_nr': module_id})['status']}
            )
        self._shutters_last_updated = time.time()

    # Thermostats

    def get_thermostats(self):
        """ Returns thermostat information """
        self._ensure_gateway_api()
        self._refresh_thermostats()  # Always return the latest information
        return self._thermostat_status.get_thermostats()

    def _thermostat_changed(self, thermostat_id, status):
        """ Executed by the Thermostat Status tracker when an output changed state """
        self._dbus_service.send_event(DBusEvents.THERMOSTAT_CHANGE, {'id': thermostat_id})
        location = {'room_id': self._thermostats_config[thermostat_id]['room']}
        for callback in self._event_subscriptions:
            callback(Event(event_type=Event.Types.THERMOSTAT_CHANGE,
                           data={'id': thermostat_id,
                                 'status': {'preset': status['preset'],
                                            'current_setpoint': status['current_setpoint'],
                                            'actual_temperature': status['actual_temperature'],
                                            'output_0': status['output_0'],
                                            'output_1': status['output_1']},
                                 'location': location}))

    def _thermostat_group_changed(self, status):
        self._dbus_service.send_event(DBusEvents.THERMOSTAT_CHANGE, {'id': None})
        for callback in self._event_subscriptions:
            callback(Event(event_type=Event.Types.THERMOSTAT_GROUP_CHANGE,
                           data={'id': 0,
                                 'status': {'state': status['state'],
                                            'mode': status['mode']},
                                 'location': {}}))

    def _refresh_thermostats(self):
        """
        Get basic information about all thermostats and pushes it in to the Thermostat Status tracker
        """
        def get_automatic_setpoint(_mode):
            _automatic = bool(_mode & 1 << 3)
            return _automatic, 0 if _automatic else (_mode & 0b00000111)

        thermostat_info = self._master_communicator.do_command(master_api.thermostat_list())
        thermostat_mode = self._master_communicator.do_command(master_api.thermostat_mode_list())
        aircos = self._master_communicator.do_command(master_api.read_airco_status_bits())
        outputs = self.get_outputs()

        mode = thermostat_info['mode']
        thermostats_on = bool(mode & 1 << 7)
        cooling = bool(mode & 1 << 4)
        automatic, setpoint = get_automatic_setpoint(thermostat_mode['mode0'])

        fields = ['sensor', 'output0', 'output1', 'name', 'room']
        if cooling:
            self._thermostats_config = self._gateway_api.get_cooling_configurations(fields=fields)
        else:
            self._thermostats_config = self._gateway_api.get_thermostat_configurations(fields=fields)

        thermostats = []
        for thermostat_id in xrange(32):
            config = self._thermostats_config[thermostat_id]
            if (config['sensor'] <= 31 or config['sensor'] == 240) and config['output0'] <= 240:
                t_mode = thermostat_mode['mode{0}'.format(thermostat_id)]
                t_automatic, t_setpoint = get_automatic_setpoint(t_mode)
                thermostat = {'id': thermostat_id,
                              'act': thermostat_info['tmp{0}'.format(thermostat_id)].get_temperature(),
                              'csetp': thermostat_info['setp{0}'.format(thermostat_id)].get_temperature(),
                              'outside': thermostat_info['outside'].get_temperature(),
                              'mode': t_mode,
                              'automatic': t_automatic,
                              'setpoint': t_setpoint,
                              'name': config['name'],
                              'sensor_nr': config['sensor'],
                              'airco': aircos['ASB{0}'.format(thermostat_id)]}
                for output in [0, 1]:
                    output_nr = config['output{0}'.format(output)]
                    if output_nr < len(outputs) and outputs[output_nr]['status']:
                        thermostat['output{0}'.format(output)] = outputs[output_nr]['dimmer']
                    else:
                        thermostat['output{0}'.format(output)] = 0
                thermostats.append(thermostat)

        self._thermostat_status.full_update({'thermostats_on': thermostats_on,
                                             'automatic': automatic,
                                             'setpoint': setpoint,
                                             'cooling': cooling,
                                             'status': thermostats})
        self._thermostats_last_updated = time.time()