def encode_gadget(self, gadget: Gadget) -> dict: """ Serializes a gadget according to api specification :param gadget: The gadget to serialize :return: The serialized version of the gadget as dict :raises GadgetEncodeError: If anything goes wrong during the serialization process """ try: identifier = self.encode_gadget_identifier(gadget) except IdentifierEncodeError as err: self._logger.error(err.args[0]) raise GadgetEncodeError(gadget.__class__.__name__, gadget.get_name()) characteristics_json = [self.encode_characteristic(x) for x in gadget.get_characteristics()] mapping_json = {} for mapping in gadget.get_event_mapping(): if mapping.get_id() in mapping_json: self._logger.error(f"found double mapping for {mapping.get_id()}") continue mapping_json[mapping.get_id()] = mapping.get_list() gadget_json = {"type": int(identifier), "id": gadget.get_name(), "characteristics": characteristics_json, "event_map": mapping_json} return gadget_json
def merge_gadgets(self, old_gadget: Gadget, new_gadget: Gadget) -> Gadget: """ Merges two gadgets (typically one 'original' and a new one with update information) into a new gadget containing with name, client and class of the old and characteristics of the new one. :param old_gadget: Original gadget to base merged gadget on :param new_gadget: Gadget wth update information :return: The emrged gadget :raise GadgetMergeError: If anything goes wrong during merges """ encoder = ApiEncoder() if not isinstance( new_gadget, AnyGadget) and old_gadget.__class__ != new_gadget.__class__: raise GadgetMergeError( f"Cannot merge gadgets with different classes {old_gadget.__class__.__name__} " f"and {new_gadget.__class__.__name__}") try: gadget_type = encoder.encode_gadget_identifier(old_gadget) except IdentifierEncodeError as err: raise GadgetMergeError(err.args[0]) try: merged_gadget = self.create_gadget( gadget_type, old_gadget.get_name(), old_gadget.get_host_client(), new_gadget.get_characteristics()) except GadgetCreationError as err: raise GadgetMergeError(err.args[0]) return merged_gadget
def receive_gadget(self, gadget: Gadget): if self._last_published_gadget is not None and self._last_published_gadget == gadget.get_name( ): return fetched_gadget = self._fetch_gadget_data(gadget.get_name()) if fetched_gadget is None: # Gadget with given name does not exist on the remote system, or is broken somehow try: self.create_gadget(gadget) except GadgetCreationError as err: self._logger.error(err.args[0]) raise GadgetUpdateError(gadget.get_name()) else: if self._gadget_needs_update(gadget, fetched_gadget): # Gadget needs to be recreated due to characteristic boundaries changes try: self.remove_gadget(gadget.get_name()) except GadgetDeletionError as err: self._logger.error(err.args[0]) try: self.create_gadget(gadget) except GadgetCreationError as err: self._logger.error(err.args[0]) raise GadgetUpdateError(gadget.get_name()) else: # Gadget does not need to be re-created, only updated for characteristic in gadget.get_characteristics(): fetched_characteristic = fetched_gadget.get_characteristic( characteristic.get_type()) if fetched_characteristic.get_true_value( ) != characteristic.get_true_value(): self._update_characteristic(gadget, characteristic.get_type())
def receive_gadget(self, gadget: Gadget): found_gadget = self.get_gadget(gadget.get_name()) if found_gadget is None: if not isinstance(gadget, AnyGadget): self._logger.info(f"Adding gadget '{gadget.get_name()}'") self._gadgets.append(gadget) self._publish_gadget(gadget) else: self._logger.error( f"Received sync data for unknown gadget '{gadget.get_name()}'" ) else: if gadget.__class__ == found_gadget.__class__ and gadget.equals_in_characteristic_values( found_gadget): return self._logger.info(f"Syncing existing gadget '{gadget.get_name()}'") self._gadgets.remove(found_gadget) try: factory = GadgetFactory() merged_gadget = factory.merge_gadgets(found_gadget, gadget) except GadgetCreationError as err: self._logger.error(err.args[0]) return except NotImplementedError: self._logger.error( f"Merging gadgets of the type '{gadget.__class__.__name__}' is not implemented" ) return self._gadgets.append(merged_gadget) self._publish_gadget(merged_gadget)
def create_gadget(self, gadget: Gadget): self._logger.info( f"Creating gadget '{gadget.get_name()}' on external source") adding_successful = self._network_connector.add_gadget(gadget) if not adding_successful: raise GadgetCreationError(gadget.get_name()) for characteristic in gadget.get_characteristics(): self._update_characteristic(gadget, characteristic.get_type())
def _gadget_needs_update(local_gadget: Gadget, fetched_gadget: Gadget): """ Checks if the gadget on the remote storage needs an update by comparing the gadgets characteristics boundaries :param local_gadget: The local gadget (master) :param fetched_gadget: The fetched gadget to compare it with :return: Whether the fetched gadget needs to be updated """ needs_update = local_gadget.get_characteristics( ) != fetched_gadget.get_characteristics() return needs_update
def encode_gadget_update(self, gadget: Gadget) -> dict: """ Serializes gadget update information according to api specification :param gadget: The gadget to serialize :return: The serialized version of the changeable gadget information as dict :raises GadgetEncodeError: If anything goes wrong during the serialization process """ characteristics_json = [self.encode_characteristic_update(x) for x in gadget.get_characteristics()] gadget_json = {"id": gadget.get_name(), "characteristics": characteristics_json} return gadget_json
def test_api_handle_gadget_update(api: ApiManager, network: DummyNetworkConnector, delegate: DummyApiDelegate): delegate.add_gadget( Gadget(GADGET_NAME, "spongolopolus", [Characteristic(CharacteristicIdentifier.status, 0, 1, 1, 1)])) network.mock_receive(ApiURIs.update_gadget.uri, REQ_SENDER, {"gadget": { "yolo": "blub" }}) assert delegate.get_last_gadget_update() is None network.mock_receive(ApiURIs.update_gadget.uri, REQ_SENDER, GADGET_UPDATE_ERR) assert delegate.get_last_gadget_update() is None network.mock_receive(ApiURIs.update_gadget.uri, REQ_SENDER, GADGET_UPDATE_ERR_UNKNOWN) assert delegate.get_last_gadget_update() is None network.mock_receive(ApiURIs.update_gadget.uri, REQ_SENDER, GADGET_UPDATE_OK) assert delegate.get_last_gadget_update() is not None assert delegate.get_last_gadget_update().get_characteristic_types( )[0] == GADGET_CHARACTERISTIC_TYPE
def _update_characteristic(self, gadget: Gadget, characteristic: CharacteristicIdentifier): """ Updates a specific characteristic on from the gadget on the remote storage :param gadget: Gadget to get characteristic information from :param characteristic: Characteristic to update :return: None :raises CharacteristicParsingError: If selected characteristic could not be parsed correctly """ characteristic_value = gadget.get_characteristic( characteristic).get_true_value() characteristic_str = HomebridgeCharacteristicTranslator.type_to_string( characteristic) self._network_connector.update_characteristic(gadget.get_name(), characteristic_str, characteristic_value)
def receive_gadget_update(self, gadget: Gadget): found_gadget = self.get_gadget(gadget.get_name()) if found_gadget is None: self._logger.error( f"Received update data for unknown gadget '{gadget.get_name()}'" ) return changed_characteristics = [] for c in found_gadget.get_characteristics(): buf_characteristic = gadget.get_characteristic(c.get_type()) if buf_characteristic is None: continue if c.get_step_value() != buf_characteristic.get_step_value(): c.set_step_value(buf_characteristic.get_step_value()) changed_characteristics.append(c) if changed_characteristics: buf_gadget = AnyGadget(gadget.get_name(), "any", changed_characteristics) self._publish_gadget_update(buf_gadget)
def _encode_characteristic( gadget: Gadget, characteristic_type: CharacteristicIdentifier) -> dict: gadget_characteristic = gadget.get_characteristic(characteristic_type) value_range = gadget_characteristic.get_max( ) - gadget_characteristic.get_min() min_step = value_range // gadget_characteristic.get_steps() return { "minValue": gadget_characteristic.get_min(), "maxValue": gadget_characteristic.get_max(), "minStep": min_step }
def _remove_gadget_from_publishers(self, gadget: Gadget): """ Removes a gadget from all publishers :param gadget: The gadget that should be removed :return: None """ self._logger.info( f"Removing gadget '{gadget.get_name()}' from {len(self._gadget_publishers)} publishers" ) for publisher in self._gadget_publishers: publisher.remove_gadget(gadget.get_name())
def test_gadget_publisher_homebridge_network( publisher_network: GadgetPublisherHomeBridge, gadget: Gadget): with pytest.raises(GadgetDeletionError): publisher_network.remove_gadget(gadget.get_name()) publisher_network.create_gadget(gadget) with pytest.raises(GadgetCreationError): publisher_network.create_gadget(gadget) publisher_network.remove_gadget(gadget.get_name()) fan_speed = gadget.get_characteristic(CharacteristicIdentifier.fan_speed) fan_speed.set_step_value(2) publisher_network.receive_gadget(gadget) fan_speed.set_step_value(3) publisher_network.receive_gadget(gadget) publisher_network.remove_gadget(gadget.get_name())
def test_api_gadget_de_serialization(f_validator: Validator, encoder: ApiEncoder, decoder: ApiDecoder, f_any_gadget: Gadget, f_dummy_gadget: Gadget): serialized_data = encoder.encode_gadget(f_any_gadget) f_validator.validate(serialized_data, "api_gadget_data") with pytest.raises(NotImplementedError): deserialized_gadget = decoder.decode_gadget( serialized_data, f_any_gadget.get_host_client()) # assert deserialized_gadget.equals(f_any_gadget) with pytest.raises(GadgetEncodeError): encoder.encode_gadget(f_dummy_gadget)
def _get_base_info(gadget: Gadget) -> dict: return { "name": gadget.get_name(), "service_name": gadget.get_name(), }
def _publish_gadget(self, gadget: Gadget): with self.__publish_lock: self._last_published_gadget = gadget.get_name() super()._publish_gadget(gadget) self._last_published_gadget = None
def receive_gadget_update(self, gadget: Gadget): for identifier in [x.get_type() for x in gadget.get_characteristics()]: try: self._update_characteristic(gadget, identifier) except CharacteristicParsingError as err: self._logger.info(err.args[0])