diff --git a/docs/index.md b/docs/index.md index bde3b6c..3c837a6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ work in progress. - python :: dateutil - python :: httpx - python :: authlib -- openHAB version 3 +- openHAB version 3 / 4 # Installation @@ -29,6 +29,9 @@ Example usage of the library: ```python +import datetime +import json + from openhab import OpenHAB base_url = 'http://localhost:8080/rest' @@ -69,6 +72,22 @@ lights_group.on() # send update to each member for v in lights_group.members.values(): v.update('OFF') + +# start_time for fetching persistence data +start_time = datetime.datetime.fromtimestamp(1695504300123 / 1000, tz=datetime.UTC) + +# fetch persistence data using the OpenHAB client object +for k in openhab.get_item_persistence(knx_day_night.name, + page_length=20, + start_time=start_time + ): + print(json.dumps(k, indent=4)) + +# fetch persistence data using the item directly +for k in item.persistence(page_length=20, + start_time=start_time + ): + print(json.dumps(k, indent=4)) ``` # Note on NULL and UNDEF diff --git a/openhab/client.py b/openhab/client.py index 5af42e1..915b4dd 100644 --- a/openhab/client.py +++ b/openhab/client.py @@ -17,7 +17,7 @@ # along with python-openhab. If not, see . # -# pylint: disable=bad-indentation +import datetime import logging import typing @@ -133,7 +133,7 @@ def _check_req_return(req: httpx.Response) -> None: if not 200 <= req.status_code < 300: req.raise_for_status() - def req_get(self, uri_path: str) -> typing.Any: + def req_get(self, uri_path: str, params: typing.Optional[typing.Union[typing.Dict[str, typing.Any], list, tuple]] = None) -> typing.Any: """Helper method for initiating a HTTP GET request. Besides doing the actual request, it also checks the return value and returns the resulting decoded @@ -145,7 +145,7 @@ def req_get(self, uri_path: str) -> typing.Any: Returns: dict: Returns a dict containing the data returned by the OpenHAB REST server. """ - r = self.session.get(self.url_rest + uri_path) + r = self.session.get(f'{self.url_rest}{uri_path}', params=params) self._check_req_return(r) return r.json() @@ -395,7 +395,7 @@ def create_or_update_item(self, if function_name is not None: if function_name not in ( - 'EQUALITY', 'AND', 'OR', 'NAND', 'NOR', 'AVG', 'SUM', 'MAX', 'MIN', 'COUNT', 'LATEST', 'EARLIEST'): + 'EQUALITY', 'AND', 'OR', 'NAND', 'NOR', 'AVG', 'SUM', 'MAX', 'MIN', 'COUNT', 'LATEST', 'EARLIEST'): raise ValueError(f'Invalid function name "{function_name}') if function_name in ('AND', 'OR', 'NAND', 'NOR') and (not function_params or len(function_params) != 2): @@ -412,3 +412,55 @@ def create_or_update_item(self, self.logger.debug('About to create item with PUT request:\n%s', str(paramdict)) self.req_put(f'/items/{name}', json_data=paramdict, headers={'Content-Type': 'application/json'}) + + def get_item_persistence(self, + name: str, + service_id: typing.Optional[str] = None, + start_time: typing.Optional[datetime.datetime] = None, + end_time: typing.Optional[datetime.datetime] = None, + page: int = 0, + page_length: int = 0, + boundary: bool = False, + ) -> typing.Iterator[typing.Dict[str, typing.Union[str, int]]]: + """Method for fetching persistence data for a given item. + + Args: + name: The item name persistence data should be fetched for. + service_id: ID of the persistence service. If not provided the default service will be used. + start_time: Start time of the data to return. Will default to 1 day before end_time. + end_time: End time of the data to return. Will default to current time. + page: Page number of data to return. Defaults to 0 if not provided. + page_length: The length of each page. Defaults to 0 which disabled paging. + boundary: Gets one value before and after the requested period. + + Returns: + Iterator over dict values containing time and state value, e.g. + {"time": 1695588900122, + "state": "23" + } + """ + params = {'boundary': str(boundary).lower(), + 'page': page, + 'pagelength': page_length, + } + + if service_id is not None: + params['serviceId'] = service_id + + if start_time is not None: + params['starttime'] = start_time.isoformat() + + if end_time is not None: + params['endtime'] = end_time.isoformat() + + if start_time == end_time: + raise ValueError('start_time must differ from end_time') + + res = self.req_get(f'/persistence/items/{name}', params=params) + + yield from res['data'] + + while page_length > 0 and int(res['datapoints']) > 0: + params['page'] += 1 + res = self.req_get(f'/persistence/items/{name}', params=params) + yield from res['data'] diff --git a/openhab/items.py b/openhab/items.py index 95a7ba9..1aab5ad 100644 --- a/openhab/items.py +++ b/openhab/items.py @@ -17,7 +17,6 @@ # along with python-openhab. If not, see . # -# pylint: disable=bad-indentation import datetime import logging @@ -206,7 +205,7 @@ def __str__(self) -> str: """String representation.""" state = self._state if self._unitOfMeasure and not isinstance(self._state, tuple): - state = f'{self._state} {self._unitOfMeasure}' + state = f'{self._state} {self._unitOfMeasure}' return f'<{self.type_} - {self.name} : {state}>' def _update(self, value: typing.Any) -> None: @@ -283,6 +282,39 @@ def is_state_undef(self) -> bool: return False + def persistence(self, + service_id: typing.Optional[str] = None, + start_time: typing.Optional[datetime.datetime] = None, + end_time: typing.Optional[datetime.datetime] = None, + page: int = 0, + page_length: int = 0, + boundary: bool = False, + ) -> typing.Iterator[typing.Dict[str, typing.Union[str, int]]]: + """Method for fetching persistence data for a given item. + + Args: + service_id: ID of the persistence service. If not provided the default service will be used. + start_time: Start time of the data to return. Will default to 1 day before end_time. + end_time: End time of the data to return. Will default to current time. + page: Page number of data to return. Defaults to 0 if not provided. + page_length: The length of each page. Defaults to 0 which disabled paging. + boundary: Gets one value before and after the requested period. + + Returns: + Iterator over dict values containing time and state value, e.g. + {"time": 1695588900122, + "state": "23" + } + """ + yield from self.openhab.get_item_persistence(name=self.name, + service_id=service_id, + start_time=start_time, + end_time=end_time, + page=page, + page_length=page_length, + boundary=boundary, + ) + class GroupItem(Item): """String item type.""" @@ -476,9 +508,9 @@ def _rest_format(self, value: typing.Union[float, typing.Tuple[float, str], str] str or bytes: A string or bytes as converted from the value parameter. """ if isinstance(value, tuple) and len(value) == 2: - return super()._rest_format(f'{value[0]:G} {value[1]}') + 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(f'{value:G}') return super()._rest_format(value) diff --git a/test.py b/test.py index e25f4f4..2e45813 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # Georges Toth (c) 2016-present @@ -20,6 +19,8 @@ import datetime +import json + import openhab base_url = 'http://localhost:8080/rest' @@ -41,3 +42,19 @@ knx_day_night.off() print(knx_day_night.state) + +# start_time for fetching persistence data +start_time = datetime.datetime.fromtimestamp(1695504300123 / 1000, tz=datetime.UTC) + +# fetch persistence data using the OpenHAB client object +for k in openhab.get_item_persistence(knx_day_night.name, + page_length=20, + start_time=start_time + ): + print(json.dumps(k, indent=4)) + +# fetch persistence data using the item directly +for k in knx_day_night.persistence(page_length=20, + start_time=start_time + ): + print(json.dumps(k, indent=4))