Skip to content

Add support for setting unit of measure when updating Number items #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions openhab/command_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def parse(cls, value: str) -> typing.Union[None, typing.Tuple[typing.Union[int,
if value in DecimalType.UNDEFINED_STATES:
return None

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

@classmethod
def validate(cls, value: typing.Union[float, int]) -> None:
def validate(cls, value: typing.Union[int, float, typing.Tuple[typing.Union[int, float], str], str]) -> None:
"""Value validation method.

Valid values are any of data_type ``float`` or ``int``.
Valid values are any of data_type:
- ``int``
- ``float``
- a tuple of (``int`` or ``float``, ``str``) for numeric value, unit of measure
- a ``str`` that can be parsed to one of the above by ``DecimalType.parse``

Args:
value (float): The value to validate.
value (int, float, tuple, str): The value to validate.

Raises:
ValueError: Raises ValueError if an invalid value has been specified.
"""
if not isinstance(value, (int, float)):
raise ValueError()
if isinstance(value, str):
DecimalType.parse(value)
elif isinstance(value, tuple) and len(value) == 2:
DecimalType.parse(f'{value[0]} {value[1]}')
elif not isinstance(value, (int, float)):
raise ValueError()


class PercentType(CommandType):
Expand Down
23 changes: 15 additions & 8 deletions openhab/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,9 @@ def _rest_format(self, value: str) -> typing.Union[str, bytes]:
# pylint: disable=no-self-use
_value = value # type: typing.Union[str, bytes]

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

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

def __str__(self) -> str:
"""String representation."""
return f'<{self.type_} - {self.name} : {self._state}>'
state = self._state
if self._unitOfMeasure and not isinstance(self._state, tuple):
state = f'{self._state} {self._unitOfMeasure}'
return f'<{self.type_} - {self.name} : {state}>'

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

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

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

def _rest_format(self, value: float) -> str: # type: ignore[override]
def _rest_format(self, value: typing.Union[float, typing.Tuple[float, str], str]) -> typing.Union[str, bytes]:
"""Format a value before submitting to openHAB.

Args:
value (float): A float argument to be converted into a string.
value: Either a float, a tuple of (float, str), or string; in the first two cases we have to cast it to a string.

Returns:
str: The string as converted from the float parameter.
str or bytes: A string or bytes as converted from the value parameter.
"""
return str(value)
if isinstance(value, tuple) and len(value) == 2:
return super()._rest_format(f'{value[0]:G} {value[1]}')
if not isinstance(value, str):
return super()._rest_format(f'{value:G}')
return super()._rest_format(value)


class ContactItem(Item):
Expand Down
43 changes: 42 additions & 1 deletion tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import time

import openhab

Expand Down Expand Up @@ -46,12 +47,23 @@ def test_float():
assert float_obj.state == 1.0


def test_non_latin1_string():
def test_scientific_notation():
float_obj = oh.get_item('floattest')

float_obj.state = 1e-10
time.sleep(1) # Allow time for OpenHAB test instance to process state update
assert float_obj.state == 1e-10


def test_non_ascii_string():
string_obj = oh.get_item('stringtest')

string_obj.state = 'שלום'
assert string_obj.state == 'שלום'

string_obj.state = '°F'
assert string_obj.state == '°F'


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

coloritem.state = 'ON'
assert coloritem.state == (1.1, 1.2, 100.0)


def test_number_temperature():
# Tests below require the OpenHAB test instance to be configured with '°C' as
# the unit of measure for the 'Dining_Temperature' item
temperature_item = oh.get_item('Dining_Temperature')

temperature_item.state = 1.0
time.sleep(1) # Allow time for OpenHAB test instance to process state update
assert temperature_item.state == 1.0
assert temperature_item.unit_of_measure == '°C'

temperature_item.state = '2 °C'
time.sleep(1)
assert temperature_item.state == 2
assert temperature_item.unit_of_measure == '°C'

temperature_item.state = (3, '°C')
time.sleep(1)
assert temperature_item.state == 3
assert temperature_item.unit_of_measure == '°C'

# Unit of measure conversion (performed by OpenHAB server)
temperature_item.state = (32, '°F')
assert round(temperature_item.state, 2) == 0
temperature_item.state = (212, '°F')
time.sleep(1)
assert temperature_item.state == 100
assert temperature_item.unit_of_measure == '°C'
43 changes: 42 additions & 1 deletion tests/test_oauth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import os
import pathlib
import time

import openhab.oauth2_helper

Expand Down Expand Up @@ -59,12 +60,23 @@ def test_float():
assert float_obj.state == 1.0


def test_non_latin1_string():
def test_scientific_notation():
float_obj = oh.get_item('floattest')

float_obj.state = 1e-10
time.sleep(1) # Allow time for OpenHAB test instance to process state update
assert float_obj.state == 1e-10


def test_non_ascii_string():
string_obj = oh.get_item('stringtest')

string_obj.state = 'שלום'
assert string_obj.state == 'שלום'

string_obj.state = '°F'
assert string_obj.state == '°F'


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


def test_number_temperature():
# Tests below require the OpenHAB test instance to be configured with '°C' as
# the unit of measure for the 'Dining_Temperature' item
temperature_item = oh.get_item('Dining_Temperature')

temperature_item.state = 1.0
time.sleep(1) # Allow time for OpenHAB test instance to process state update
assert temperature_item.state == 1.0
assert temperature_item.unit_of_measure == '°C'

temperature_item.state = '2 °C'
time.sleep(1)
assert temperature_item.state == 2
assert temperature_item.unit_of_measure == '°C'

temperature_item.state = (3, '°C')
time.sleep(1)
assert temperature_item.state == 3
assert temperature_item.unit_of_measure == '°C'

# Unit of measure conversion (performed by OpenHAB server)
temperature_item.state = (32, '°F')
assert round(temperature_item.state, 2) == 0
temperature_item.state = (212, '°F')
time.sleep(1)
assert temperature_item.state == 100
assert temperature_item.unit_of_measure == '°C'


def test_session_logout():
assert oh.logout()