Skip to content

Commit 0a1c2c4

Browse files
authored
Add support for setting unit of measure when updating Number items (#27)
Add support for setting unit of measure when updating Number items * fix support for non-ascii strings * add support for updating status of Number items with unit of measure * include unit of measure in item string representation * fix number parsing to handle scientific notation * fix tests failing due to delay in OpenHAB number update processing * fix additional test failing due to slow number update processing
1 parent eb2af7f commit 0a1c2c4

File tree

4 files changed

+113
-16
lines changed

4 files changed

+113
-16
lines changed

openhab/command_types.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def parse(cls, value: str) -> typing.Union[None, typing.Tuple[typing.Union[int,
279279
if value in DecimalType.UNDEFINED_STATES:
280280
return None
281281

282-
m = re.match(r'(-?[0-9.]+)\s?(.*)?$', value)
282+
m = re.match(r'(-?[0-9.]+(?:[eE]-?[0-9]+)?)\s?(.*)?$', value)
283283
if m:
284284
value_value = m.group(1)
285285
value_unit_of_measure = m.group(2)
@@ -297,19 +297,27 @@ def parse(cls, value: str) -> typing.Union[None, typing.Tuple[typing.Union[int,
297297
raise ValueError()
298298

299299
@classmethod
300-
def validate(cls, value: typing.Union[float, int]) -> None:
300+
def validate(cls, value: typing.Union[int, float, typing.Tuple[typing.Union[int, float], str], str]) -> None:
301301
"""Value validation method.
302302
303-
Valid values are any of data_type ``float`` or ``int``.
303+
Valid values are any of data_type:
304+
- ``int``
305+
- ``float``
306+
- a tuple of (``int`` or ``float``, ``str``) for numeric value, unit of measure
307+
- a ``str`` that can be parsed to one of the above by ``DecimalType.parse``
304308
305309
Args:
306-
value (float): The value to validate.
310+
value (int, float, tuple, str): The value to validate.
307311
308312
Raises:
309313
ValueError: Raises ValueError if an invalid value has been specified.
310314
"""
311-
if not isinstance(value, (int, float)):
312-
raise ValueError()
315+
if isinstance(value, str):
316+
DecimalType.parse(value)
317+
elif isinstance(value, tuple) and len(value) == 2:
318+
DecimalType.parse(f'{value[0]} {value[1]}')
319+
elif not isinstance(value, (int, float)):
320+
raise ValueError()
313321

314322

315323
class PercentType(CommandType):

openhab/items.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,9 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]:
189189
# pylint: disable=no-self-use
190190
_value = value # type: typing.Union[str, bytes]
191191

192-
# Only latin-1 encoding is supported by default. If non-latin-1 characters were provided, convert them to bytes.
192+
# Only ascii encoding is supported by default. If non-ascii characters were provided, convert them to bytes.
193193
try:
194-
_ = value.encode('latin-1')
194+
_ = value.encode('ascii')
195195
except UnicodeError:
196196
_value = value.encode('utf-8')
197197

@@ -207,7 +207,10 @@ def is_undefined(self, value: str) -> bool:
207207

208208
def __str__(self) -> str:
209209
"""String representation."""
210-
return f'<{self.type_} - {self.name} : {self._state}>'
210+
state = self._state
211+
if self._unitOfMeasure and not isinstance(self._state, tuple):
212+
state = f'{self._state} {self._unitOfMeasure}'
213+
return f'<{self.type_} - {self.name} : {state}>'
211214

212215
def _update(self, value: typing.Any) -> None:
213216
"""Updates the state of an item, input validation is expected to be already done.
@@ -451,7 +454,7 @@ def _parse_rest(self, value: str) -> typing.Tuple[typing.Union[float, None], str
451454
return None, ''
452455
# m = re.match(r'''^(-?[0-9.]+)''', value)
453456
try:
454-
m = re.match(r'(-?[0-9.]+)\s?(.*)?$', value)
457+
m = re.match(r'(-?[0-9.]+(?:[eE]-?[0-9]+)?)\s?(.*)?$', value)
455458

456459
if m:
457460
value = m.group(1)
@@ -466,16 +469,20 @@ def _parse_rest(self, value: str) -> typing.Tuple[typing.Union[float, None], str
466469

467470
raise ValueError(f'{self.__class__}: unable to parse value "{value}"')
468471

469-
def _rest_format(self, value: float) -> str: # type: ignore[override]
472+
def _rest_format(self, value: typing.Union[float, typing.Tuple[float, str], str]) -> typing.Union[str, bytes]:
470473
"""Format a value before submitting to openHAB.
471474
472475
Args:
473-
value (float): A float argument to be converted into a string.
476+
value: Either a float, a tuple of (float, str), or string; in the first two cases we have to cast it to a string.
474477
475478
Returns:
476-
str: The string as converted from the float parameter.
479+
str or bytes: A string or bytes as converted from the value parameter.
477480
"""
478-
return str(value)
481+
if isinstance(value, tuple) and len(value) == 2:
482+
return super()._rest_format(f'{value[0]:G} {value[1]}')
483+
if not isinstance(value, str):
484+
return super()._rest_format(f'{value:G}')
485+
return super()._rest_format(value)
479486

480487

481488
class ContactItem(Item):

tests/test_basic.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import time
23

34
import openhab
45

@@ -46,12 +47,23 @@ def test_float():
4647
assert float_obj.state == 1.0
4748

4849

49-
def test_non_latin1_string():
50+
def test_scientific_notation():
51+
float_obj = oh.get_item('floattest')
52+
53+
float_obj.state = 1e-10
54+
time.sleep(1) # Allow time for OpenHAB test instance to process state update
55+
assert float_obj.state == 1e-10
56+
57+
58+
def test_non_ascii_string():
5059
string_obj = oh.get_item('stringtest')
5160

5261
string_obj.state = 'שלום'
5362
assert string_obj.state == 'שלום'
5463

64+
string_obj.state = '°F'
65+
assert string_obj.state == '°F'
66+
5567

5668
def test_color_item():
5769
coloritem = oh.get_item('color_item')
@@ -70,3 +82,32 @@ def test_color_item():
7082

7183
coloritem.state = 'ON'
7284
assert coloritem.state == (1.1, 1.2, 100.0)
85+
86+
87+
def test_number_temperature():
88+
# Tests below require the OpenHAB test instance to be configured with '°C' as
89+
# the unit of measure for the 'Dining_Temperature' item
90+
temperature_item = oh.get_item('Dining_Temperature')
91+
92+
temperature_item.state = 1.0
93+
time.sleep(1) # Allow time for OpenHAB test instance to process state update
94+
assert temperature_item.state == 1.0
95+
assert temperature_item.unit_of_measure == '°C'
96+
97+
temperature_item.state = '2 °C'
98+
time.sleep(1)
99+
assert temperature_item.state == 2
100+
assert temperature_item.unit_of_measure == '°C'
101+
102+
temperature_item.state = (3, '°C')
103+
time.sleep(1)
104+
assert temperature_item.state == 3
105+
assert temperature_item.unit_of_measure == '°C'
106+
107+
# Unit of measure conversion (performed by OpenHAB server)
108+
temperature_item.state = (32, '°F')
109+
assert round(temperature_item.state, 2) == 0
110+
temperature_item.state = (212, '°F')
111+
time.sleep(1)
112+
assert temperature_item.state == 100
113+
assert temperature_item.unit_of_measure == '°C'

tests/test_oauth.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import os
33
import pathlib
4+
import time
45

56
import openhab.oauth2_helper
67

@@ -59,12 +60,23 @@ def test_float():
5960
assert float_obj.state == 1.0
6061

6162

62-
def test_non_latin1_string():
63+
def test_scientific_notation():
64+
float_obj = oh.get_item('floattest')
65+
66+
float_obj.state = 1e-10
67+
time.sleep(1) # Allow time for OpenHAB test instance to process state update
68+
assert float_obj.state == 1e-10
69+
70+
71+
def test_non_ascii_string():
6372
string_obj = oh.get_item('stringtest')
6473

6574
string_obj.state = 'שלום'
6675
assert string_obj.state == 'שלום'
6776

77+
string_obj.state = '°F'
78+
assert string_obj.state == '°F'
79+
6880

6981
def test_color_item():
7082
coloritem = oh.get_item('color_item')
@@ -85,5 +97,34 @@ def test_color_item():
8597
assert coloritem.state == (1.1, 1.2, 100.0)
8698

8799

100+
def test_number_temperature():
101+
# Tests below require the OpenHAB test instance to be configured with '°C' as
102+
# the unit of measure for the 'Dining_Temperature' item
103+
temperature_item = oh.get_item('Dining_Temperature')
104+
105+
temperature_item.state = 1.0
106+
time.sleep(1) # Allow time for OpenHAB test instance to process state update
107+
assert temperature_item.state == 1.0
108+
assert temperature_item.unit_of_measure == '°C'
109+
110+
temperature_item.state = '2 °C'
111+
time.sleep(1)
112+
assert temperature_item.state == 2
113+
assert temperature_item.unit_of_measure == '°C'
114+
115+
temperature_item.state = (3, '°C')
116+
time.sleep(1)
117+
assert temperature_item.state == 3
118+
assert temperature_item.unit_of_measure == '°C'
119+
120+
# Unit of measure conversion (performed by OpenHAB server)
121+
temperature_item.state = (32, '°F')
122+
assert round(temperature_item.state, 2) == 0
123+
temperature_item.state = (212, '°F')
124+
time.sleep(1)
125+
assert temperature_item.state == 100
126+
assert temperature_item.unit_of_measure == '°C'
127+
128+
88129
def test_session_logout():
89130
assert oh.logout()

0 commit comments

Comments
 (0)