async def test_call(assert_run): def myfunc(a, b, c=0, d=4): return a, b, c, d xs = stream.call(myfunc, 1, 2, c=3) await assert_run(xs, [(1, 2, 3, 4)]) async def myasyncfunc(a, b, c=0, d=4): return a, b, c, d xs = stream.call(myasyncfunc, 1, 2, c=3) await assert_run(xs, [(1, 2, 3, 4)])
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 _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
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_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 _read_sensor( # pylint: disable=too-many-arguments self, sid: int, interval: float, unit: str, topic: str, timeout: float ) -> AsyncGenerator[DataEvent, None]: if self.__uuid is None: raise SensorNotReady("You must enumerate the sensor before reading.") return ( stream.repeat(stream.call(self._device.get_by_function_id, sid), interval=interval) | pipe.concat(task_limit=1) | pipe.timeout(timeout) | pipe.map(lambda value: DataEvent(sender=self.__uuid, topic=topic, value=value, sid=sid, unit=str(unit))) )
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 _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
def raw_stream(self): return (stream.call(self.sock.receive) | pipe.cycle())