From f5d1a7e79d69fd82a9459aac12758545a1a380ba Mon Sep 17 00:00:00 2001 From: charlie2clarke Date: Mon, 10 Oct 2022 15:17:37 +0100 Subject: [PATCH] feat: sending sensor readings to different azure devices --- config.json | 10 +- soil-moisture-sensor/app.py | 109 ------------------ soil_moisture_sensor/app.py | 61 ++++++++++ soil_moisture_sensor/iot/__init__.py | 0 soil_moisture_sensor/iot/azure_device.py | 82 +++++++++++++ soil_moisture_sensor/iot/azure_iot_hub.py | 44 +++++++ .../requirements.txt | 0 soil_moisture_sensor/test_app.py | 15 +++ 8 files changed, 210 insertions(+), 111 deletions(-) delete mode 100644 soil-moisture-sensor/app.py create mode 100644 soil_moisture_sensor/app.py create mode 100644 soil_moisture_sensor/iot/__init__.py create mode 100644 soil_moisture_sensor/iot/azure_device.py create mode 100644 soil_moisture_sensor/iot/azure_iot_hub.py rename {soil-moisture-sensor => soil_moisture_sensor}/requirements.txt (100%) create mode 100644 soil_moisture_sensor/test_app.py diff --git a/config.json b/config.json index bbd2acf..e313fc9 100644 --- a/config.json +++ b/config.json @@ -3,12 +3,18 @@ "type": "Soil Moisture", "units": "NoUnits", "min": 0, - "max": 1023 + "max": 1023, + "azure": { + "device_id": "soil_moisture_sensor" + } }, { "type": "Temperature", "units": "Celsius", "min": -20, - "max": 45 + "max": 45, + "azure": { + "device_id": "temperature_sensor" + } } ] \ No newline at end of file diff --git a/soil-moisture-sensor/app.py b/soil-moisture-sensor/app.py deleted file mode 100644 index ac91dec..0000000 --- a/soil-moisture-sensor/app.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -import os -from types import SimpleNamespace - -import requests -from counterfit_connection import CounterFitConnection - -CounterFitConnection.init('127.0.0.1', 5000) - -import json -import time - -from azure.iot.device import IoTHubDeviceClient, Message -from counterfit_connection import CounterFitConnection -from counterfit_shims_grove.adc import ADC - -ALLOWED_SENSORS = [ - "Soil Moisture", - "Temperature" -] - -connection_string = os.getenv("IOT_CONNECTION_STRING") -if connection_string is None: - raise Exception("IOT_CONNECTION_STRING is not set") - -adc = ADC() - -device_client = IoTHubDeviceClient.create_from_connection_string(connection_string) - -print('Connecting') -device_client.connect() -print('Connected') - -def _do_counterfit_req(endpoint, payload): - url = 'http://localhost:5000{}'.format(endpoint) - - payload = json.dumps(payload) - headers = { - 'Content-Type': 'application/json' - } - response = requests.request("POST", url, headers=headers, data=payload) - - return response.status_code - - -def create_sensor(i, sensor): - if sensor.type not in ALLOWED_SENSORS: - raise Exception("sensor is not valid") - - payload = { - "type": sensor.type, - "pin": i, - "i2c_pin": i, - "port": "/dev/ttyAMA0", - "name": "sensor_" + str(i), - "unit": sensor.units, - "i2c_unit": sensor.units, - } - - http_code = _do_counterfit_req("/create_sensor", payload) - if http_code != 200: - raise Exception("unsuccessful request to counterfit with payload: " + payload) - - -def configure_sensor(i, sensor): - if (sensor.min or sensor.max) is None: - raise Exception("no min or max value set for sensor") - - payload = { - "port": str(i), - "value": i, - "is_random": True, - "random_min": sensor.min, - "random_max": sensor.max - } - - http_code = _do_counterfit_req("/integer_sensor_settings", payload) - if http_code != 200: - raise Exception("unsuccessful request to counterfit with payload: " + payload) - - -def read_sensor_values(sensors): - sensor_dict = {} - - while True: - for i in range(len(sensors)): - sensor = sensors[i] - sensor_name = '{}_{}'.format(sensor.type, i) - sensor_dict[sensor_name] = adc.read(i) - print(sensor_name + " " + str(sensor_dict[sensor_name])) - - json_sensor_name = sensor.type.lower().replace(" ", "_") - - message = Message(json.dumps({ json_sensor_name: sensor_dict[sensor_name] })) - device_client.send_message(message) - - time.sleep(10) - - -if __name__ == "__main__": - conf_file = open("./config.json", "r") - conf = json.load(conf_file, object_hook=lambda d: SimpleNamespace(**d)) - - for i, sensor in enumerate(conf): - create_sensor(i, sensor) - configure_sensor(i, sensor) - - read_sensor_values(conf) - \ No newline at end of file diff --git a/soil_moisture_sensor/app.py b/soil_moisture_sensor/app.py new file mode 100644 index 0000000..d932fd7 --- /dev/null +++ b/soil_moisture_sensor/app.py @@ -0,0 +1,61 @@ +import json +import os +import time +from types import SimpleNamespace + +from counterfit_connection import CounterFitConnection +from iot.azure_device import IoTDevice +from iot.azure_iot_hub import IoTHubClient +from typing import Any + + +def initialise_device(sensor: Any, connection_str: str, i: int) -> IoTDevice: + iot_hub = IoTHubClient( + sensor.type, + connection_str, + ) + + iot_device = IoTDevice( + sensor.type, + sensor.units, + sensor.min, + sensor.max, + sensor.azure.device_id, + iot_hub, + ) + + iot_device.create_sensor(i) + + iot_device.configure_sensor(i) + + return iot_device + + +def run() -> None: + iot_devices = [] + + CounterFitConnection.init("127.0.0.1", 5000) + + conf_file = open("./config.json", "r") + conf = json.load(conf_file, object_hook=lambda d: SimpleNamespace(**d)) + + for i, sensor in enumerate(conf): + conn_str_env_var = "{}_connection_str".format(sensor.azure.device_id) + connection_string = os.getenv(conn_str_env_var) + + if connection_string == "": + raise Exception( + "no connection string found with the name: " + conn_str_env_var + ) + + iot_device = initialise_device(sensor, connection_string, i) + iot_devices.append(iot_device) + + while True: + for i, device in enumerate(iot_devices): + device.read_sensor_values(i) + time.sleep(10) + + +if __name__ == "__main__": + run() diff --git a/soil_moisture_sensor/iot/__init__.py b/soil_moisture_sensor/iot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/soil_moisture_sensor/iot/azure_device.py b/soil_moisture_sensor/iot/azure_device.py new file mode 100644 index 0000000..77dceb3 --- /dev/null +++ b/soil_moisture_sensor/iot/azure_device.py @@ -0,0 +1,82 @@ +import json +import time + +from azure.iot.device import Message + +from .azure_iot_hub import IoTHubClient + + +class IoTDevice: + ALLOWED_SENSORS = ["Soil Moisture", "Temperature"] + + def __init__( + self, + type: str, + units: str, + min: int, + max: int, + device_id: str, + client: IoTHubClient, + ) -> None: + self.type = type + self.units = units + self.min = min + self.max = max + self.device_id = device_id + self.client = client + + def create_sensor(self, i: int) -> None: + if self.type not in IoTDevice.ALLOWED_SENSORS: + raise Exception("sensor is not valid") + + payload = { + "type": self.type, + "pin": i, + "i2c_pin": i, + "port": "/dev/ttyAMA0", + "name": "sensor_" + str(i), + "unit": self.units, + "i2c_unit": self.units, + } + + http_code = self.client.post("/create_sensor", payload) + if http_code != 200: + raise Exception( + "unsuccessful request to counterfit with payload: " + payload + ) + + def configure_sensor(self, i: int) -> None: + if (self.min or self.max) is None: + raise Exception("no min or max value set for sensor") + + payload = { + "port": str(i), + "value": i, + "is_random": True, + "random_min": self.min, + "random_max": self.max, + } + + http_code = self.client.post("/integer_sensor_settings", payload) + if http_code != 200: + raise Exception( + "unsuccessful request to counterfit with payload: " + payload + ) + + def read_sensor_values(self, i): + sensor_dict = {} + + sensor_name = "{}_{}".format(self.type, i) + sensor_dict[sensor_name] = self.client.adc.read(i) + print(sensor_name + " " + str(sensor_dict[sensor_name])) + + json_sensor_name = self.type.lower().replace(" ", "_") + + message = Message( + json.dumps( + { + json_sensor_name: sensor_dict[sensor_name], + } + ) + ) + self.client.device_client.send_message(message) diff --git a/soil_moisture_sensor/iot/azure_iot_hub.py b/soil_moisture_sensor/iot/azure_iot_hub.py new file mode 100644 index 0000000..9483bed --- /dev/null +++ b/soil_moisture_sensor/iot/azure_iot_hub.py @@ -0,0 +1,44 @@ +import os +import json +import requests +from azure.iot.device import IoTHubDeviceClient, MethodResponse +from counterfit_shims_grove.adc import ADC + + +class IoTHubClient: + def __init__( + self, + device_name: str, + connection_str: str, + ) -> None: + self.device_name = device_name + self.adc = ADC() + self.connection_str = connection_str + try: + self.device_client = self.create_device_client() + except Exception as e: + print("couldn't create device_client") + print(e) + os._exit(502) + + self.device_client.connect() + self.device_client.on_method_request_received = self.set_request_handler + print("connected to " + device_name) + + def set_request_handler(self, request): + print("Direct method received - ", request.name) + + method_response = MethodResponse.create_from_method_request(request, 200) + self.device_client.send_method_response(method_response) + + def create_device_client(self) -> IoTHubDeviceClient: + return IoTHubDeviceClient.create_from_connection_string(self.connection_str) + + def post(self, endpoint, payload) -> int: + url = "http://localhost:5000{}".format(endpoint) + + payload = json.dumps(payload) + headers = {"Content-Type": "application/json"} + response = requests.request("POST", url, headers=headers, data=payload) + + return response.status_code diff --git a/soil-moisture-sensor/requirements.txt b/soil_moisture_sensor/requirements.txt similarity index 100% rename from soil-moisture-sensor/requirements.txt rename to soil_moisture_sensor/requirements.txt diff --git a/soil_moisture_sensor/test_app.py b/soil_moisture_sensor/test_app.py new file mode 100644 index 0000000..4268d09 --- /dev/null +++ b/soil_moisture_sensor/test_app.py @@ -0,0 +1,15 @@ +from app import create_sensor +import logging +import pytest + + +def test_create_sensor(mocker): + test_cases = [ + {"name": "valid sensor is created", "input": [1, "Soil Mositure"], "want": True} + ] + for test_case in test_cases: + logging.info(test_case["name"]) + something = mocker.patch("app._do_counterfit_req", return_value=200) + something.assert_once_called() + got = create_sensor(test_case["input"]) + assert got == test_case["want"]