From 2319c7eb7153cfb5652902e5e8bacef20bd6b5b7 Mon Sep 17 00:00:00 2001 From: djesic Date: Wed, 28 Jul 2021 16:40:02 +0200 Subject: [PATCH] Example clean up --- README.md | 14 ++-- examples/full_feature_set/main.py | 68 +++---------------- examples/simple/main.py | 35 ++-------- ...test_wolkabout_protocol_message_factory.py | 30 +++++++- wolk/interface/message_factory.py | 13 ++-- wolk/wolk_connect.py | 34 ++++++---- wolk/wolkabout_protocol_message_factory.py | 30 +++++--- 7 files changed, 97 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index fedd385..edfc92e 100644 --- a/README.md +++ b/README.md @@ -75,17 +75,13 @@ wolk_device = wolk.WolkConnect( wolk_device.connect() ``` -### Adding sensor readings +### Adding feed values ```python -wolk_device.add_sensor_reading("T", 26.93) +wolk_device.add_feed_value(("T", 26.93)) -# Multi-value sensor reading -wolk_device.add_sensor_reading("ACL", (4, 2, 0)) -``` -or multiple sensors at once with `add_sensor_readings`: -```python -wolk_device.add_sensor_readings({"T": 26.93, "ACL": (4, 2, 0)}) +# or multiple feed value readings +wolk_device.add_feed_value([("T": 27.11), ("H": 54.34), ("P", 1002.3)]) ``` Optionally pass a `timestamp` as `round(time.time()) * 1000`. @@ -95,7 +91,7 @@ If `timestamp` is not provided, the library will assign a timestamp before placi ### Data publish strategy -Stored sensor readings are pushed to WolkAbout IoT platform on demand by calling: +Stored feed values are pushed to WolkAbout IoT platform on demand by calling: ```python wolk_device.publish() ``` diff --git a/examples/full_feature_set/main.py b/examples/full_feature_set/main.py index eace9df..82fc0db 100644 --- a/examples/full_feature_set/main.py +++ b/examples/full_feature_set/main.py @@ -20,7 +20,6 @@ from traceback import print_exc from typing import Dict from typing import Optional -from typing import Tuple from typing import Union import requests @@ -112,15 +111,13 @@ def main() -> None: Pass all of these to a WolkConnect class and start a loop to send different types of random readings. """ + # TODO: Update this example + print("TODO: Update this example") + return + # Insert the device credentials received # from WolkAbout IoT Platform when creating the device - # List actuator references included on your device - actuator_references = ["SW", "SL"] - device = wolk.Device( - key="device_key", - password="some_password", - actuator_references=actuator_references, - ) + device = wolk.Device(key="device_key", password="some_password") try: global configurations with open(configuration_file) as file: @@ -128,43 +125,18 @@ def main() -> None: wolk.logging_config(configurations["LL"]) # Log level except Exception: print( - "Failed load configuraiton options " + "Failed load configuration options " f"from file '{configuration_file}'" ) print_exc() - sys.exit(1) + raise RuntimeError - class Actuator: + class InOutFeed: def __init__( self, inital_value: Optional[Union[bool, int, float, str]] ): self.value = inital_value - switch = Actuator(False) - slider = Actuator(0) - - # Provide a way to read actuator status if your device has actuators - def actuator_status_provider( - reference: str, - ) -> Tuple[wolk.State, Optional[Union[bool, int, float, str]]]: - if reference == actuator_references[0]: - return wolk.State.READY, switch.value - elif reference == actuator_references[1]: - return wolk.State.READY, slider.value - - return wolk.State.ERROR, None - - # Provide an actuation handler if your device has actuators - def actuation_handler( - reference: str, value: Union[bool, int, float, str] - ) -> None: - print(f"Setting actuator '{reference}' to value: {value}") - if reference == actuator_references[0]: - switch.value = value - - elif reference == actuator_references[1]: - slider.value = value - # The URL download implementation can be substituted # This is optional and passed as an argument when creating wolk_device def url_download(file_url: str, file_path: str) -> bool: @@ -210,14 +182,6 @@ def get_current_version(self) -> str: port=8883, ca_cert=".." + os.sep + ".." + os.sep + "wolk" + os.sep + "ca.crt", ) - .with_actuators( - actuation_handler=actuation_handler, - actuator_status_provider=actuator_status_provider, - ) - .with_configuration( - configuration_handler=configuration_handler, - configuration_provider=configuration_provider, - ) .with_file_management( preferred_package_size=1000 * 1000, max_file_size=100 * 1000 * 1000, @@ -235,13 +199,6 @@ def get_current_version(self) -> str: print("Connecting to WolkAbout IoT Platform") wolk_device.connect() - # Successfully connecting to the platform will publish device configuration - # all actuator statuses, files present on device, current firmware version - # and the result of a firmware update if it occurred - wolk_device.publish_configuration() - wolk_device.publish_actuator_status("SW") - wolk_device.publish_actuator_status("SL") - publish_period_seconds = configurations["HB"] # Heart beat while True: @@ -253,11 +210,6 @@ def get_current_version(self) -> str: temperature = random.uniform(15, 30) humidity = random.uniform(10, 55) pressure = random.uniform(975, 1030) - accelerometer = ( - random.uniform(0, 100), - random.uniform(0, 100), - random.uniform(0, 100), - ) # Enabled feeds if "T" in configurations["EF"]: @@ -271,10 +223,6 @@ def get_current_version(self) -> str: wolk_device.add_alarm("HH", False) if "P" in configurations["EF"]: wolk_device.add_sensor_reading("P", pressure, timestamp) - if "ACL" in configurations["EF"]: - wolk_device.add_sensor_reading( - "ACL", accelerometer, timestamp - ) else: wolk_device.connect() diff --git a/examples/simple/main.py b/examples/simple/main.py index 2c3e1c6..4c97331 100644 --- a/examples/simple/main.py +++ b/examples/simple/main.py @@ -23,49 +23,28 @@ # NOTE: Enable debug logging by uncommenting the following line # Optionally, as a second argument pass a file name -wolk.logging_config("debug") +# wolk.logging_config("debug") def main(): """Connect to WolkAbout IoT Platform and send a random sensor reading.""" # Insert the device credentials received # from WolkAbout IoT Platform when creating the device - device = wolk.Device( - key="danilo_pull_dev", - password="AIGAYDA51S", - data_delivery=wolk.DataDelivery.PULL, - ) + device = wolk.Device(key="some_key", password="some_password") - def incoming_feed_value_handler(feed_values): - for feed_value in feed_values: - for reference, value in feed_value.items(): - if reference == "timestamp": - continue - - if reference == "SL": - print("[dummy] setting SL to: " + value) - - wolk_device = wolk.WolkConnect( - device, - host="10.0.50.168", - port=1883, - ).with_incoming_feed_value_handler(incoming_feed_value_handler) + wolk_device = wolk.WolkConnect(device) # Establish a connection to the WolkAbout IoT Platform print("Connecting to WolkAbout IoT Platform") wolk_device.connect() - # NOTE: Connect calls these implicitly - # wolk_device.pull_parameters() - # wolk_device.pull_feed_values() - - publish_period_seconds = 45 + publish_period_seconds = 60 while True: try: - slider = random.randint(-20, 80) - wolk_device.add_feed_value("SL", slider) - print('Publishing "SL": ' + str(slider)) + temperature = random.randint(-20, 80) + wolk_device.add_feed_value(("T", temperature)) + print(f'Publishing "T": {temperature}') wolk_device.publish() time.sleep(publish_period_seconds) except KeyboardInterrupt: diff --git a/test/test_wolkabout_protocol_message_factory.py b/test/test_wolkabout_protocol_message_factory.py index 969c2f1..cf610fa 100644 --- a/test/test_wolkabout_protocol_message_factory.py +++ b/test/test_wolkabout_protocol_message_factory.py @@ -48,10 +48,30 @@ def test_init(self): """Test that object is created with correct device key.""" self.assertEqual(self.device_key, self.factory.device_key) + def test_feed_value(self): + """Test valid message for string feed.""" + reference = "SNL" + value = "string" + timestamp = round(time.time()) * 1000 + + expected_topic = self.factory.common_topic + WAPMF.FEED_VALUES + expected_payload = json.dumps( + [{reference: value, "timestamp": timestamp}] + ) + expected_message = Message(expected_topic, expected_payload) + + serialized_message = self.factory.make_from_feed_value( + (reference, value), timestamp + ) + + self.assertEqual(expected_message, serialized_message) + def test_feed_values(self): - """Test valid message for string with newline sensor reading.""" + """Test valid message for two string feeds.""" reference = "SNL" value = "string" + reference_2 = "SNL" + value_2 = "string" timestamp = round(time.time()) * 1000 expected_topic = self.factory.common_topic + WAPMF.FEED_VALUES @@ -61,11 +81,17 @@ def test_feed_values(self): expected_message = Message(expected_topic, expected_payload) serialized_message = self.factory.make_from_feed_value( - reference, value, timestamp + [(reference, value), (reference_2, value_2)], timestamp ) self.assertEqual(expected_message, serialized_message) + def test_feed_value_throws_on_invalid_data(self): + """Test valid message for two string feeds.""" + self.assertRaises( + ValueError, self.factory.make_from_feed_value, "foo", 1 + ) + def test_file_list_update(self): """Test message for file list update.""" file_list = ["file1.txt", "file2.bin", "file3 with spaces.jpg"] diff --git a/wolk/interface/message_factory.py b/wolk/interface/message_factory.py index f7dcba0..86a0997 100644 --- a/wolk/interface/message_factory.py +++ b/wolk/interface/message_factory.py @@ -17,6 +17,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import Tuple from typing import Union from wolk.model.data_type import DataType @@ -26,6 +27,9 @@ from wolk.model.message import Message from wolk.model.unit import Unit +OutgoingDataTypes = Union[bool, int, float, str] +Reading = Tuple[str, OutgoingDataTypes] + class MessageFactory(ABC): """Serialize messages to be sent to WolkAbout IoT Platform.""" @@ -33,17 +37,14 @@ class MessageFactory(ABC): @abstractmethod def make_from_feed_value( self, - reference: str, - value: Union[bool, int, float, str], + reading: Union[Reading, List[Reading]], timestamp: Optional[int], ) -> Message: """ Serialize feed value data. - :param reference: Feed identifier - :type reference: str - :param value: Value of the feed - :type value: Union[bool, int, float, str] + :param reading: Feed value data as (reference, value) or list of tuple + :type reading: Union[Reading, List[Reading]] :param timestamp: Unix timestamp in ms. Default to current time if None :returns: message :rtype: Message diff --git a/wolk/wolk_connect.py b/wolk/wolk_connect.py index d218306..f8311df 100644 --- a/wolk/wolk_connect.py +++ b/wolk/wolk_connect.py @@ -18,6 +18,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import Tuple from typing import Union from wolk import logger_factory @@ -52,6 +53,8 @@ ) IncomingData = List[Dict[str, Union[bool, int, float, str]]] +OutgoingDataTypes = Union[bool, int, float, str] +Reading = Tuple[str, OutgoingDataTypes] class WolkConnect: @@ -361,28 +364,33 @@ def disconnect(self) -> None: def add_feed_value( self, - reference: str, - value: Union[bool, int, float, str], + reading: Union[Reading, List[Reading]], timestamp: Optional[int] = None, ) -> None: """ - Place a feed value into storage. + Place a feed value reading into storage. - :param reference: Feed reference - :type reference: str - :param value: Value of the feed - :type value: Union[bool, int, float, str] - :param timestamp: Unix timestamp. If not provided, library will assign + A reading is identified by a unique feed reference string and + the current value of the feed. + + This reading can either be passed as a tuple of (reference, value) + for a single feed or as a list of previously mentioned tuples + to pass multiple feed readings at once. + + A Unix epoch timestamp in milliseconds as int can be provided to + denote when the reading occurred. By default, the current system + provided time will be assigned to a reading. + + :param reading: Feed value reading + :type reading: Union[Reading, List[Reading]] + :param timestamp: Unix timestamp. Defaults to system time. :type timestamp: Optional[int] """ self.logger.debug( - f"Adding feed value: reference = '{reference}', " - f"value = {value}, timestamp = {timestamp}" + f"Adding feed value: reading: {reading}, timestamp = {timestamp}" ) - message = self.message_factory.make_from_feed_value( - reference, value, timestamp - ) + message = self.message_factory.make_from_feed_value(reading, timestamp) # NOTE: if device is PUSH, do we try to publish instantly? self.message_queue.put(message) diff --git a/wolk/wolkabout_protocol_message_factory.py b/wolk/wolkabout_protocol_message_factory.py index 2bc487b..c0c0aa5 100644 --- a/wolk/wolkabout_protocol_message_factory.py +++ b/wolk/wolkabout_protocol_message_factory.py @@ -17,6 +17,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import Tuple from typing import Union from wolk import logger_factory @@ -30,6 +31,9 @@ from wolk.model.message import Message from wolk.model.unit import Unit +OutgoingDataTypes = Union[bool, int, float, str] +Reading = Tuple[str, OutgoingDataTypes] + class WolkAboutProtocolMessageFactory(MessageFactory): """Serialize messages to be sent to WolkAbout IoT Platform.""" @@ -77,29 +81,37 @@ def __init__(self, device_key: str) -> None: def make_from_feed_value( self, - reference: str, - value: Union[bool, int, float, str], + reading: Union[Reading, List[Reading]], timestamp: Optional[int], ) -> Message: """ Serialize feed value data. - :param reference: Feed identifier - :type reference: str - :param value: Value of the feed - :type value: Union[bool, int, float, str] + :param reading: Feed value data as (reference, value) or list of tuple + :type reading: Union[Reading, List[Reading]] :param timestamp: Unix timestamp in ms. Default to current time if None + :raises ValueError: Reading is invalid data type :returns: message :rtype: Message """ topic = self.common_topic + self.FEED_VALUES - payload = {reference: value} - payload["timestamp"] = ( + if isinstance(reading, tuple): + reference, value = reading + feed_value = {reference: value} + elif isinstance(reading, list): + feed_value = dict(reading) + else: + raise ValueError( + f"Expected reading as tuple or list, got {type(reading)}" + ) + + feed_value["timestamp"] = ( timestamp if timestamp is not None else round(time() * 1000) ) + payload = [feed_value] - message = Message(topic, json.dumps([payload])) + message = Message(topic, json.dumps(payload)) self.logger.debug(f"{message}") return message