├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── index.md ├── senml_base.md ├── senml_pack.md ├── senml_record.md └── senml_unit.md ├── examples ├── actuator.py ├── base.py ├── basic.py ├── basic2.py ├── cbor_basic.py ├── custom_record.py ├── gateway.py ├── gateway_actuators.py └── supported_data_types.py ├── kpn_senml ├── __init__.py ├── senml_base.py ├── senml_kpn_names.py ├── senml_pack.py ├── senml_record.py └── senml_unit.py ├── requirements.txt ├── requirements_test.txt ├── setup.py └── tests ├── __init__.py ├── test_names.py ├── test_records.py └── test_units.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/pip-selfcheck.json 3 | venv/ 4 | *.pyc 5 | dist/ 6 | kpn_senml.egg-info/ 7 | .cache/ 8 | build/ 9 | tests/htmlcov/ 10 | tests/.coverage 11 | env 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | fast_finish: true 3 | include: 4 | - os: linux 5 | sudo: false 6 | language: python 7 | python: '2.7' 8 | - os: linux 9 | sudo: false 10 | language: python 11 | python: '3.6' 12 | 13 | install: 14 | - pip install -e .[test] 15 | 16 | script: 17 | - python setup.py install 18 | - cd tests 19 | - pytest . -vvvv --cov=kpn_senml 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 KPN IoT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KPN SenML 2 | 3 | #### Introduction 4 | 5 | The KPN SenML library helps you create and parse [senml documents](https://tools.ietf.org/html/draft-ietf-core-senml-13) 6 | in both json and cbor format. 7 | 8 | #### Key Features 9 | 10 | - Object oriented design. 11 | - built in support for [senml's unit registry](https://tools.ietf.org/html/draft-ietf-core-senml-12#section-12.1) 12 | - extensible for new data types 13 | - makes use of (but doesn't restrict to) KPN's predefined list of record names. 14 | - direct support to read/write in json and cbor format. 15 | - automatically adjusts record data with respect to base time, base value & base sum. 16 | - for python 2.7 and 3.5 (geared towards embedded systems) 17 | 18 | #### Testing 19 | 20 | To execute all unit tests: 21 | 22 | $ pip install -r requirements_test.txt 23 | $ cd tests 24 | $ pytest . -v --cov=kpn_senml --cov-report=html 25 | 26 | Please visit our [docs site](https://kpn-iot.github.io/senml-python-library) for more info. 27 | 28 | #### LICENSE 29 | [MIT](LICENSE) 30 | 31 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | Welcome to the API documet site for the python version of KPN's SenML library. 2 | 3 | Check out the [detailed docs](https://kpn-iot.github.io/senml-library/) for an in-depth explanation of all the features in the library. 4 | 5 | The following api sections are available: 6 | 7 | - [senml-base](./senml_base): the base class for all senml objects. 8 | - [senml-pack](./senml_pack): the class that represents root documents. 9 | - [senml-record](./senml_record): the class that stores sensor measurements 10 | - [senml-unit](./senml_unit): the list of all unit names that can be used. 11 | 12 | 13 | 14 | copyright © 2018 KPN -------------------------------------------------------------------------------- /docs/senml_base.md: -------------------------------------------------------------------------------- 1 | 2 | # senml_base Module 3 | 4 | 5 | ## senml_base.SenmlBase Objects 6 | 7 | 8 | the base class for all senml objects. 9 | -------------------------------------------------------------------------------- /docs/senml_pack.md: -------------------------------------------------------------------------------- 1 | 2 | # senml_pack Module 3 | 4 | 5 | ## senml_pack.SenmlPack Objects 6 | 7 | 8 | represents a senml pack object. This can contain multiple records but also other (child) pack objects. 9 | When the pack object only contains records, it represents the data of a device. 10 | If the pack object has child pack objects, then it represents a gateway 11 | 12 | ### __enter__ 13 | 14 | ```Python 15 | __enter__(self) 16 | ``` 17 | 18 | for supporting the 'with' statement 19 | 20 | 21 | _returns_: self 22 | 23 | ### __exit__ 24 | 25 | ```Python 26 | __exit__(self, exc_type, exc_val, exc_tb) 27 | ``` 28 | 29 | when destroyed in a 'with' statement, make certain that the item is removed from the parent list. 30 | 31 | 32 | _returns_: None 33 | 34 | ### __init__ 35 | 36 | ```Python 37 | __init__(self, name, callback=None) 38 | ``` 39 | 40 | initialize the object 41 | 42 | _parameters:_ 43 | 44 | - `name:` {string} the name of the pack 45 | 46 | ### __iter__ 47 | 48 | ```Python 49 | __iter__(self) 50 | ``` 51 | 52 | 53 | 54 | ### add 55 | 56 | ```Python 57 | adds the item to the list of records 58 | ``` 59 | 60 | 61 | _parameters:_ 62 | 63 | - `item:` {SenmlRecord} the item that needs to be added to the pack 64 | 65 | 66 | _returns_: None 67 | 68 | ### base_sum 69 | 70 | the base sum of the pack. 71 | 72 | 73 | _returns_: a number 74 | 75 | ### base_time 76 | 77 | Get the base time assigned to this pack object. 78 | While rendering, this value will be subtracted from the value of the records. 79 | 80 | 81 | _returns_: unix time stamp representing the base time 82 | 83 | ### base_value 84 | 85 | the base value of the pack. The value of the records will be subtracted by this value during rendering. 86 | While parsing, this value is added to the value of the records. 87 | 88 | 89 | _returns_: a number 90 | 91 | ### clear 92 | 93 | ```Python 94 | clear(self) 95 | ``` 96 | clear the list of the pack 97 | 98 | 99 | 100 | _returns_: None 101 | 102 | ### do_actuate 103 | 104 | ```Python 105 | do_actuate(self, raw, naming_map, device=None) 106 | ``` 107 | 108 | called while parsing incoming data for a record that is not yet part of this pack object. 109 | adds a new record and raises the actuate callback of the pack with the newly created record as argument 110 | 111 | _parameters:_ 112 | 113 | - naming_map: 114 | - `device:` optional: if the device was not found 115 | - `raw:` the raw record definition, as found in the json structure. this still has invalid labels. 116 | 117 | 118 | _returns_: None 119 | 120 | ### from_cbor 121 | 122 | ```Python 123 | from_cbor(self, data) 124 | ``` 125 | 126 | parse a cbor data byte array to a senml pack structure. 127 | 128 | _parameters:_ 129 | 130 | - `data:` a byte array. 131 | 132 | 133 | _returns_: None 134 | 135 | ### from_json 136 | 137 | ```Python 138 | from_json(self, data) 139 | ``` 140 | 141 | parse a json string and convert it to a senml pack structure 142 | 143 | _parameters:_ 144 | 145 | - `data:` a string containing json data. 146 | 147 | 148 | _returns_: None, will call the appropriate callback functions. 149 | 150 | 151 | 152 | ### remove 153 | 154 | ```Python 155 | remove(self, item) 156 | ``` 157 | removes the item from the pack 158 | 159 | 160 | _parameters:_ 161 | 162 | - `item:` {SenmlRecord} the item that needs to be removed 163 | 164 | 165 | _returns_: None 166 | 167 | ### to_cbor 168 | 169 | ```Python 170 | to_cbor(self) 171 | ``` 172 | 173 | render the content of this object to a cbor byte array 174 | 175 | 176 | _returns_: a byte array 177 | 178 | ### to_json 179 | 180 | ```Python 181 | to_json(self) 182 | ``` 183 | 184 | render the content of this object to a string. 185 | 186 | 187 | _returns_: a string representing the senml pack object 188 | 189 | ## senml_pack.SenmlPackIterator Objects 190 | 191 | 192 | an iterator to walk over all records in a pack 193 | 194 | ### __init__ 195 | 196 | ```Python 197 | __init__(self, list) 198 | ``` 199 | 200 | 201 | 202 | ### __iter__ 203 | 204 | ```Python 205 | __iter__(self) 206 | ``` 207 | 208 | 209 | 210 | ### __next__ 211 | 212 | ```Python 213 | __next__(self) 214 | ``` 215 | 216 | 217 | -------------------------------------------------------------------------------- /docs/senml_record.md: -------------------------------------------------------------------------------- 1 | 2 | # senml_record Module 3 | 4 | 5 | ## senml_record.SenmlRecord Objects 6 | 7 | 8 | represents a single value in a senml pack object 9 | 10 | ### __enter__ 11 | 12 | ```Python 13 | __enter__(self) 14 | ``` 15 | 16 | for supporting the 'with' statement 17 | 18 | 19 | _returns_: self 20 | 21 | ### __exit__ 22 | 23 | ```Python 24 | __exit__(self, exc_type, exc_val, exc_tb) 25 | ``` 26 | 27 | when destroyed in a 'with' statement, make certain that the item is removed from the parent list. 28 | 29 | 30 | _returns_: None 31 | 32 | ### __init__ 33 | 34 | ```Python 35 | __init__(self, name, **kwargs) 36 | ``` 37 | 38 | create a new senml record 39 | 40 | _parameters:_ 41 | 42 | - `kwargs:` optional parameters: 43 | - value: the value to store in the record 44 | - time: the timestamp to use (when was the value measured) 45 | - name: the name of hte record 46 | - unit: unit value 47 | - sum: sum value 48 | - update_time: max time before sensor will provide an updated reading 49 | - callback: a callback function taht will be called when actuator data has been found. Expects no params 50 | 51 | ### do_actuate 52 | 53 | ```Python 54 | do_actuate(self, raw, naming_map) 55 | ``` 56 | 57 | called when a raw senml record was found for this object. Stores the data and if there is a callback, calls it. 58 | 59 | _parameters:_ 60 | 61 | - `raw:` raw senml object 62 | 63 | 64 | _returns_: None 65 | 66 | ### sum 67 | 68 | get/set the sum assigned to the record 69 | 70 | ### time 71 | 72 | get the time at which the measurement for the record was taken. 73 | 74 | 75 | _returns_: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 76 | 77 | ### update_time 78 | 79 | get the time at which the next measurement is expected to be taken for this record. 80 | 81 | 82 | _returns_: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 83 | 84 | ### value 85 | 86 | get the value currently assigned to the object 87 | -------------------------------------------------------------------------------- /docs/senml_unit.md: -------------------------------------------------------------------------------- 1 | 2 | # senml_unit Module 3 | 4 | 5 | ## Functions 6 | 7 | 8 | 9 | ## senml_unit.SenmlUnits Objects 10 | 11 | 12 | 13 | 14 | ##### `SENML_UNIT_ACCELERATION` 15 | 16 | 17 | ##### `SENML_UNIT_AMPERE` 18 | 19 | 20 | ##### `SENML_UNIT_BEATS` 21 | 22 | 23 | ##### `SENML_UNIT_BECQUEREL` 24 | 25 | 26 | ##### `SENML_UNIT_BEL` 27 | 28 | 29 | ##### `SENML_UNIT_BIT` 30 | 31 | 32 | ##### `SENML_UNIT_BIT_PER_SECOND` 33 | 34 | 35 | ##### `SENML_UNIT_BPM` 36 | 37 | 38 | ##### `SENML_UNIT_CANDELA` 39 | 40 | 41 | ##### `SENML_UNIT_CANDELA_PER_SQUARE_METER` 42 | 43 | 44 | ##### `SENML_UNIT_COULOMB` 45 | 46 | 47 | ##### `SENML_UNIT_COUNTER` 48 | 49 | 50 | ##### `SENML_UNIT_CUBIC_METER` 51 | 52 | 53 | ##### `SENML_UNIT_CUBIC_METER_PER_SECOND` 54 | 55 | 56 | ##### `SENML_UNIT_DECIBEL` 57 | 58 | 59 | ##### `SENML_UNIT_DECIBEL_RELATIVE_TO_1_W` 60 | 61 | 62 | ##### `SENML_UNIT_DEGREES_CELSIUS` 63 | 64 | 65 | ##### `SENML_UNIT_DEGREES_LATITUDE` 66 | 67 | 68 | ##### `SENML_UNIT_DEGREES_LONGITUDE` 69 | 70 | 71 | ##### `SENML_UNIT_EVENT_RATE_PER_MINUTE` 72 | 73 | 74 | ##### `SENML_UNIT_EVENT_RATE_PER_SECOND` 75 | 76 | 77 | ##### `SENML_UNIT_FARAD` 78 | 79 | 80 | ##### `SENML_UNIT_GRAM` 81 | 82 | 83 | ##### `SENML_UNIT_GRAY` 84 | 85 | 86 | ##### `SENML_UNIT_HENRY` 87 | 88 | 89 | ##### `SENML_UNIT_HERTZ` 90 | 91 | 92 | ##### `SENML_UNIT_JOULE` 93 | 94 | 95 | ##### `SENML_UNIT_KATAL` 96 | 97 | 98 | ##### `SENML_UNIT_KELVIN` 99 | 100 | 101 | ##### `SENML_UNIT_KILOGRAM` 102 | 103 | 104 | ##### `SENML_UNIT_LITER` 105 | 106 | 107 | ##### `SENML_UNIT_LITER_PER_SECOND` 108 | 109 | 110 | ##### `SENML_UNIT_LUMEN` 111 | 112 | 113 | ##### `SENML_UNIT_LUX` 114 | 115 | 116 | ##### `SENML_UNIT_METER` 117 | 118 | 119 | ##### `SENML_UNIT_MOLE` 120 | 121 | 122 | ##### `SENML_UNIT_NEWTON` 123 | 124 | 125 | ##### `SENML_UNIT_OHM` 126 | 127 | 128 | ##### `SENML_UNIT_PASCAL` 129 | 130 | 131 | ##### `SENML_UNIT_PERCENTAGE_REMAINING_BATTERY_LEVEL` 132 | 133 | 134 | ##### `SENML_UNIT_PH` 135 | 136 | 137 | ##### `SENML_UNIT_RADIAN` 138 | 139 | 140 | ##### `SENML_UNIT_RATIO` 141 | 142 | 143 | ##### `SENML_UNIT_RELATIVE_HUMIDITY` 144 | 145 | 146 | ##### `SENML_UNIT_SECOND` 147 | 148 | 149 | ##### `SENML_UNIT_SECONDS_REMAINING_BATTERY_LEVEL` 150 | 151 | 152 | ##### `SENML_UNIT_SIEMENS` 153 | 154 | 155 | ##### `SENML_UNIT_SIEMENS_PER_METER` 156 | 157 | 158 | ##### `SENML_UNIT_SIEVERT` 159 | 160 | 161 | ##### `SENML_UNIT_SQUARE_METER` 162 | 163 | 164 | ##### `SENML_UNIT_STERADIAN` 165 | 166 | 167 | ##### `SENML_UNIT_TESLA` 168 | 169 | 170 | ##### `SENML_UNIT_VELOCITY` 171 | 172 | 173 | ##### `SENML_UNIT_VOLT` 174 | 175 | 176 | ##### `SENML_UNIT_WATT` 177 | 178 | 179 | ##### `SENML_UNIT_WATT_PER_SQUARE_METER` 180 | 181 | 182 | ##### `SENML_UNIT_WEBER` 183 | 184 | -------------------------------------------------------------------------------- /examples/actuator.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # actuator example 12 | 13 | from kpn_senml import * 14 | import binascii 15 | 16 | def do_actuate(record): 17 | ''' 18 | called when actuate_me receives a value. 19 | :return: None 20 | ''' 21 | print(record.value) 22 | 23 | def generic_callback(record, **kwargs): 24 | """ 25 | a generic callback, attached to the device. Called when a record is found that has not yet been registered 26 | in the pack. When this callback is called, the record will already be added to the pack. 27 | :param record: the newly found record. 28 | :return: None 29 | """ 30 | print("found record: " + record.name) 31 | print("with value: " + str(record.value)) 32 | 33 | pack = SenmlPack("device_name", generic_callback) 34 | actuate_me = SenmlRecord("actuator", callback=do_actuate) 35 | 36 | pack.add(actuate_me) 37 | 38 | json_data = '[{"bn": "device_name", "n":"actuator", "v": 10 }]' 39 | print(json_data) 40 | pack.from_json(json_data) 41 | 42 | json_data = '[{"bn": "device_name", "n":"actuator", "v": 20 }, {"n": "another_actuator", "vs": "a value"}]' 43 | print(json_data) 44 | pack.from_json(json_data) 45 | 46 | print('[{"bn": "device_name", "n":"temp", "v": 20, "u": "Cel" }]') 47 | # this represents the cbor json struct: [{-2: "device_name", 0: "temp", 1: "Cel", 2: 20}] 48 | cbor_data = binascii.unhexlify("81A4216B6465766963655F6E616D65006474656D70016343656C0214") 49 | pack.from_cbor(cbor_data) -------------------------------------------------------------------------------- /examples/base.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # base value example 12 | 13 | from kpn_senml import * 14 | import time 15 | import datetime 16 | 17 | pack = SenmlPack("device_name") 18 | temp = SenmlRecord(SenmlNames.KPN_SENML_TEMPERATURE, unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5) 19 | door_pos = SenmlRecord("doorPos", update_time=20, value=True) 20 | int_val = SenmlRecord("int_val", sum=100) 21 | 22 | pack.add(temp) 23 | pack.add(door_pos) 24 | pack.add(int_val) 25 | 26 | random_time = datetime.datetime.strptime('Jan 1 2018 1:33PM', '%b %d %Y %I:%M%p') 27 | pack.base_time = time.mktime(random_time.timetuple()) # set a base time 28 | pack.base_value = 5 29 | pack.base_sum = 50 30 | temp.time = time.mktime(datetime.datetime.now().timetuple()) # all child objects will receive the time value 31 | 32 | 33 | 34 | print(pack.to_json()) 35 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # basic example 12 | 13 | from kpn_senml import * 14 | import time 15 | import random 16 | 17 | pack = SenmlPack("device_name") 18 | 19 | while True: 20 | with SenmlRecord("test", value=random.randint(0,1000)) as rec: # use a with statement to automatically remove the item from the list when it goes out of scope, generate a value for the record 21 | pack.add(rec) 22 | print(pack.to_json()) 23 | time.sleep(1) 24 | -------------------------------------------------------------------------------- /examples/basic2.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # basic example 12 | 13 | from kpn_senml import * 14 | import time 15 | 16 | pack = SenmlPack("device_name") 17 | temp = SenmlRecord(SenmlNames.KPN_SENML_TEMPERATURE, unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5) 18 | door_pos = SenmlRecord("doorPos", update_time=20, value=True) 19 | str_val = SenmlRecord("str val") 20 | 21 | pack.add(temp) 22 | pack.add(door_pos) 23 | pack.add(str_val) 24 | 25 | while True: 26 | temp.value = temp.value + 1.1 27 | door_pos.value = not door_pos.value 28 | str_val.value = "test" 29 | print(pack.to_json()) 30 | time.sleep(1) 31 | -------------------------------------------------------------------------------- /examples/cbor_basic.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # cbor basic example 12 | 13 | from kpn_senml import * 14 | import time 15 | import random 16 | import cbor2 17 | 18 | pack = SenmlPack("device_name") 19 | 20 | while True: 21 | with SenmlRecord("test", value=random.randint(0,1000)) as rec: # use a with statement to automatically remove the item from the list when it goes out of scope, generate a value for the record 22 | pack.add(rec) 23 | cbor_val = pack.to_cbor() 24 | print(cbor_val) 25 | print(cbor2.loads(cbor_val)) # convert to string again so we can print it. 26 | time.sleep(1) 27 | -------------------------------------------------------------------------------- /examples/custom_record.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # custom records example 12 | 13 | 14 | from kpn_senml import * 15 | import datetime 16 | import time 17 | 18 | 19 | class Coordinates(SenmlRecord): 20 | 21 | def __init__(self, name, **kwargs): 22 | '''overriding the init function so we can initiate the 3 senml records that will represent lat,lon, alt''' 23 | self._lat = SenmlRecord(SenmlNames.KPN_SENML_LATTITUDE, unit=SenmlUnits.SENML_UNIT_DEGREES_LATITUDE) # create these befor calling base constructor so that all can be init correctly from constructor 24 | self._lon = SenmlRecord(SenmlNames.KPN_SENML_LONGITUDE, unit=SenmlUnits.SENML_UNIT_DEGREES_LONGITUDE) 25 | self._alt = SenmlRecord(SenmlNames.KPN_SENML_ALTITUDE, unit=SenmlUnits.SENML_UNIT_METER) 26 | super(Coordinates, self).__init__(name, **kwargs) # need to call base init, to make certain all is ok. 27 | 28 | 29 | def _check_value_type(self, value): 30 | '''overriding the check on value type to make certain that only an array with 3 values is assigned: lat,lon/alt''' 31 | if (not value == None): 32 | if not isinstance(value, list): 33 | raise Exception("invalid data type: array with 3 elements expected lat, lon, alt") 34 | 35 | def _build_rec_dict(self, naming_map, appendTo): 36 | ''' 37 | override the rendering of the senml data objects. These will be converted to json or cbor 38 | :param naming_map: {dictionary} a map that determines the field names, these are different for json vs cbor 39 | :param appendTo: {list} the result list 40 | :return: None 41 | ''' 42 | self._lat._build_rec_dict(naming_map, appendTo) 43 | self._lon._build_rec_dict(naming_map, appendTo) 44 | self._alt._build_rec_dict(naming_map, appendTo) 45 | 46 | @SenmlRecord.value.setter 47 | def value(self, value): 48 | '''set the current value. 49 | this is overridden so we can pass on the values to the internal objects. It's also stored in the parent 50 | so that a 'get-value' still returns the array. 51 | ''' 52 | SenmlRecord.value.fset(self, value) # do this first, it will check the data type 53 | if value: 54 | self._lat.value = value[0] 55 | self._lon.value = value[1] 56 | self._alt.value = value[2] 57 | else: 58 | self._lat.value = None 59 | self._lon.value = None 60 | self._alt.value = None 61 | 62 | @SenmlRecord.time.setter 63 | def time(self, value): 64 | '''set the time stamp. 65 | this is overridden so we can pass on the values to the internal objects. 66 | ''' 67 | SenmlRecord.time.fset(self, value) # do this first, it will check the data type 68 | self._lat.time = value 69 | self._lon.time = value 70 | self._alt.time = value 71 | 72 | @SenmlRecord.update_time.setter 73 | def update_time(self, value): 74 | '''set the time stamp. 75 | this is overridden so we can pass on the values to the internal objects. 76 | ''' 77 | SenmlRecord.update_time.fset(self, value) # do this first, it will check the data type 78 | self._lat.update_time = value 79 | self._lon.update_time = value 80 | self._alt.update_time = value 81 | 82 | @SenmlRecord._parent.setter 83 | def _parent(self, value): 84 | '''set the time stamp. 85 | this is overridden so we can pass on the values to the internal objects. 86 | This is needed so that the child objects can correctly take base time (optionally also base-sum, base-value) into account 87 | ''' 88 | SenmlRecord._parent.fset(self, value) # do this first, it will check the data type 89 | self._lat._parent = value 90 | self._lon._parent = value 91 | self._alt._parent = value 92 | 93 | 94 | 95 | pack = SenmlPack("device_name") 96 | loc = Coordinates("location") 97 | loc2 = Coordinates("location", value=[52.0259, 5.4775, 230]) 98 | pack.add(loc) 99 | pack.add(loc2) 100 | 101 | loc.value = [51.0259, 4.4775, 10] 102 | print(pack.to_json()) 103 | 104 | random_time = datetime.datetime.strptime('Jan 1 2018 1:33PM', '%b %d %Y %I:%M%p') 105 | pack.base_time = time.mktime(random_time.timetuple()) # set a base time 106 | loc.time = time.mktime(datetime.datetime.now().timetuple()) # all child objects will receive the time value 107 | print(pack.to_json()) 108 | 109 | -------------------------------------------------------------------------------- /examples/gateway.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # gateway example 12 | 13 | from kpn_senml import * 14 | import time 15 | 16 | gateway_pack = SenmlPack("gateway") 17 | 18 | dev1_pack = SenmlPack("dev1") 19 | dev2_pack = SenmlPack("dev2") 20 | 21 | temp = SenmlRecord(SenmlNames.KPN_SENML_TEMPERATURE, unit=SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5) 22 | door_pos = SenmlRecord("doorPos", update_time=20, value=True) 23 | str_val = SenmlRecord("str val") 24 | 25 | gateway_pack.add(temp) 26 | gateway_pack.add(dev1_pack) 27 | gateway_pack.add(dev2_pack) 28 | dev1_pack.add(door_pos) 29 | dev2_pack.add(str_val) 30 | 31 | while True: 32 | temp.value = round(temp.value + 1.1, 2) # use round() to get consistent rendering length for value, otherwise there are rounding errors. 33 | door_pos.value = not door_pos.value 34 | str_val.value = "test" 35 | print(gateway_pack.to_json()) 36 | time.sleep(1) 37 | -------------------------------------------------------------------------------- /examples/gateway_actuators.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # gateway actuators example 12 | 13 | from kpn_senml import * 14 | 15 | def do_actuate(record): 16 | ''' 17 | called when actuate_me receives a value. 18 | :return: None 19 | ''' 20 | print("for known device: ") 21 | print(record.value) 22 | 23 | def device_callback(record, **kwargs): 24 | """ 25 | a generic callback, attached to the device. Called when a record is found that has not yet been registered 26 | in the pack. When this callback is called, the record will already be added to the pack. 27 | :param kwargs: optional extra parameters 28 | :param record: the newly found record. 29 | :return: None 30 | """ 31 | print("found record: " + record.name) 32 | print("with value: " + record.value) 33 | 34 | 35 | def gateway_callback(record, **kwargs): 36 | """ 37 | a generic callback, attached to the device. Called when a record is found that has not yet been registered 38 | in the pack. When this callback is called, the record will already be added to the pack. 39 | :param record: the newly found record. 40 | :param kwargs: optional extra parameters (device can be found here) 41 | :return: None 42 | """ 43 | if 'device' in kwargs and kwargs['device'] != None: 44 | print("for device: " + kwargs['device'].name) 45 | else: 46 | print("for gateway: ") 47 | print("found record: " + record.name) 48 | print("with value: " + str(record.value)) 49 | 50 | 51 | 52 | 53 | gateway = SenmlPack("gateway_name", gateway_callback) 54 | device = SenmlPack("device_name", device_callback) 55 | actuate_me = SenmlRecord("actuator", callback=do_actuate) 56 | 57 | gateway.add(device) 58 | device.add(actuate_me) 59 | gateway.from_json('[{"bn": "gateway_name", "bv": 10, "n":"temp", "v": 22},{"n": "gateway_actuator", "vb": true}, {"bn": "device_name", "n":"actuator", "v": 20 }, {"n": "another_actuator", "vs": "a value"}, {"bn": "device_2", "bv": 10.5, "n":"temp", "v": 20 }, {"n": "actuator2", "vs": "value2"}]') -------------------------------------------------------------------------------- /examples/supported_data_types.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # supported datatypes example 12 | 13 | from kpn_senml import * 14 | import cbor2 15 | 16 | pack = SenmlPack("device_name") 17 | 18 | double_val = SenmlRecord("double", value=23.5) 19 | int_val = SenmlRecord("int", value=23) 20 | bool_val = SenmlRecord("bool", value=True) 21 | str_val = SenmlRecord("str val", value="test") 22 | bytes_val = SenmlRecord("bytes", value=bytearray.fromhex(u'00 1e 05 ff')) 23 | 24 | #invalid value 25 | try: 26 | invalid = SenmlRecord("invalid", value={'a': 1}) 27 | except Exception as error: 28 | print(error) 29 | 30 | 31 | pack.add(double_val) 32 | pack.add(int_val) 33 | pack.add(bool_val) 34 | pack.add(str_val) 35 | pack.add(bytes_val) 36 | 37 | print(pack.to_json()) 38 | cbor_val = pack.to_cbor() 39 | print(cbor_val) 40 | print(cbor2.loads(cbor_val)) -------------------------------------------------------------------------------- /kpn_senml/__init__.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # include all 12 | 13 | from kpn_senml.senml_base import SenmlBase 14 | from .senml_pack import SenmlPack 15 | from .senml_record import SenmlRecord 16 | from .senml_unit import SenmlUnits 17 | from .senml_kpn_names import SenmlNames 18 | 19 | ''' 20 | The KPN SenML library helps you create and parse [senml documents](https://tools.ietf.org/html/draft-ietf-core-senml-13) 21 | in both json and cbor format. 22 | ''' -------------------------------------------------------------------------------- /kpn_senml/senml_base.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # base for senml objects 12 | 13 | class SenmlBase(object): 14 | ''' 15 | the base class for all senml objects. 16 | ''' -------------------------------------------------------------------------------- /kpn_senml/senml_kpn_names.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # sensor names 12 | 13 | from kpn_senml.senml_unit import enum 14 | 15 | 16 | SenmlNames = enum(KPN_SENML_PRESSURE="pressure", 17 | KPN_SENML_ANGLE="angle", 18 | KPN_SENML_LENGHT="length", 19 | KPN_SENML_LENGTH="length", 20 | KPN_SENML_BREADTH="breadth", 21 | KPN_SENML_HEIGHT="height", 22 | KPN_SENML_WEIGHT="weight", 23 | KPN_SENML_THICKNESS="thickness", 24 | KPN_SENML_DISTANCE="distance", 25 | KPN_SENML_AREA="area", 26 | KPN_SENML_VOLUME="volume", 27 | KPN_SENML_VELOCITY="velocity", 28 | KPN_SENML_ELECTRICCURRENT="electricCurrent", 29 | KPN_SENML_ELECTRIC_CURRENT="electricCurrent", 30 | KPN_SENML_ELECTRICPOTENTIAL="electricPotential", 31 | KPN_SENML_ELECTRIC_POTENTIAL="electricPotential", 32 | KPN_SENML_ELECTRICRESISTANCE="electricResistance", 33 | KPN_SENML_ELECTRIC_RESISTANCE="electricResistance", 34 | KPN_SENML_TEMPERATURE="temperature", 35 | KPN_SENML_ILLUMINANCE="illuminance", 36 | KPN_SENML_ALTITUDE="altitude", 37 | KPN_SENML_ACCELERATIONX="accelerationX", 38 | KPN_SENML_ACCELERATION_X="accelerationX", 39 | KPN_SENML_ACCELERATIONY="accelerationY", 40 | KPN_SENML_ACCELERATION_Y="accelerationY", 41 | KPN_SENML_ACCELERATIONZ="accelerationZ", 42 | KPN_SENML_ACCELERATION_Z="accelerationZ", 43 | KPN_SENML_HEADING="heading", 44 | KPN_SENML_LONGITUDE="longitude", 45 | KPN_SENML_LATTITUDE="latitude", 46 | KPN_SENML_LATITUDE="latitude", 47 | KPN_SENML_CARBONMONOXIDE="carbonMonoxide", 48 | KPN_SENML_CARBONDIOXIDE="carbonDioxide", 49 | KPN_SENML_SOUND="sound", 50 | KPN_SENML_FREQUENCY="frequency", 51 | KPN_SENML_BATTERYLEVEL="batteryLevel", 52 | KPN_SENML_BATTERY_LEVEL="batteryLevel", 53 | KPN_SENML_BATTERY_VOLTAGE="batteryVoltage", 54 | KPN_SENML_BATTERY_LEVEL_LOW="batteryLevelLow", 55 | KPN_SENML_HUMIDITY="humidity", 56 | KPN_SENML_POWER="power", 57 | KPN_SENML_CO_CONCENTRATION="COConcentration", 58 | KPN_SENML_CO2_CONCENTRATION="CO2Concentration", 59 | KPN_SENML_RADIUS="radius", 60 | KPN_SENML_COMPASS_X="compassX", 61 | KPN_SENML_COMPASS_Y="compassY", 62 | KPN_SENML_COMPASS_Z="compassZ", 63 | KPN_SENML_READ_SWITCH="readSwitch", 64 | KPN_SENML_PRESENCE="presence", 65 | KPN_SENML_COUNTER="counter", 66 | KPN_SENML_URL="url", 67 | KPN_SENML_TOKEN="token", 68 | KPN_SENML_FIRMWARE="firmware" 69 | ) -------------------------------------------------------------------------------- /kpn_senml/senml_pack.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # pack object 12 | 13 | 14 | from kpn_senml.senml_record import SenmlRecord 15 | from kpn_senml.senml_base import SenmlBase 16 | import json 17 | import cbor2 18 | 19 | 20 | class SenmlPackIterator: 21 | '''an iterator to walk over all records in a pack''' 22 | 23 | def __init__(self, list): 24 | self._list = list 25 | self._index = 0 26 | 27 | def __iter__(self): 28 | return self 29 | 30 | def __next__(self): 31 | if(self._index < len(self._list)): 32 | res = self._list[self._index] 33 | self._index += 1 34 | return res 35 | else: 36 | raise StopIteration() 37 | 38 | class SenmlPack(SenmlBase): 39 | ''' 40 | represents a senml pack object. This can contain multiple records but also other (child) pack objects. 41 | When the pack object only contains records, it represents the data of a device. 42 | If the pack object has child pack objects, then it represents a gateway 43 | ''' 44 | 45 | json_mappings = {'bn': 'bn', 'bt': 'bt', 'bu': 'bu', 'bv': 'bv', 'bs': 'bs', 46 | 'n': 'n', 'u': 'u', 'v': 'v', 'vs': 'vs', 'vb': 'vb', 'vd': 'vd', 's': 's', 't': 't', 'ut': 'ut'} 47 | 48 | def __init__(self, name, callback=None): 49 | ''' 50 | initialize the object 51 | :param name: {string} the name of the pack 52 | ''' 53 | self._data = [] 54 | self.name = name 55 | self._base_value = None 56 | self._base_time = None 57 | self._base_sum = None 58 | self.base_unit = None 59 | self._parent = None # a pack can also be the child of another pack. 60 | self.actuate = callback # actuate callback function 61 | 62 | def __iter__(self): 63 | return SenmlPackIterator(self._data) 64 | 65 | def __enter__(self): 66 | ''' 67 | for supporting the 'with' statement 68 | :return: self 69 | ''' 70 | return self 71 | 72 | def __exit__(self, exc_type, exc_val, exc_tb): 73 | ''' 74 | when destroyed in a 'with' statement, make certain that the item is removed from the parent list. 75 | :return: None 76 | ''' 77 | if self._parent: 78 | self._parent.remove(self) 79 | 80 | @property 81 | def base_value(self): 82 | ''' 83 | the base value of the pack. The value of the records will be subtracted by this value during rendering. 84 | While parsing, this value is added to the value of the records. 85 | :return: a number 86 | ''' 87 | return self._base_value 88 | 89 | @base_value.setter 90 | def base_value(self, value): 91 | ''' 92 | set the base value. 93 | :param value: only number allowed 94 | :return: 95 | ''' 96 | self._check_value_type(value, "base_value") 97 | self._base_value = value 98 | 99 | 100 | @property 101 | def base_sum(self): 102 | ''' 103 | the base sum of the pack. 104 | :return: a number 105 | ''' 106 | return self._base_sum 107 | 108 | @base_sum.setter 109 | def base_sum(self, value): 110 | ''' 111 | set the base sum. 112 | :param value: only number allowed 113 | :return: 114 | ''' 115 | self._check_value_type(value, "base_sum") 116 | self._base_sum = value 117 | 118 | @property 119 | def base_time(self): 120 | ''' 121 | Get the base time assigned to this pack object. 122 | While rendering, this value will be subtracted from the value of the records. 123 | :return: unix time stamp representing the base time 124 | ''' 125 | return self._base_time 126 | 127 | @base_time.setter 128 | def base_time(self, value): 129 | self._check_value_type(value, "base_time") 130 | self._base_time = value 131 | 132 | def _check_value_type(self, value, field_name): 133 | ''' 134 | checks if the type of value is allowed for senml 135 | :return: None, raisee exception if not ok. 136 | ''' 137 | if (not value == None): 138 | if not (isinstance(value, int) or isinstance(value, float)): 139 | raise Exception("invalid type for " + field_name + ", only numbers allowed") 140 | 141 | 142 | def from_json(self, data): 143 | ''' 144 | parse a json string and convert it to a senml pack structure 145 | :param data: a string containing json data. 146 | :return: None, will call the appropriate callback functions. 147 | ''' 148 | records = json.loads(data) # load the raw senml data 149 | self._process_incomming_data(records, SenmlPack.json_mappings) 150 | 151 | 152 | def _process_incomming_data(self, records, naming_map): 153 | ''' 154 | generic processor for incoming data (actuators. 155 | :param records: the list of raw senml data, parsed from a json or cbor structure 156 | :param naming_map: translates cbor to json field names (when needed). 157 | :return: None 158 | ''' 159 | cur_pack_el = self 160 | new_pack = False 161 | for item in records: 162 | if naming_map['bn'] in item: # ref to a pack element, either this or a child pack. 163 | if item[naming_map['bn']] != self.name: 164 | pack_el = next( (x for x in self._data if x.name == item[naming_map['bn']]), None ) 165 | else: 166 | pack_el = self 167 | if pack_el: 168 | cur_pack_el = pack_el 169 | new_pack = False 170 | else: 171 | device = SenmlPack(item[naming_map['bn']]) 172 | self._data.append(device) 173 | cur_pack_el = device 174 | new_pack = True 175 | 176 | if naming_map['bv'] in item: # need to copy the base value assigned to the pack element so we can do proper conversion for actuators. 177 | cur_pack_el.base_value = item[naming_map['bv']] 178 | 179 | rec_el = next( (x for x in cur_pack_el._data if x.name == item[naming_map['n']]), None ) 180 | if rec_el: 181 | rec_el.do_actuate(item, naming_map) 182 | elif new_pack: 183 | self.do_actuate(item, naming_map, cur_pack_el) 184 | else: 185 | cur_pack_el.do_actuate(item, naming_map) 186 | else: 187 | rec_el = next((x for x in self._data if x.name == item[naming_map['n']]), None) 188 | if rec_el: 189 | rec_el.do_actuate(item, naming_map) 190 | elif new_pack: 191 | self.do_actuate(item, naming_map, cur_pack_el) 192 | else: 193 | cur_pack_el.do_actuate(item, naming_map) 194 | 195 | 196 | def do_actuate(self, raw, naming_map, device = None): 197 | ''' 198 | called while parsing incoming data for a record that is not yet part of this pack object. 199 | adds a new record and raises the actuate callback of the pack with the newly created record as argument 200 | :param naming_map: 201 | :param device: optional: if the device was not found 202 | :param raw: the raw record definition, as found in the json structure. this still has invalid labels. 203 | :return: None 204 | ''' 205 | rec = SenmlRecord(raw[naming_map['n']]) 206 | if device: 207 | device.add(rec) 208 | rec._from_raw(raw, naming_map) 209 | if self.actuate: 210 | self.actuate(rec, device=device) 211 | else: 212 | self.add(rec) 213 | rec._from_raw(raw, naming_map) 214 | if self.actuate: 215 | self.actuate(rec, device=None) 216 | 217 | 218 | def to_json(self): 219 | ''' 220 | render the content of this object to a string. 221 | :return: a string representing the senml pack object 222 | ''' 223 | converted = [] 224 | self._build_rec_dict(SenmlPack.json_mappings, converted) 225 | return json.dumps(converted) 226 | 227 | 228 | def _build_rec_dict(self, naming_map, appendTo): 229 | ''' 230 | converts the object to a senml object with the proper naming in place. 231 | This can be recursive: a pack can contain other packs. 232 | :param naming_map: a dictionary used to pick the correct field names for either senml json or senml cbor 233 | :return: None 234 | ''' 235 | internalList = [] 236 | for item in self._data: 237 | item._build_rec_dict(naming_map,internalList) 238 | if len(internalList) > 0: 239 | first_rec = internalList[0] 240 | else: 241 | first_rec = {} 242 | internalList.append(first_rec) 243 | 244 | if self.name: 245 | first_rec[naming_map['bn']] = self.name 246 | if self.base_value: 247 | first_rec[naming_map['bv']] = self.base_value 248 | if self.base_unit: 249 | first_rec[naming_map['bu']] = self.base_unit 250 | if self.base_sum: 251 | first_rec[naming_map['bs']] = self.base_sum 252 | if self.base_time: 253 | first_rec[naming_map['bt']] = self.base_time 254 | appendTo.extend(internalList) 255 | 256 | def from_cbor(self, data): 257 | ''' 258 | parse a cbor data byte array to a senml pack structure. 259 | :param data: a byte array. 260 | :return: None 261 | ''' 262 | records = cbor2.loads(data) # load the raw senml data 263 | naming_map = {'bn': -2, 'bt': -3, 'bu': -4, 'bv': -5, 'bs': -16, 264 | 'n': 0, 'u': 1, 'v': 2, 'vs': 3, 'vb': 4, 'vd': 8, 's': 5, 't': 6, 'ut': 7} 265 | self._process_incomming_data(records, naming_map) 266 | 267 | def to_cbor(self): 268 | ''' 269 | render the content of this object to a cbor byte array 270 | :return: a byte array 271 | ''' 272 | naming_map = {'bn': -2, 'bt': -3, 'bu': -4, 'bv': -5, 'bs': -16, 273 | 'n': 0, 'u': 1, 'v': 2, 'vs': 3, 'vb': 4, 'vd': 8, 's': 5, 't': 6, 'ut': 7} 274 | converted = [] 275 | self._build_rec_dict(naming_map, converted) 276 | return cbor2.dumps(converted) 277 | 278 | def add(self, item): 279 | ''' 280 | adds the item to the list of records 281 | :param item: {SenmlRecord} the item that needs to be added to the pack 282 | :return: None 283 | ''' 284 | if not (isinstance(item, SenmlBase)): 285 | raise Exception('invalid type of param, SenmlRecord or SenmlPack expected') 286 | if not item._parent == None: 287 | raise Exception('item is already part of a pack') 288 | 289 | self._data.append(item) 290 | item._parent = self 291 | 292 | def remove(self, item): 293 | ''' 294 | removes the item from the pack 295 | :param item: {SenmlRecord} the item that needs to be removed 296 | :return: None 297 | ''' 298 | if not (isinstance(item, SenmlBase)): 299 | raise Exception('invalid type of param, SenmlRecord or SenmlPack expected') 300 | if not item._parent == self: 301 | raise Exception('item is not part of this pack') 302 | 303 | self._data.remove(item) 304 | item._parent = None 305 | 306 | def clear(self): 307 | ''' 308 | clear the list of the pack 309 | :return: None 310 | ''' 311 | for item in self._data: 312 | item._parent = None 313 | self._data = [] -------------------------------------------------------------------------------- /kpn_senml/senml_record.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # record object 12 | 13 | import base64 14 | from kpn_senml.senml_base import SenmlBase 15 | 16 | 17 | class SenmlRecord(SenmlBase): 18 | '''represents a single value in a senml pack object''' 19 | 20 | def __init__(self, name, **kwargs): 21 | ''' 22 | create a new senml record 23 | :param kwargs: optional parameters: 24 | - value: the value to store in the record 25 | - time: the timestamp to use (when was the value measured) 26 | - name: the name of hte record 27 | - unit: unit value 28 | - sum: sum value 29 | - update_time: max time before sensor will provide an updated reading 30 | - callback: a callback function taht will be called when actuator data has been found. Expects no params 31 | ''' 32 | self.__parent = None # using double __ cause it's a field for an internal property 33 | self._unit = None # declare and init internal fields 34 | self._value = None 35 | self._time = None 36 | self._sum = None 37 | self._update_time = None 38 | 39 | self._parent = None # internal reference to the parent object 40 | self.name = name 41 | self.unit = kwargs.get('unit', None) 42 | self.value = kwargs.get('value',None) 43 | self.time = kwargs.get('time',None) 44 | self.sum = kwargs.get('sum', None) 45 | self.update_time = kwargs.get('update_time', None) 46 | self.actuate = kwargs.get('callback', None) # actuate callback function 47 | 48 | 49 | def __enter__(self): 50 | ''' 51 | for supporting the 'with' statement 52 | :return: self 53 | ''' 54 | return self 55 | 56 | def __exit__(self, exc_type, exc_val, exc_tb): 57 | ''' 58 | when destroyed in a 'with' statement, make certain that the item is removed from the parent list. 59 | :return: None 60 | ''' 61 | if self._parent: 62 | self._parent.remove(self) 63 | 64 | def _check_value_type(self, value): 65 | ''' 66 | checks if the type of value is allowed for senml 67 | :return: None, raisee exception if not ok. 68 | ''' 69 | if (not value == None): 70 | if not (isinstance(value, bool) or isinstance(value, int) or 71 | isinstance(value, float) or isinstance(value, bytearray) or 72 | isinstance(value, str)): 73 | raise Exception("invalid type for value, only numbers, strings, boolean and byte arrays allowed") 74 | 75 | def _check_number_type(self, value, field_name): 76 | ''' 77 | checks if the type of value is allowed for senml 78 | :return: None, raisee exception if not ok. 79 | ''' 80 | if (not value == None): 81 | if not (isinstance(value, int) or isinstance(value, float)): 82 | raise Exception("invalid type for " + field_name + ", only numbers allowed") 83 | 84 | 85 | @property 86 | def value(self): 87 | '''get the value currently assigned to the object''' 88 | return self._value 89 | 90 | @value.setter 91 | def value(self, value): 92 | '''set the current value. Will not automatically update the time stamp. This has to be done seperatly for more 93 | finegrained control 94 | Note: when the value is a float, you can control rounding in the rendered output by using the function 95 | round() while assigning the value. ex: record.value = round(12.2 / 1.5423, 2) 96 | ''' 97 | self._check_value_type(value) 98 | self._value = value 99 | 100 | 101 | @property 102 | def time(self): 103 | ''' get the time at which the measurement for the record was taken. 104 | :return: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 105 | ''' 106 | return self._time 107 | 108 | @time.setter 109 | def time(self, value): 110 | ''' 111 | Set the time at which the measurement for this record was taken. 112 | This should be the absolute value. Any adjustments that need to be done for the base-time of the pack are 113 | done automatically during rendering. 114 | :param value: a unix time stamp 115 | :return: None. 116 | ''' 117 | self._check_number_type(value, 'time') 118 | self._time = value 119 | 120 | @property 121 | def update_time(self): 122 | ''' get the time at which the next measurement is expected to be taken for this record. 123 | :return: a unix time stamp. This is the absolute value, not adjusted to the base time of the pack. 124 | ''' 125 | return self._update_time 126 | 127 | @update_time.setter 128 | def update_time(self, value): 129 | ''' 130 | Set the time at which the next measurement is expected to be taken for this record. 131 | This should be the absolute value. Any adjustments that need to be done for the base-time of the pack are 132 | done automatically during rendering. 133 | :param value: a unix time stamp 134 | :return: None. 135 | ''' 136 | self._check_number_type(value, 'update_time') 137 | self._update_time = value 138 | 139 | @property 140 | def sum(self): 141 | return self._sum 142 | 143 | @sum.setter 144 | def sum(self, value): 145 | self._check_number_type(value, 'sum') 146 | self._sum = value 147 | 148 | @property 149 | def _parent(self): 150 | ''' 151 | the parent pack object for this record. This is a property so that inheriters can override and do custom 152 | actions when the parent is set (like passing it on to their children 153 | :return: a SenMLBase object that contains this object as child. 154 | ''' 155 | return self.__parent 156 | 157 | @_parent.setter 158 | def _parent(self, value): 159 | ''' 160 | Set the parent pack object for this record. This is a property so that inheriters can override and do custom 161 | actions when the parent is set (like passing it on to their children 162 | :param value: the object to assign as parent. 163 | :return: None 164 | ''' 165 | self.__parent = value 166 | 167 | def _build_rec_dict(self, naming_map, appendTo): 168 | ''' 169 | converts the object to a dictionary that can be rendered to senml. 170 | :param naming_map: a dictionary that maps the field names to senml json or senml cbor. keys are in the 171 | form 'n', 'v',... values for 'n' are either 'n' or 0 (number is for cbor) 172 | :return: a senml dictionary representation of the record 173 | ''' 174 | result = { } 175 | 176 | if self.name: 177 | result[naming_map['n']] = self.name 178 | 179 | if self._sum: 180 | if self._parent and self._parent.base_sum: 181 | result[naming_map['s']] = self._sum - self._parent.base_sum 182 | else: 183 | result[naming_map['s']] = self._sum 184 | elif isinstance(self._value, bool): 185 | result[naming_map['vb']] = self._value 186 | elif isinstance(self._value, int) or isinstance(self._value, float): 187 | if self._parent and self._parent.base_value: 188 | result[naming_map['v']] = self._value - self._parent.base_value 189 | else: 190 | result[naming_map['v']] = self._value 191 | elif isinstance(self._value, str): 192 | result[naming_map['vs']] = self._value 193 | elif isinstance(self._value, bytearray): 194 | if naming_map['vd'] == 'vd': # neeed to make a distinction between json (needs base64) and cbor (needs binary) 195 | result[naming_map['vd']] = base64.b64encode(self._value) 196 | else: 197 | result[naming_map['vd']] = self._value 198 | else: 199 | raise Exception("sum or value of type bootl, number, string or byte-array is required") 200 | 201 | if self._time: 202 | if self._parent and self._parent.base_time: 203 | result[naming_map['t']] = self._time - self._parent.base_time 204 | else: 205 | result[naming_map['t']] = self._time 206 | 207 | if self.unit: 208 | result[naming_map['u']] = self.unit 209 | 210 | if self._update_time: 211 | if self._parent and self._parent.base_time: 212 | result[naming_map['ut']] = self._update_time - self._parent.base_time 213 | else: 214 | result[naming_map['ut']] = self._update_time 215 | 216 | appendTo.append(result) 217 | 218 | 219 | def _from_raw(self, raw, naming_map): 220 | ''' 221 | extracts te data from the raw record. Used during parsing of incoming data. 222 | :param raw: a raw senml record which still contains the original field names 223 | :param naming_map: used to map cbor names to json field names 224 | :return: None 225 | ''' 226 | if naming_map['v'] in raw: 227 | val = raw[naming_map['v']] 228 | if self._parent and self._parent.base_value: 229 | val += self._parent.base_value 230 | elif naming_map['vs'] in raw: 231 | val = raw[naming_map['vs']] 232 | elif naming_map['vb'] in raw: 233 | val = raw[naming_map['vb']] 234 | elif naming_map['vd'] in raw: 235 | val = base64.b64decode(raw[naming_map['vb']]) 236 | else: 237 | val = None 238 | self.value = val 239 | 240 | def do_actuate(self, raw, naming_map): 241 | ''' 242 | called when a raw senml record was found for this object. Stores the data and if there is a callback, calls it. 243 | :param raw: raw senml object 244 | :return: None 245 | ''' 246 | self._from_raw(raw, naming_map) 247 | if self.actuate: 248 | self.actuate(self) -------------------------------------------------------------------------------- /kpn_senml/senml_unit.py: -------------------------------------------------------------------------------- 1 | # _ __ ____ _ _ 2 | # | |/ / | _ \ | \ | | 3 | # | ' / | |_) | | \| | 4 | # | . \ | __/ | |\ | 5 | # |_|\_\ |_| |_| \_| 6 | # 7 | # (c) 2018 KPN 8 | # License: MIT license. 9 | # Author: Jan Bogaerts 10 | # 11 | # unit names 12 | 13 | def enum(**enums): 14 | return type('Enum', (), enums) 15 | 16 | SenmlUnits = enum(SENML_UNIT_METER="m", 17 | SENML_UNIT_KILOGRAM="kg", 18 | SENML_UNIT_GRAM="g", 19 | SENML_UNIT_SECOND="s", 20 | SENML_UNIT_AMPERE="A", 21 | SENML_UNIT_KELVIN="K", 22 | SENML_UNIT_CANDELA="cd", 23 | SENML_UNIT_MOLE="mol", 24 | SENML_UNIT_HERTZ="Hz", 25 | SENML_UNIT_RADIAN="rad", 26 | SENML_UNIT_STERADIAN="sr", 27 | SENML_UNIT_NEWTON="N", 28 | SENML_UNIT_PASCAL="Pa", 29 | SENML_UNIT_JOULE="J", 30 | SENML_UNIT_WATT="W", 31 | SENML_UNIT_COULOMB="C", 32 | SENML_UNIT_VOLT="V", 33 | SENML_UNIT_FARAD="F", 34 | SENML_UNIT_OHM="Ohm", 35 | SENML_UNIT_SIEMENS="S", 36 | SENML_UNIT_WEBER="Wb", 37 | SENML_UNIT_TESLA="T", 38 | SENML_UNIT_HENRY="H", 39 | SENML_UNIT_DEGREES_CELSIUS="Cel", 40 | SENML_UNIT_LUMEN="lm", 41 | SENML_UNIT_LUX="lx", 42 | SENML_UNIT_BECQUEREL="Bq", 43 | SENML_UNIT_GRAY="Gy", 44 | SENML_UNIT_SIEVERT="Sv", 45 | SENML_UNIT_KATAL="kat", 46 | SENML_UNIT_SQUARE_METER="m2", 47 | SENML_UNIT_CUBIC_METER="m3", 48 | SENML_UNIT_LITER="l", 49 | SENML_UNIT_VELOCITY="m/s", 50 | SENML_UNIT_ACCELERATION="m/s2", 51 | SENML_UNIT_CUBIC_METER_PER_SECOND="m3/s", 52 | SENML_UNIT_LITER_PER_SECOND="l/s", 53 | SENML_UNIT_WATT_PER_SQUARE_METER="W/m2", 54 | SENML_UNIT_CANDELA_PER_SQUARE_METER="cd/m2", 55 | SENML_UNIT_BIT="bit", 56 | SENML_UNIT_BIT_PER_SECOND="bit/s", 57 | SENML_UNIT_DEGREES_LATITUDE="lat", 58 | SENML_UNIT_DEGREES_LONGITUDE="lon", 59 | SENML_UNIT_PH="pH", 60 | SENML_UNIT_DECIBEL="dB", 61 | SENML_UNIT_DECIBEL_RELATIVE_TO_1_W="dBW", 62 | SENML_UNIT_BEL="Bspl", 63 | SENML_UNIT_COUNTER="count", 64 | SENML_UNIT_RATIO="//", 65 | SENML_UNIT_RELATIVE_HUMIDITY="%RH", 66 | SENML_UNIT_PERCENTAGE_REMAINING_BATTERY_LEVEL="%EL", 67 | SENML_UNIT_SECONDS_REMAINING_BATTERY_LEVEL="EL", 68 | SENML_UNIT_EVENT_RATE_PER_SECOND="1/s", 69 | SENML_UNIT_EVENT_RATE_PER_MINUTE="1/min", 70 | SENML_UNIT_BPM="beat/min", 71 | SENML_UNIT_BEATS="beats", 72 | SENML_UNIT_SIEMENS_PER_METER="S/m") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cbor2==4.1.0 -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest==3.8.1 2 | pytest_cov>=2.5.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def get_requires(filename): 7 | requirements = [] 8 | with open(filename) as req_file: 9 | for line in req_file.read().splitlines(): 10 | if not line.strip().startswith("#"): 11 | requirements.append(line) 12 | return requirements 13 | 14 | 15 | setup( 16 | name='kpn_senml', 17 | version='1.1.1', 18 | packages=find_packages(exclude=['tests']), 19 | url='https://kpn-iot.github.io/senml-python-library/', 20 | license='MIT', 21 | author='Jan Bogaerts', 22 | author_email='jb@elastetic.com', 23 | description='generate and parse senml json and cbor data', 24 | long_description='With this library you can generate senml packs containing sensor data in both json and cbor format. It can also parse senml data in both json and cbor format in order to support actuators.', 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'Topic :: Software Development :: Embedded Systems', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Programming Language :: Python :: 2', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Operating System :: OS Independent', 34 | ], 35 | keywords='senml kpn cbor json', 36 | install_requires=get_requires('requirements.txt'), 37 | extras_require={'test': get_requires('requirements_test.txt')}, 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpn-iot/senml-python-library/562ab57920a7abe4b2f960a58b403966d065241b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_names.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import kpn_senml 6 | 7 | 8 | def test_enums(): 9 | assert kpn_senml.SenmlNames.KPN_SENML_PRESSURE == "pressure" 10 | assert kpn_senml.SenmlNames.KPN_SENML_ANGLE == "angle" 11 | assert kpn_senml.SenmlNames.KPN_SENML_LENGHT == "length" 12 | assert kpn_senml.SenmlNames.KPN_SENML_BREADTH == "breadth" 13 | assert kpn_senml.SenmlNames.KPN_SENML_HEIGHT == "height" 14 | assert kpn_senml.SenmlNames.KPN_SENML_WEIGHT == "weight" 15 | assert kpn_senml.SenmlNames.KPN_SENML_THICKNESS == "thickness" 16 | assert kpn_senml.SenmlNames.KPN_SENML_DISTANCE == "distance" 17 | assert kpn_senml.SenmlNames.KPN_SENML_AREA == "area" 18 | assert kpn_senml.SenmlNames.KPN_SENML_VOLUME == "volume" 19 | assert kpn_senml.SenmlNames.KPN_SENML_VELOCITY == "velocity" 20 | assert kpn_senml.SenmlNames.KPN_SENML_ELECTRICCURRENT == "electricCurrent" 21 | assert kpn_senml.SenmlNames.KPN_SENML_ELECTRICPOTENTIAL == "electricPotential" 22 | assert kpn_senml.SenmlNames.KPN_SENML_ELECTRICRESISTANCE == "electricResistance" 23 | assert kpn_senml.SenmlNames.KPN_SENML_TEMPERATURE == "temperature" 24 | assert kpn_senml.SenmlNames.KPN_SENML_ILLUMINANCE == "illuminance" 25 | assert kpn_senml.SenmlNames.KPN_SENML_ALTITUDE == "altitude" 26 | assert kpn_senml.SenmlNames.KPN_SENML_ACCELERATIONX == "accelerationX" 27 | assert kpn_senml.SenmlNames.KPN_SENML_ACCELERATIONY == "accelerationY" 28 | assert kpn_senml.SenmlNames.KPN_SENML_ACCELERATIONZ == "accelerationZ" 29 | assert kpn_senml.SenmlNames.KPN_SENML_HEADING == "heading" 30 | assert kpn_senml.SenmlNames.KPN_SENML_LONGITUDE == "longitude" 31 | assert kpn_senml.SenmlNames.KPN_SENML_LATTITUDE == "latitude" 32 | assert kpn_senml.SenmlNames.KPN_SENML_CARBONMONOXIDE == "carbonMonoxide" 33 | assert kpn_senml.SenmlNames.KPN_SENML_CARBONDIOXIDE == "carbonDioxide" 34 | assert kpn_senml.SenmlNames.KPN_SENML_SOUND == "sound" 35 | assert kpn_senml.SenmlNames.KPN_SENML_FREQUENCY == "frequency" 36 | assert kpn_senml.SenmlNames.KPN_SENML_BATTERYLEVEL == "batteryLevel" 37 | assert kpn_senml.SenmlNames.KPN_SENML_HUMIDITY == "humidity" 38 | -------------------------------------------------------------------------------- /tests/test_records.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import kpn_senml 6 | import random 7 | import json 8 | import cbor2 9 | 10 | 11 | def test_basic(): 12 | pack = kpn_senml.SenmlPack("foobar") 13 | with kpn_senml.SenmlRecord("qux", value=random.randint(1,1000)) as record: 14 | pack.add(record) 15 | json_data = json.loads(pack.to_json()) 16 | 17 | assert json_data[0]["bn"] == "foobar" 18 | assert json_data[0]["n"] == "qux" 19 | assert 1000 > json_data[0]["v"] > 1 20 | 21 | 22 | def test_cbor(): 23 | pack = kpn_senml.SenmlPack("device_name") 24 | with kpn_senml.SenmlRecord("test", value=random.randint(0, 1000)) as record: 25 | pack.add(record) 26 | cbor_data = cbor2.loads(pack.to_cbor()) 27 | 28 | assert 'device_name' in cbor_data[0].values() 29 | assert 'test' in cbor_data[0].values() 30 | 31 | 32 | def test_gateway(): 33 | gateway_pack = kpn_senml.SenmlPack("gateway") 34 | dev1_pack = kpn_senml.SenmlPack("dev1") 35 | dev2_pack = kpn_senml.SenmlPack("dev2") 36 | 37 | temp = kpn_senml.SenmlRecord(kpn_senml.SenmlNames.KPN_SENML_TEMPERATURE, unit=kpn_senml.SenmlUnits.SENML_UNIT_DEGREES_CELSIUS, value=23.5) 38 | door_pos = kpn_senml.SenmlRecord("doorPos", update_time=20, value=True) 39 | str_val = kpn_senml.SenmlRecord("str val") 40 | str_val.value = "test" 41 | 42 | gateway_pack.add(temp) 43 | gateway_pack.add(dev1_pack) 44 | gateway_pack.add(dev2_pack) 45 | dev1_pack.add(door_pos) 46 | dev2_pack.add(str_val) 47 | 48 | json_data = json.loads(gateway_pack.to_json()) 49 | assert json_data[0]["bn"] == "gateway" 50 | -------------------------------------------------------------------------------- /tests/test_units.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import kpn_senml 6 | 7 | 8 | def test_enums(): 9 | assert kpn_senml.SenmlUnits.SENML_UNIT_METER == "m" 10 | assert kpn_senml.SenmlUnits.SENML_UNIT_KILOGRAM == "kg" 11 | assert kpn_senml.SenmlUnits.SENML_UNIT_GRAM == "g" 12 | assert kpn_senml.SenmlUnits.SENML_UNIT_SECOND == "s" 13 | assert kpn_senml.SenmlUnits.SENML_UNIT_AMPERE == "A" 14 | assert kpn_senml.SenmlUnits.SENML_UNIT_KELVIN == "K" 15 | assert kpn_senml.SenmlUnits.SENML_UNIT_CANDELA == "cd" 16 | assert kpn_senml.SenmlUnits.SENML_UNIT_MOLE == "mol" 17 | assert kpn_senml.SenmlUnits.SENML_UNIT_HERTZ == "Hz" 18 | assert kpn_senml.SenmlUnits.SENML_UNIT_RADIAN == "rad" 19 | assert kpn_senml.SenmlUnits.SENML_UNIT_STERADIAN == "sr" 20 | assert kpn_senml.SenmlUnits.SENML_UNIT_NEWTON == "N" 21 | assert kpn_senml.SenmlUnits.SENML_UNIT_PASCAL == "Pa" 22 | assert kpn_senml.SenmlUnits.SENML_UNIT_JOULE == "J" 23 | assert kpn_senml.SenmlUnits.SENML_UNIT_WATT == "W" 24 | assert kpn_senml.SenmlUnits.SENML_UNIT_COULOMB == "C" 25 | assert kpn_senml.SenmlUnits.SENML_UNIT_VOLT == "V" 26 | assert kpn_senml.SenmlUnits.SENML_UNIT_FARAD == "F" 27 | assert kpn_senml.SenmlUnits.SENML_UNIT_OHM == "Ohm" 28 | assert kpn_senml.SenmlUnits.SENML_UNIT_SIEMENS == "S" 29 | assert kpn_senml.SenmlUnits.SENML_UNIT_WEBER == "Wb" 30 | assert kpn_senml.SenmlUnits.SENML_UNIT_TESLA == "T" 31 | assert kpn_senml.SenmlUnits.SENML_UNIT_HENRY == "H" 32 | assert kpn_senml.SenmlUnits.SENML_UNIT_DEGREES_CELSIUS == "Cel" 33 | assert kpn_senml.SenmlUnits.SENML_UNIT_LUMEN == "lm" 34 | assert kpn_senml.SenmlUnits.SENML_UNIT_LUX == "lx" 35 | assert kpn_senml.SenmlUnits.SENML_UNIT_BECQUEREL == "Bq" 36 | assert kpn_senml.SenmlUnits.SENML_UNIT_GRAY == "Gy" 37 | assert kpn_senml.SenmlUnits.SENML_UNIT_SIEVERT == "Sv" 38 | assert kpn_senml.SenmlUnits.SENML_UNIT_KATAL == "kat" 39 | assert kpn_senml.SenmlUnits.SENML_UNIT_SQUARE_METER == "m2" 40 | assert kpn_senml.SenmlUnits.SENML_UNIT_CUBIC_METER == "m3" 41 | assert kpn_senml.SenmlUnits.SENML_UNIT_LITER == "l" 42 | assert kpn_senml.SenmlUnits.SENML_UNIT_VELOCITY == "m/s" 43 | assert kpn_senml.SenmlUnits.SENML_UNIT_ACCELERATION == "m/s2" 44 | assert kpn_senml.SenmlUnits.SENML_UNIT_CUBIC_METER_PER_SECOND == "m3/s" 45 | assert kpn_senml.SenmlUnits.SENML_UNIT_LITER_PER_SECOND == "l/s" 46 | assert kpn_senml.SenmlUnits.SENML_UNIT_WATT_PER_SQUARE_METER == "W/m2" 47 | assert kpn_senml.SenmlUnits.SENML_UNIT_CANDELA_PER_SQUARE_METER == "cd/m2" 48 | assert kpn_senml.SenmlUnits.SENML_UNIT_BIT == "bit" 49 | assert kpn_senml.SenmlUnits.SENML_UNIT_BIT_PER_SECOND == "bit/s" 50 | assert kpn_senml.SenmlUnits.SENML_UNIT_DEGREES_LATITUDE == "lat" 51 | assert kpn_senml.SenmlUnits.SENML_UNIT_DEGREES_LONGITUDE == "lon" 52 | assert kpn_senml.SenmlUnits.SENML_UNIT_PH == "pH" 53 | assert kpn_senml.SenmlUnits.SENML_UNIT_DECIBEL == "dB" 54 | assert kpn_senml.SenmlUnits.SENML_UNIT_DECIBEL_RELATIVE_TO_1_W == "dBW" 55 | assert kpn_senml.SenmlUnits.SENML_UNIT_BEL == "Bspl" 56 | assert kpn_senml.SenmlUnits.SENML_UNIT_COUNTER == "count" 57 | assert kpn_senml.SenmlUnits.SENML_UNIT_RATIO == "//" 58 | assert kpn_senml.SenmlUnits.SENML_UNIT_RELATIVE_HUMIDITY == "%RH" 59 | assert kpn_senml.SenmlUnits.SENML_UNIT_PERCENTAGE_REMAINING_BATTERY_LEVEL == "%EL" 60 | assert kpn_senml.SenmlUnits.SENML_UNIT_SECONDS_REMAINING_BATTERY_LEVEL == "EL" 61 | assert kpn_senml.SenmlUnits.SENML_UNIT_EVENT_RATE_PER_SECOND == "1/s" 62 | assert kpn_senml.SenmlUnits.SENML_UNIT_EVENT_RATE_PER_MINUTE == "1/min" 63 | assert kpn_senml.SenmlUnits.SENML_UNIT_BPM == "beat/min" 64 | assert kpn_senml.SenmlUnits.SENML_UNIT_BEATS == "beats" 65 | assert kpn_senml.SenmlUnits.SENML_UNIT_SIEMENS_PER_METER == "S/m" 66 | --------------------------------------------------------------------------------