From de601a55ade866363826b5c06325d8f25147685e Mon Sep 17 00:00:00 2001 From: Akash Sharma Date: Thu, 29 Dec 2022 13:19:42 -0600 Subject: [PATCH 1/5] ListDevicesRequest >> _prepare_params_for_list: (#16) modified to handle gatewayListOptions following NodeJS SDK --- clearblade/cloud/iot_v1/device_types.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/clearblade/cloud/iot_v1/device_types.py b/clearblade/cloud/iot_v1/device_types.py index 1bdfdcb2..528aa652 100644 --- a/clearblade/cloud/iot_v1/device_types.py +++ b/clearblade/cloud/iot_v1/device_types.py @@ -398,7 +398,12 @@ def _prepare_params_for_list(self): if self.field_mask: params['fieldMask'] = self.field_mask if self.gateway_list_options : - params['gatewayListOptions'] = self.gateway_list_options + if 'associationsDeviceId' in self.gateway_list_options: + params['gatewayListOptions.associationsDeviceId'] = self.gateway_list_options['associationsDeviceId'] + if 'associationsGatewayId' in self.gateway_list_options: + params['gatewayListOptions.associationsGatewayId'] = self.gateway_list_options['associationsGatewayId'] + if 'gatewayType' in self.gateway_list_options: + params['gatewayListOptions.gatewayType'] = self.gateway_list_options['gatewayType'] if self.page_token: params['pageToken'] = self.page_token From 8775b04c6457cdc41ab432a495e7c72988861075 Mon Sep 17 00:00:00 2001 From: rajasd27 Date: Mon, 9 Jan 2023 12:52:21 -0600 Subject: [PATCH 2/5] updated device class and enums (#19) --- clearblade/cloud/iot_v1/__init__.py | 10 +++- clearblade/cloud/iot_v1/device_types.py | 49 +++++++++++++------ clearblade/cloud/iot_v1/devices.py | 4 +- clearblade/cloud/iot_v1/registry.py | 2 +- clearblade/cloud/iot_v1/registry_types.py | 7 +-- clearblade/cloud/iot_v1/resources.py | 44 ++++++++--------- samples/clearblade/create_device_async.py | 2 +- .../create_device_registry_async.py | 8 +-- .../clearblade/create_device_registry_sync.py | 7 ++- samples/clearblade/create_device_sync.py | 6 ++- .../clearblade/create_device_sync_es256.py | 3 +- .../clearblade/create_device_sync_rs256.py | 8 +-- samples/clearblade/update_device_async.py | 2 +- samples/clearblade/update_device_sync.py | 4 +- 14 files changed, 93 insertions(+), 63 deletions(-) diff --git a/clearblade/cloud/iot_v1/__init__.py b/clearblade/cloud/iot_v1/__init__.py index 3b736104..86561049 100644 --- a/clearblade/cloud/iot_v1/__init__.py +++ b/clearblade/cloud/iot_v1/__init__.py @@ -34,4 +34,12 @@ "ListDeviceRegistryPager", "ListDeviceRegistriesAsyncPager", "ListDevicesPager", - "ListDevicesAsyncPager") + "ListDevicesAsyncPager", + "MqttState", + "HtttState", + "LogLevel", + "GatewayType", + "GatewayAuthMethod", + "PublicKeyCertificateFormat", + "PublicKeyFormat" + ) diff --git a/clearblade/cloud/iot_v1/device_types.py b/clearblade/cloud/iot_v1/device_types.py index 528aa652..94ac4ed4 100644 --- a/clearblade/cloud/iot_v1/device_types.py +++ b/clearblade/cloud/iot_v1/device_types.py @@ -1,5 +1,5 @@ from typing import List - +from .resources import GatewayType, LogLevel from .utils import get_value @@ -9,17 +9,16 @@ class Device(): """ # TODO: find a better way to construct the Device object. I dont like so much parameter in a constructor - def __init__(self, id: str = None, name: str = None, num_id: str = None, + def __init__(self, id: str, num_id: str = None, credentials: list = [], last_heartbeat_time: str = None, last_event_time: str = None, last_state_time: str = None, last_config_ack_time: str = None, last_config_send_time: str = None, blocked: bool = False, - last_error_time: str = None, last_error_status_code: dict = {"code":None, "message":""}, + last_error_time: str = None, last_error_status_code: dict = None, config: dict = {"cloudUpdateTime":None, "version":""} , state: dict = {"updateTime":None, "binaryData":None}, - log_level: str = "NONE", meta_data: dict = {}, gateway_config : dict = {}) -> None: + log_level: str = LogLevel.NONE, meta_data: dict = {}, gateway_config : dict = {"gatewayType": GatewayType.NON_GATEWAY}) -> None: self._id = id - self._name = name self._num_id = num_id self._credentials = credentials self._last_heartbeat_time = last_heartbeat_time @@ -38,7 +37,7 @@ def __init__(self, id: str = None, name: str = None, num_id: str = None, @staticmethod def from_json(json): - return Device(id=json['id'], name=json['name'], num_id=json['numId'], + return Device(id=json['id'], num_id=json['numId'], credentials=json['credentials'], last_heartbeat_time=json['lastHeartbeatTime'], last_event_time=json['lastEventTime'], last_state_time=json['lastStateTime'], last_config_ack_time=json['lastConfigAckTime'], last_config_send_time=json['lastConfigSendTime'], @@ -51,10 +50,6 @@ def from_json(json): def id(self): return self._id - @property - def name(self): - return self._name - @property def num_id(self): return self._num_id @@ -63,6 +58,10 @@ def num_id(self): def credentials(self): return self._credentials + @credentials.setter + def credentials(self, credentials): + self._credentials = credentials + @property def last_error_status(self): return self._last_error_status_code @@ -78,15 +77,27 @@ def state(self): @property def log_level(self): return self._log_level + + @log_level.setter + def log_level(self, log_level): + self._log_level = log_level @property def meta_data(self): return self._meta_data + @meta_data.setter + def meta_data(self, meta_data): + self._meta_data = meta_data + @property def gateway_config(self): return self._gateway_config + @gateway_config.setter + def gateway_config(self, gateway_config): + self._gateway_config = gateway_config + @property def log_level(self): return self._log_level @@ -94,18 +105,26 @@ def log_level(self): @property def last_heartbeat_time(self): return self._last_heartbeat_time + + @property + def blocked(self): + return self._blocked + + @blocked.setter + def blocked(self, blocked): + self._blocked = blocked # classes to mock googles request & response class DeviceState(): - def __init__(self, updated_time: str = None, binary_data:str = None) -> None: - self._updated_time = updated_time + def __init__(self, update_time: str = None, binary_data:str = None) -> None: + self._update_time = update_time self._binary_data = binary_data @property - def updated_time(self): - return self._updated_time + def update_time(self): + return self._update_time @property def binary_data(self): @@ -113,7 +132,7 @@ def binary_data(self): @staticmethod def from_json(response_json): - return DeviceState(updated_time=get_value(response_json, 'updateTime'), + return DeviceState(update_time=get_value(response_json, 'updateTime'), binary_data=get_value(response_json, 'binaryData')) diff --git a/clearblade/cloud/iot_v1/devices.py b/clearblade/cloud/iot_v1/devices.py index 20d62c1d..b79e8407 100644 --- a/clearblade/cloud/iot_v1/devices.py +++ b/clearblade/cloud/iot_v1/devices.py @@ -30,10 +30,10 @@ def _prepare_for_send_command(self, return params,body def _create_device_body(self, device: Device) : - return {'id':device.name, 'name':device.name, + return {'id':device.id, 'credentials':device.credentials, 'lastErrorStatus':device.last_error_status, 'config':device.config, 'state':device.state, - 'loglevel':device.log_level, 'metadata':device.meta_data, + 'logLevel':device.log_level, 'metadata':device.meta_data, 'gatewayConfig':device.gateway_config} def _create_device_from_response(self, json_response) -> Device : diff --git a/clearblade/cloud/iot_v1/registry.py b/clearblade/cloud/iot_v1/registry.py index b8a869fc..900defbb 100644 --- a/clearblade/cloud/iot_v1/registry.py +++ b/clearblade/cloud/iot_v1/registry.py @@ -21,7 +21,7 @@ def _create_registry_body(self, registry: DeviceRegistry) : if registry.event_notification_configs: registry_json['eventNotificationConfigs']=registry.event_notification_configs if registry.log_level: - registry_json['loglevel']=registry.log_level + registry_json['logLevel']=registry.log_level return registry_json def _prepare_params_for_registry_list(self, request:ListDeviceRegistriesRequest): diff --git a/clearblade/cloud/iot_v1/registry_types.py b/clearblade/cloud/iot_v1/registry_types.py index 979e11d4..1e65a278 100644 --- a/clearblade/cloud/iot_v1/registry_types.py +++ b/clearblade/cloud/iot_v1/registry_types.py @@ -1,4 +1,5 @@ from .utils import get_value +from .resources import HttpState, MqttState, LogLevel class EventNotificationConfig: def __init__(self, pub_sub_topic_name, subfolder_matches=None) -> None: @@ -17,9 +18,9 @@ class DeviceRegistry: def __init__(self, id:str = None, name:str = None, eventNotificationConfigs:list = [], stateNotificationConfig:dict = {'pubsubTopicName': ''}, - mqttConfig:dict = {'mqttEnabledState':'MQTT_ENABLED'}, - httpConfig:dict = {'httpEnabledState':'HTTP_ENABLED'}, - logLevel:str = None, credentials:list = []) -> None: + mqttConfig:dict = {'mqttEnabledState': MqttState.MQTT_ENABLED}, + httpConfig:dict = {'httpEnabledState': HttpState.HTTP_ENABLED}, + logLevel:str = LogLevel.NONE, credentials:list = []) -> None: self._id = id self._name = name self._event_notification_configs = eventNotificationConfigs diff --git a/clearblade/cloud/iot_v1/resources.py b/clearblade/cloud/iot_v1/resources.py index 56ee03e1..0989f754 100644 --- a/clearblade/cloud/iot_v1/resources.py +++ b/clearblade/cloud/iot_v1/resources.py @@ -3,21 +3,21 @@ class MqttState(): r"""Indicates whether an MQTT connection is enabled or disabled. See the field description for details. """ - MQTT_STATE_UNSPECIFIED = 0 - MQTT_ENABLED = 1 - MQTT_DISABLED = 2 + MQTT_STATE_UNSPECIFIED = "MQTT_STATE_UNSPECIFIED" + MQTT_ENABLED = "MQTT_ENABLED" + MQTT_DISABLED = "MQTT_DISABLED" class HttpState(): r"""Indicates whether DeviceService (HTTP) is enabled or disabled for the registry. See the field description for details. """ - HTTP_STATE_UNSPECIFIED = 0 - HTTP_ENABLED = 1 - HTTP_DISABLED = 2 + HTTP_STATE_UNSPECIFIED = "HTTP_STATE_UNSPECIFIED" + HTTP_ENABLED = "HTTP_ENABLED" + HTTP_DISABLED = "HTTP_DISABLED" -class LogLevel(): +class LogLevel: r"""**Beta Feature** The logging verbosity for device activity. Specifies which events @@ -25,18 +25,18 @@ class LogLevel(): only events that terminate in errors will be logged. LogLevel is inclusive; enabling INFO logging will also enable ERROR logging. """ - LOG_LEVEL_UNSPECIFIED = 0 - NONE = 10 - ERROR = 20 - INFO = 30 - DEBUG = 40 + LOG_LEVEL_UNSPECIFIED = "LOG_LEVEL_UNSPECIFIED" + NONE = "NONE" + ERROR = "ERROR" + INFO = "INFO" + DEBUG = "DEBUG" -class GatewayType(): +class GatewayType: r"""Gateway type.""" - GATEWAY_TYPE_UNSPECIFIED = 0 - GATEWAY = 1 - NON_GATEWAY = 2 + GATEWAY_TYPE_UNSPECIFIED = "GATEWAY_TYPE_UNSPECIFIED" + GATEWAY = "GATEWAY" + NON_GATEWAY = "NON_GATEWAY" class GatewayAuthMethod(): @@ -44,16 +44,16 @@ class GatewayAuthMethod(): determines how Cloud IoT Core authorizes/authenticate devices to access the gateway. """ - GATEWAY_AUTH_METHOD_UNSPECIFIED = 0 - ASSOCIATION_ONLY = 1 - DEVICE_AUTH_TOKEN_ONLY = 2 - ASSOCIATION_AND_DEVICE_AUTH_TOKEN = 3 + GATEWAY_AUTH_METHOD_UNSPECIFIED = "GATEWAY_AUTH_METHOD_UNSPECIFIED" + ASSOCIATION_ONLY = "ASSOCIATION_ONLY" + DEVICE_AUTH_TOKEN_ONLY = "DEVICE_AUTH_TOKEN_ONLY" + ASSOCIATION_AND_DEVICE_AUTH_TOKEN = "ASSOCIATION_AND_DEVICE_AUTH_TOKEN" class PublicKeyCertificateFormat(): r"""The supported formats for the public key.""" - UNSPECIFIED_PUBLIC_KEY_CERTIFICATE_FORMAT = 0 - X509_CERTIFICATE_PEM = 1 + UNSPECIFIED_PUBLIC_KEY_CERTIFICATE_FORMAT = "UNSPECIFIED_PUBLIC_KEY_CERTIFICATE_FORMAT" + X509_CERTIFICATE_PEM = "X509_CERTIFICATE_PEM" class PublicKeyFormat: diff --git a/samples/clearblade/create_device_async.py b/samples/clearblade/create_device_async.py index 730eac54..3ddc4a82 100644 --- a/samples/clearblade/create_device_async.py +++ b/samples/clearblade/create_device_async.py @@ -12,7 +12,7 @@ async def sample_create_device_async(): "asia-east1", "test-asia-east1") - device = iot_v1.Device(id="Python_SDK", name="Python_SDK") + device = iot_v1.Device(id="Python_SDK") request = iot_v1.CreateDeviceRequest(parent=parent, device=device) response = await async_client.create_device(request) diff --git a/samples/clearblade/create_device_registry_async.py b/samples/clearblade/create_device_registry_async.py index 7c353319..2b08b6f1 100644 --- a/samples/clearblade/create_device_registry_async.py +++ b/samples/clearblade/create_device_registry_async.py @@ -8,10 +8,10 @@ async def sample_create_device_registry(): # Create a client client = iot_v1.DeviceManagerAsyncClient() - registry = iot_v1.DeviceRegistry(id='test-registry', name='test-registry', - mqttConfig={'mqttEnabledState':'MQTT_ENABLED'}, - httpConfig={'httpEnabledState':'HTTP_ENABLED'}, - logLevel='ERROR', + registry = iot_v1.DeviceRegistry(id='rajas-dummy-registry', + mqttConfig={'mqttEnabledState':iot_v1.resources.MqttState.MQTT_ENABLED}, + httpConfig={'httpEnabledState':iot_v1.resources.HttpState.HTTP_ENABLED}, + logLevel=iot_v1.resources.LogLevel.ERROR, eventNotificationConfigs=[{'pubsubTopicName':'projects/ingressdevelopmentenv/topics/deleting'}] ) diff --git a/samples/clearblade/create_device_registry_sync.py b/samples/clearblade/create_device_registry_sync.py index 5f2981d1..d3b361f5 100644 --- a/samples/clearblade/create_device_registry_sync.py +++ b/samples/clearblade/create_device_registry_sync.py @@ -9,10 +9,9 @@ def sample_create_device_registry(): registry = iot_v1.DeviceRegistry( id='test-registry', - name='test-registry', - mqttConfig={'mqttEnabledState':'MQTT_ENABLED'}, - httpConfig={'httpEnabledState':'HTTP_ENABLED'}, - logLevel='ERROR', + mqttConfig={'mqttEnabledState':iot_v1.MqttState.MQTT_ENABLED}, + httpConfig={'httpEnabledState':iot_v1.HttpState.HTTP_ENABLED}, + logLevel=iot_v1.LogLevel.NONE, eventNotificationConfigs=[{'pubsubTopicName':'projects/api-project-320446546234/topics/deleting'}] ) diff --git a/samples/clearblade/create_device_sync.py b/samples/clearblade/create_device_sync.py index f5725e0a..e8fe6587 100644 --- a/samples/clearblade/create_device_sync.py +++ b/samples/clearblade/create_device_sync.py @@ -10,7 +10,11 @@ def sample_create_device(): "us-central1", "test-registry") - device = iot_v1.Device(id="Python_11", name="Python_11") + device = iot_v1.Device( + id="Python_12", + gateway_config={"gatewayType": iot_v1.GatewayType.NON_GATEWAY}, + log_level=iot_v1.LogLevel.ERROR) + request = iot_v1.CreateDeviceRequest(parent=parent, device=device) response = client.create_device(request) diff --git a/samples/clearblade/create_device_sync_es256.py b/samples/clearblade/create_device_sync_es256.py index 9efd4975..ed5ff357 100644 --- a/samples/clearblade/create_device_sync_es256.py +++ b/samples/clearblade/create_device_sync_es256.py @@ -18,11 +18,10 @@ def create_device_in_dev_iot(name, keyFile): device = iot_v1.Device( id="my_test_device", - name=name, credentials=[ { "publicKey": { - "format": "ES256_PEM", + "format": iot_v1.PublicKeyFormat.ES256_PEM, "key": public_key, } }]) diff --git a/samples/clearblade/create_device_sync_rs256.py b/samples/clearblade/create_device_sync_rs256.py index f74fc194..306a29e0 100644 --- a/samples/clearblade/create_device_sync_rs256.py +++ b/samples/clearblade/create_device_sync_rs256.py @@ -10,15 +10,14 @@ def create_device_in_dev_iot(name, keyFile): parent = client.registry_path( "api-project-320446546234", - "asia-east1", - "test-asia-east1") + "us-central1", + "test-registry") with io.open(keyFile) as f: public_key = f.read() device = iot_v1.Device( - id="python_sdk_device_dummy", - name=name, + id=name, credentials=[ { "publicKey": { @@ -32,4 +31,5 @@ def create_device_in_dev_iot(name, keyFile): device_name = "python_sdk_device_dummy" key_path = "../api-client/manager/resources/ec_public.pem" +os.environ["CLEARBLADE_CONFIGURATION"] = "/Users/rajas/Downloads/test-credentials.json" create_device_in_dev_iot(device_name, key_path) diff --git a/samples/clearblade/update_device_async.py b/samples/clearblade/update_device_async.py index f19c1dc1..ba340abc 100644 --- a/samples/clearblade/update_device_async.py +++ b/samples/clearblade/update_device_async.py @@ -12,7 +12,7 @@ async def sample_update_device_async(): "us-central1", "test-registry") - device = iot_v1.Device(id="test-dev-1", blocked=True, log_level='NONE') + device = iot_v1.Device(id="test-dev-1", blocked=True, log_level=iot_v1.LogLevel.ERROR) request = iot_v1.UpdateDeviceRequest( parent=registry_path, diff --git a/samples/clearblade/update_device_sync.py b/samples/clearblade/update_device_sync.py index 2018d3c8..2816a536 100644 --- a/samples/clearblade/update_device_sync.py +++ b/samples/clearblade/update_device_sync.py @@ -11,7 +11,7 @@ def sample_update_device(): "us-central1", "test-registry") - device = iot_v1.Device(id="test-dev-1", blocked=True, log_level='NONE') + device = iot_v1.Device(id="python_11", blocked=True, log_level=iot_v1.LogLevel.ERROR) request = iot_v1.UpdateDeviceRequest( parent=registry_path, @@ -24,5 +24,5 @@ def sample_update_device(): print(response) -os.environ["CLEARBLADE_CONFIGURATION"] = "/Users/DummyUser/Downloads/test-credentials.json" +os.environ["CLEARBLADE_CONFIGURATION"] = "/Users/rajas/Downloads/test-credentials.json" sample_update_device() From 4d25b0f28e538e322e241a4f2c9994b5aeb408f7 Mon Sep 17 00:00:00 2001 From: rajasd27 Date: Mon, 9 Jan 2023 12:52:36 -0600 Subject: [PATCH 3/5] updated send_command function to base64 encode payload (#18) --- clearblade/cloud/iot_v1/devices.py | 3 ++- samples/clearblade/send_command_to_device.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/clearblade/cloud/iot_v1/devices.py b/clearblade/cloud/iot_v1/devices.py index b79e8407..75a4242b 100644 --- a/clearblade/cloud/iot_v1/devices.py +++ b/clearblade/cloud/iot_v1/devices.py @@ -2,6 +2,7 @@ from .device_types import * from .http_client import AsyncClient, SyncClient from .pagers import ListDevicesAsyncPager, ListDevicesPager +import base64 class ClearBladeDeviceManager(): @@ -25,7 +26,7 @@ def _prepare_for_send_command(self, if request is None: request = SendCommandToDeviceRequest(name, binary_data, subfolder) params = {'name':request.parent,'method':'sendCommandToDevice'} - body = {'binaryData':request.binary_data.decode("utf-8")} + body = {'binaryData':base64.b64encode(request.binary_data).decode("utf-8")} return params,body diff --git a/samples/clearblade/send_command_to_device.py b/samples/clearblade/send_command_to_device.py index 80186840..5f08bc97 100644 --- a/samples/clearblade/send_command_to_device.py +++ b/samples/clearblade/send_command_to_device.py @@ -1,3 +1,4 @@ +import json import os from clearblade.cloud import iot_v1 @@ -6,13 +7,20 @@ def sample_send_command_to_device(): client = iot_v1.DeviceManagerClient() + data = json.dumps({ + "device_code": "1", + "command": "test", + "command_option": "0", + "seq_no": 10 + }).encode("utf-8") + device_path = client.device_path( "api-project-320446546234", "us-central1", "test-registry", "test-dev-1") - request = iot_v1.SendCommandToDeviceRequest(name=device_path, binary_data=b"QUJD") + request = iot_v1.SendCommandToDeviceRequest(name=device_path, binary_data=data) response = client.send_command_to_device(request) print(response) From bb2915dbb3b005d8ed758cccf473e5f3fc97499d Mon Sep 17 00:00:00 2001 From: Akash Sharma Date: Tue, 10 Jan 2023 04:38:14 -0800 Subject: [PATCH 4/5] Changed 'cloud_ack_time' to 'cloud_update_time' (#20) --- clearblade/cloud/iot_v1/developer_tests.py | 2 +- clearblade/cloud/iot_v1/device_types.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/clearblade/cloud/iot_v1/developer_tests.py b/clearblade/cloud/iot_v1/developer_tests.py index 872a83ff..cb5daa34 100644 --- a/clearblade/cloud/iot_v1/developer_tests.py +++ b/clearblade/cloud/iot_v1/developer_tests.py @@ -109,7 +109,7 @@ def test_get_device_configVersions(): device_configs = response.device_configs for device_config in device_configs: print("Device version = {} Device Ack Time {} \n".format(device_config.version, - device_config.cloud_ack_time)) + device_config.cloud_update_time)) async def test_get_device_configVersions_async(): async_client = DeviceManagerAsyncClient() diff --git a/clearblade/cloud/iot_v1/device_types.py b/clearblade/cloud/iot_v1/device_types.py index 94ac4ed4..e0667ed0 100644 --- a/clearblade/cloud/iot_v1/device_types.py +++ b/clearblade/cloud/iot_v1/device_types.py @@ -193,12 +193,12 @@ def binary_data(self): class DeviceConfig(Request): def __init__(self, name, version, - cloud_ack_time, + cloud_update_time, device_ack_time, binary_data) -> None: super().__init__(name) self._version = version - self._cloud_ack_time = cloud_ack_time + self._cloud_update_time = cloud_update_time self._device_ack_time = device_ack_time self._binary_data = binary_data @@ -207,8 +207,8 @@ def version(self): return self._version @property - def cloud_ack_time(self): - return self._cloud_ack_time + def cloud_update_time(self): + return self._cloud_update_time @property def device_ack_time(self): @@ -222,7 +222,7 @@ def binary_data(self): def from_json(json): return DeviceConfig(name='', version=get_value(json, 'version'), - cloud_ack_time=get_value(json,'cloudUpdateTime'), + cloud_update_time=get_value(json,'cloudUpdateTime'), device_ack_time=get_value(json, 'deviceAckTime'), binary_data=get_value(json,'binaryData')) From 76c48bcf6d720355a0e5c40d4edc68512d5b2d20 Mon Sep 17 00:00:00 2001 From: akash-sharma-ext-clearblade Date: Tue, 28 Feb 2023 08:27:19 -0800 Subject: [PATCH 5/5] Iot 918 conform to google format times binary data (#24) * 1. imports incl. google.api_core.datetime_helpers 2. Added logic to def from_json in class DeviceState and DeviceConfig to convert times to DateTimeWithNanoseconds and binaryData to bytes if env. vars set * 1. Changed Device.from_json to support GCP types for time, binaryData. 2. Changed DeviceState.from_json to handle blank binaryData 3. Changed DeviceConfig.from_json to handle blank binaryData * Removed extra 'import base64' * Added note about types of times, binaryData * Cleaned up formatting * Further cleanup * Added copyright info. at top of files * Not to 'pip install google-api-core' * Removed instructions for installing datetime_helpers module. Also now changed 'google.api_core' to 'proto' * Added "proto.datetime_helpers" to dependencies * Changed 'google.api_core' to 'proto.' * Removed call to set 'convert_binarydata_to_bytes' in Devices.from_json * Changed README to reflect decision on ONE env. var * Changed module to import to proto-plus * Two env. vars -> BINARYDATA_AND_TIME_GOOGLE_FORMAT * Removed a '.' * Format improvements for README * Added note about new env. var in 'Quick Start' sec --- README.rst | 41 ++++++- clearblade/cloud/iot_v1/device_types.py | 141 +++++++++++++++++++++--- clearblade/cloud/iot_v1/devices.py | 31 +++++- setup.py | 2 +- 4 files changed, 197 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index 0480a6f8..9e7a1b2a 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,24 @@ +.. Copyright 2023 ClearBlade Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + Copyright 2022 Google LLC + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + Python Client for ClearBlade Internet of Things (IoT) Core API ================================================================ @@ -9,8 +30,9 @@ In order to use this library, you first need to go through the following steps: 1. Install pip package - ```pip install clearblade-cloud-iot``` -2. Set an environment variable CLEARBLADE_CONFIGURATION which should point to your clearblade service account json file. +2. Set an environment variable **CLEARBLADE_CONFIGURATION** which should point to your clearblade service account json file. +3. Optionally set an environment variable **BINARYDATA_AND_TIME_GOOGLE_FORMAT** to True. Look at **Note about types of times and binaryData** below for details. Installation ~~~~~~~~~~~~ @@ -78,3 +100,20 @@ Next Steps - and execute the setup.py file like , python setup.py install. - mostly if you change you imports from from google.cloud to clearblade.cloud everything else should work. + +Note about types of times and binaryData +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- By default the following parameters are returned as the shown types: + +1. All time parameters (e.g. **cloudUpdateTime**, **deviceAckTime**, **updateTime**): **RFC3339** strings (e.g. "2023-01-12T23:38:07.732Z") +2. **CONFIG binaryData**: **base64-encoded string** +3. **STATE binaryData**: **NON-base64-encoded string** + + +- To return these parameters using the same types used by the **Google IoTCore Python SDK**, set environment variable **BINARYDATA_AND_TIME_GOOGLE_FORMAT** to **True** (case-insensitive string). This will ensure the following parameters are returned as the shown types: + +1. All times: **DatetimeWithNanoseconds** (defined in the **proto.datetime_helpers** module) +2. All **binaryData** (CONFIG, STATE etc.): **BYTE ARRAYS** + +- If this environment variable is not set, or is set to any unexpeced values, then the default types listed previously are used. \ No newline at end of file diff --git a/clearblade/cloud/iot_v1/device_types.py b/clearblade/cloud/iot_v1/device_types.py index e0667ed0..71832484 100644 --- a/clearblade/cloud/iot_v1/device_types.py +++ b/clearblade/cloud/iot_v1/device_types.py @@ -1,7 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 ClearBlade Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# from typing import List from .resources import GatewayType, LogLevel from .utils import get_value - +import os +from proto.datetime_helpers import DatetimeWithNanoseconds +import base64 class Device(): """ @@ -37,14 +68,57 @@ def __init__(self, id: str, num_id: str = None, @staticmethod def from_json(json): - return Device(id=json['id'], num_id=json['numId'], - credentials=json['credentials'], last_heartbeat_time=json['lastHeartbeatTime'], - last_event_time=json['lastEventTime'], last_state_time=json['lastStateTime'], - last_config_ack_time=json['lastConfigAckTime'], last_config_send_time=json['lastConfigSendTime'], - blocked=json['blocked'], last_error_time=json['lastErrorTime'], - last_error_status_code=json['lastErrorStatus'], config=json['config'], - state=json['state'], log_level=json['logLevel'], meta_data=json['metadata'], - gateway_config=json['gatewayConfig']) + lastHeartbeatTimeFromJson = get_value(json,'lastHeartbeatTime') + lastEventTimeFromJson = get_value(json, 'lastEventTime') + lastStateTimeFromJson = get_value(json, 'lastStateTime') + lastConfigAckTimeFromJson = get_value(json, 'lastConfigAckTime') + lastConfigSendTimeFromJson = get_value(json, 'lastConfigSendTime') + lastErrorTimeFromJson = get_value(json, 'lastErrorTime') + + convert_times_to_datetime_with_nanoseconds = (False if os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT") == None else os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT").lower() == "true") + if convert_times_to_datetime_with_nanoseconds: + last_heartbeat_time = None if lastHeartbeatTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastHeartbeatTimeFromJson) + last_event_time = None if lastEventTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastEventTimeFromJson) + last_state_time = None if lastStateTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastStateTimeFromJson) + last_config_ack_time = None if lastConfigAckTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastConfigAckTimeFromJson) + last_config_send_time = None if lastConfigSendTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastConfigSendTimeFromJson) + last_error_time = None if lastErrorTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(lastErrorTimeFromJson) + else: + last_heartbeat_time = lastHeartbeatTimeFromJson + last_event_time = lastEventTimeFromJson + last_state_time = lastStateTimeFromJson + last_config_ack_time = lastConfigAckTimeFromJson + last_config_send_time = lastConfigSendTimeFromJson + last_error_time = lastErrorTimeFromJson + + deviceConfig = DeviceConfig.from_json(get_value(json, 'config')) + config = { "version": deviceConfig.version, "cloudUpdateTime": deviceConfig.cloud_update_time } + if (deviceConfig.binary_data not in [None, ""]): + config["binaryData"] = deviceConfig.binary_data + if (deviceConfig.device_ack_time not in [None, ""]): + config["deviceAckTime"] = deviceConfig.device_ack_time + + deviceState = DeviceState.from_json(get_value(json, 'state')) + state = { "updateTime": deviceState.update_time, "binaryData": deviceState.binary_data } + + return Device( + id=get_value(json, 'id'), + num_id=get_value(json, 'numId'), + credentials=get_value(json, 'credentials'), + last_heartbeat_time=last_heartbeat_time, + last_event_time=last_event_time, + last_state_time=last_state_time, + last_config_ack_time=last_config_ack_time, + last_config_send_time=last_config_send_time, + last_error_time=last_error_time, + blocked=get_value(json, 'blocked'), + last_error_status_code=get_value(json, 'lastErrorStatus'), + config=config, + state=state, + log_level=get_value(json, 'logLevel'), + meta_data=get_value(json, 'metadata'), + gateway_config=get_value(json, 'gatewayConfig') + ) @property def id(self): @@ -132,8 +206,26 @@ def binary_data(self): @staticmethod def from_json(response_json): - return DeviceState(update_time=get_value(response_json, 'updateTime'), - binary_data=get_value(response_json, 'binaryData')) + updateTimeFromJson = get_value(response_json, 'updateTime') + binaryDataFromJson = get_value(response_json, 'binaryData') + + convert_times_to_datetime_with_nanoseconds = (False if os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT") == None else os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT").lower() == "true") + if convert_times_to_datetime_with_nanoseconds: + update_time = None if updateTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(updateTimeFromJson) + else: + update_time = updateTimeFromJson + + if (binaryDataFromJson not in [None, ""]): + convert_binarydata_to_bytes = (False if os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT") == None else os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT").lower() == "true") + if convert_binarydata_to_bytes: + binary_data = binaryDataFromJson.encode('utf-8') + else: + binary_data = binaryDataFromJson + else: + binary_data = binaryDataFromJson + + return DeviceState(update_time=update_time, + binary_data=binary_data) class Request(): @@ -220,11 +312,32 @@ def binary_data(self): @staticmethod def from_json(json): + cloudUpdateTimeFromJson = get_value(json,'cloudUpdateTime') + deviceAckTimeFromJson = get_value(json, 'deviceAckTime') + binaryDataFromJson = get_value(json,'binaryData') + + convert_times_to_datetime_with_nanoseconds = (False if os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT") == None else os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT").lower() == "true") + if convert_times_to_datetime_with_nanoseconds: + cloud_update_time = None if cloudUpdateTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(cloudUpdateTimeFromJson) + device_ack_time = None if deviceAckTimeFromJson in [None, ""] else DatetimeWithNanoseconds.from_rfc3339(deviceAckTimeFromJson) + else: + cloud_update_time = cloudUpdateTimeFromJson + device_ack_time = deviceAckTimeFromJson + + if binaryDataFromJson not in [None, ""]: + convert_binarydata_to_bytes = (False if os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT") == None else os.environ.get("BINARYDATA_AND_TIME_GOOGLE_FORMAT").lower() == "true") + if convert_binarydata_to_bytes: + binary_data = base64.b64decode(binaryDataFromJson.encode('utf-8')) + else: + binary_data = binaryDataFromJson + else: + binary_data = binaryDataFromJson + return DeviceConfig(name='', version=get_value(json, 'version'), - cloud_update_time=get_value(json,'cloudUpdateTime'), - device_ack_time=get_value(json, 'deviceAckTime'), - binary_data=get_value(json,'binaryData')) + cloud_update_time=cloud_update_time, + device_ack_time=device_ack_time, + binary_data=binary_data) class DeleteDeviceRequest(Request): diff --git a/clearblade/cloud/iot_v1/devices.py b/clearblade/cloud/iot_v1/devices.py index 44fc9a9c..42ebbe4a 100644 --- a/clearblade/cloud/iot_v1/devices.py +++ b/clearblade/cloud/iot_v1/devices.py @@ -1,5 +1,32 @@ -import base64 - +# -*- coding: utf-8 -*- +# Copyright 2023 ClearBlade Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# from .config_manager import ClearBladeConfigManager from .device_types import * from .http_client import AsyncClient, SyncClient diff --git a/setup.py b/setup.py index 0fac11d2..4bf8c8da 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ description = "Cloud IoT API client library" version = "1.0.5" release_status = "Development Status :: 5 - Production/Stable" -dependencies = ["httpx"] +dependencies = ["httpx", "proto-plus"] package_root = os.path.abspath(os.path.dirname(__file__))