async def run(self) -> None: """ The main task, that reads data from the sensors and pushes it onto the event_bus. """ # Generate the UUIDs of new sensors sensor_stream = stream.chain( stream.iterate( iterate_safely(f"{self.__topic}/get", f"{self.__topic}/status_update")), stream.iterate(event_bus.subscribe(f"{self.__topic}/add_host")), ) | pipe.flatmap( lambda item: stream.chain( (stream.call(event_bus.call, f"{self.__topic}/get_config", item ) | catch.pipe(TopicNotRegisteredError)), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{item}/update")), ) | pipe.until(lambda config: config is None) | pipe.map(lambda config: config if self._is_config_valid( self.__node_id, config) else None) | pipe.map(self._create_transport) | pipe.switchmap(lambda transport: stream.empty() if transport is None else stream.iterate(transport.stream_data())) | pipe.action(lambda data: event_bus.publish("wamp/publish", data) )) await sensor_stream
def _stream_config_updates( sensor: TinkerforgeSensor) -> AsyncGenerator[dict[str, Any], None]: """ Tries to fetch a config from the database. It also listens to 'nodes/tinkerforge/$UID/update' for new configs from the database. Parameters ---------- sensor: TinkerforgeSensor The brick or bricklet for which to fetch a config from the database Returns ------- AsyncGenerator of dict A dict containing the configuration of the device """ return stream.chain( stream.call( call_safely, "db_tinkerforge_sensors/get_config", "db_tinkerforge_sensors/status_update", sensor.device.uid, ) | pipe.takewhile(lambda config: config is not None), stream.iterate( event_bus.subscribe( f"nodes/tinkerforge/{sensor.device.uid}/update")), )
def _configure_and_stream( self, config: dict[str, Any] | None) -> AsyncGenerator[DataEvent, None]: if config is None: return stream.empty() try: # Run all config steps in order (concat) and one at a time (task_limit=1). Drop the output. There is # nothing to compare them to (filter => false), then read all sensors of the bricklet and process them in # parallel (flatten). config_stream = stream.chain( stream.iterate(config["on_connect"]) | pipe.starmap(lambda func, timeout: stream.just(func()) | pipe .timeout(timeout)) | pipe.concat(task_limit=1) | pipe.filter(lambda result: False), stream.iterate(config["config"].items()) | pipe.starmap(self._parse_callback_configuration) | pipe.starmap(self._set_callback_configuration) | pipe.flatten() | pipe.map(lambda args: self._read_sensor(config["uuid"], *args)) | pipe.flatten(), ) return config_stream except NotConnectedError: # Do not log it raise except Exception: self._logger.exception("This should not happen.") raise
def _stream_data(self, transport): config_stream = (stream.chain( stream.call( call_safely, f"{self.__database_topic}/get_config", f"{self.__database_topic}/status_update", transport.uuid, ), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{transport.uuid}/add")), ) | pipe.map(lambda config: self._create_device( transport, config)) | pipe.starmap(lambda config, device: stream.empty( ) if device is None else device.stream_data(config)) | pipe.switch() | context.pipe( transport, on_enter=lambda: logging.getLogger(__name__).info( "Connected to %s at %s (%s).", transport.name, transport.uri, transport.label), on_exit=lambda: logging.getLogger(__name__).info( "Disconnected from %s at %s (%s).", transport. name, transport.uri, transport.label), )) return config_stream
def stream_data(self) -> AsyncGenerator[DataEvent, None]: """ Generate the initial configuration of the sensor, configure it, and finally stream the data from the sensor. If there is a configuration update, reconfigure the sensor and start streaming again. Returns ------- AsyncGenerator of DataEvent The data from the device """ # Generates the first configuration # Query the database and if it does not have a config for the sensor, wait until there is one data_stream = ( stream.chain( stream.call( call_safely, "db_labnode_sensors/get_config", "db_labnode_sensors/status_update", self.__uuid ) | pipe.takewhile(lambda config: config is not None), stream.iterate(event_bus.subscribe(f"nodes/by_uuid/{self.__uuid}/update")), ) | pipe.action( lambda config: logging.getLogger(__name__).info( "Got new configuration for: %s", self._device, ) ) | pipe.map(self._create_config) | pipe.switchmap( lambda config: stream.empty() if config is None or not config["enabled"] else (self._configure_and_stream(config)) ) ) return data_stream
def stream_data(self, config: dict[str, Any]) -> AsyncGenerator[DataEvent, None]: """ Stream the data from the sensor. Parameters ---------- config: dict A dictionary containing the sensor configuration. Returns ------- AsyncGenerator The asynchronous stream. """ data_stream = (stream.chain( stream.just(config), stream.iterate( event_bus.subscribe(f"nodes/by_uuid/{self.__uuid}/update"))) | pipe.action(lambda _: logging.getLogger(__name__).info( "Got new configuration for: %s", self) if config is not None else logging.getLogger(__name__). info("Removed configuration for: %s", self)) | pipe.map(self._parse_config) | pipe.switchmap(lambda conf: stream.empty( ) if conf is None or not conf["enabled"] else (self._configure_and_stream(conf)))) return data_stream
def stream_data(self) -> AsyncGenerator[DataEvent, None]: """ Generate the initial configuration of the sensor, configure it, and finally stream the data from the sensor. If there is a configuration update, reconfigure the sensor and start streaming again. Returns ------- AsyncGenerator of DataEvent The data from the device """ # Generates the first configuration # Query the database and if it does not have a config for the sensor, wait until there is one data_stream = stream.chain( stream.just(self), stream.iterate( event_bus.subscribe( f"nodes/tinkerforge/{self.device.uid}/remove"))[:1] | pipe.map(lambda x: None), ) | pipe.switchmap( lambda sensor: stream.empty() if sensor is None else (self._stream_config_updates(sensor) | pipe.switchmap(lambda config: stream.chain( stream.just(config), stream.iterate( event_bus.subscribe( f"nodes/by_uuid/{config['uuid']}/remove"))[:1] | pipe.map(lambda x: None), )) | pipe.action(lambda config: logging.getLogger(__name__).info( "Got new configuration for: %s", sensor.device, )) | pipe.map(self._create_config) | pipe.switchmap(lambda config: stream.empty() if config is None or not config["enabled"] else (self._configure_and_stream(config))))) return data_stream
def _stream_transport(transport: TinkerforgeTransport): sensor_stream = stream.chain( stream.call(transport.enumerate) | pipe.filter(lambda x: False), stream.iterate(transport.read_enumeration()) | pipe.action(lambda enumeration: event_bus.publish(f"nodes/tinkerforge/{enumeration[1].uid}/remove", None)) | pipe.filter(lambda enumeration: enumeration[0] is not EnumerationType.DISCONNECTED) | pipe.starmap(lambda enumeration_type, sensor: TinkerforgeSensor(sensor)) | pipe.map(lambda sensor: sensor.stream_data()) | pipe.flatten(), ) | context.pipe( transport, on_enter=lambda: logging.getLogger(__name__).info( "Connected to Tinkerforge host at %s (%s).", transport.uri, transport.label ), on_exit=lambda: logging.getLogger(__name__).info( "Disconnected from Tinkerforge host at %s (%s).", transport.uri, transport.label ), ) return sensor_stream
def stream_data(self, config: dict[str, Any]) -> AsyncGenerator[DataEvent, None]: """ Enumerate the device, then read data from it. Parameters ---------- config: dict A dict containing the configuration for the device Yields ------- DataEvent The data from the device """ return stream.chain( stream.just(self) | pipe.action(async_(lambda sensor: sensor.enumerate())) | pipe.filter(lambda x: False), super().stream_data(config), )
def _configure_and_stream( self, config: dict[str, Any]) -> AsyncGenerator[DataEvent, None]: if config is None: return stream.empty() # Run all config steps in order (concat) and one at a time (task_limit=1). Drop the output. There is nothing to # compare them to (filter => false), then read the device. config_stream = stream.chain( stream.iterate(config["on_connect"]) | pipe.starmap(lambda func, timeout: stream.just(func()) | pipe. timeout(timeout)) | pipe.concat(task_limit=1) | pipe.filter(lambda result: False), self._read_device(config) | pipe.map(lambda item: DataEvent(sender=config["uuid"], topic=config["topic"], value=item, sid=0, unit=config["unit"])) | finally_action.pipe( stream.call(self._clean_up, config["on_disconnect"])), ) | catch.pipe(TypeError, on_exc=self.on_error) return config_stream