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())
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())
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()