diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index b5eef206..8c252fa8 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -17,10 +17,10 @@ jobs: with: ref: master - - name: Set up Python 3.10 + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' - name: Install setuptools run: | diff --git a/docs/requirements.txt b/docs/requirements.txt index 4f465973..c0ef5d3b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ # Packages required to build the documentation sphinx == 8.1.3 sphinx-autodoc-typehints == 2.5.0 -sphinx_rtd_theme == 3.0.1 +sphinx_rtd_theme == 3.0.2 sphinx-exec-code == 0.13 autodoc_pydantic == 2.2.0 sphinx-copybutton == 0.5.2 diff --git a/requirements_setup.txt b/requirements_setup.txt index a08269ff..764fb4ca 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -1,6 +1,5 @@ aiohttp == 3.10.10 pydantic == 2.9.2 -msgspec == 0.18.6 bidict == 0.23.1 watchdog == 6.0.0 ujson == 5.10.0 diff --git a/src/HABApp/__check_dependency_packages__.py b/src/HABApp/__check_dependency_packages__.py index 7cc48198..2aff19cc 100644 --- a/src/HABApp/__check_dependency_packages__.py +++ b/src/HABApp/__check_dependency_packages__.py @@ -23,7 +23,6 @@ def get_dependencies() -> list[str]: 'ujson', 'immutables', 'javaproperties', - 'msgspec', 'typing-extensions', ] diff --git a/src/HABApp/__version__.py b/src/HABApp/__version__.py index 9d9dd113..815a10fe 100644 --- a/src/HABApp/__version__.py +++ b/src/HABApp/__version__.py @@ -10,4 +10,4 @@ # Development versions contain the DEV-COUNTER postfix: # - 24.01.0.DEV-1 -__version__ = '24.09.0.DEV-12' +__version__ = '24.09.0.DEV-13' diff --git a/src/HABApp/core/const/json.py b/src/HABApp/core/const/json.py index 8b6ee3d5..ba904df1 100644 --- a/src/HABApp/core/const/json.py +++ b/src/HABApp/core/const/json.py @@ -1,8 +1,6 @@ from collections.abc import Callable from typing import Any -import msgspec - try: import ujson @@ -12,7 +10,3 @@ import json load_json: Callable[[str], Any] = json.loads dump_json: Callable[[str], Any] = json.dumps - - -decode_struct = msgspec.json.decode -encode_struct = msgspec.json.encode diff --git a/src/HABApp/openhab/connection/handler/func_async.py b/src/HABApp/openhab/connection/handler/func_async.py index d08ad9df..a87ec1da 100644 --- a/src/HABApp/openhab/connection/handler/func_async.py +++ b/src/HABApp/openhab/connection/handler/func_async.py @@ -5,19 +5,25 @@ from typing import Any from urllib.parse import quote as quote_url -from HABApp.core.const.json import decode_struct from HABApp.core.internals import ItemRegistryItem from HABApp.openhab.definitions.rest import ( ItemChannelLinkResp, + ItemChannelLinkRespList, ItemHistoryResp, ItemResp, + ItemRespList, PersistenceServiceResp, + PersistenceServiceRespList, RootResp, ShortItemResp, + ShortItemRespList, SystemInfoRootResp, ThingResp, + ThingRespList, TransformationResp, + TransformationRespList, ) +from HABApp.openhab.definitions.rest.habapp_data import get_api_vals, load_habapp_meta from HABApp.openhab.errors import ( ItemNotEditableError, ItemNotFoundError, @@ -31,7 +37,6 @@ TransformationsRequestError, ) -from ...definitions.rest.habapp_data import get_api_vals, load_habapp_meta from . import convert_to_oh_type from .handler import delete, get, post, put @@ -48,7 +53,7 @@ async def async_get_root() -> RootResp | None: if not (b := await resp.read()): return None - return decode_struct(b, type=RootResp) + return RootResp.model_validate_json(b) # ---------------------------------------------------------------------------------------------------------------------- @@ -71,18 +76,18 @@ async def async_get_system_info(): if not (b := await resp.read()): return None - return decode_struct(b, type=SystemInfoRootResp).system_info + return SystemInfoRootResp.model_validate_json(b).system_info # ---------------------------------------------------------------------------------------------------------------------- # /items # ---------------------------------------------------------------------------------------------------------------------- -async def async_get_items() -> list[ItemResp]: +async def async_get_items() -> tuple[ItemResp, ...]: resp = await get('/rest/items', params={'metadata': '.+'}) body = await resp.read() - return decode_struct(body, type=list[ItemResp]) + return ItemRespList.validate_json(body) async def async_get_item(item: str | ItemRegistryItem) -> ItemResp | None: @@ -95,14 +100,14 @@ async def async_get_item(item: str | ItemRegistryItem) -> ItemResp | None: body = await resp.read() - return decode_struct(body, type=ItemResp) + return ItemResp.model_validate_json(body) -async def async_get_all_items_state() -> list[ShortItemResp]: +async def async_get_all_items_state() -> tuple[ShortItemResp, ...]: resp = await get('/rest/items', params={'fields': 'name,state,type'}) body = await resp.read() - return decode_struct(body, type=list[ShortItemResp]) + return ShortItemRespList.validate_json(body) async def async_item_exists(item: str | ItemRegistryItem) -> bool: @@ -163,7 +168,7 @@ async def async_create_item(item_type: str, name: str, return ret.status < 300 -async def async_remove_metadata(item: str | ItemRegistryItem, namespace: str): +async def async_remove_metadata(item: str | ItemRegistryItem, namespace: str) -> bool: # noinspection PyProtectedMember item = item if isinstance(item, str) else item._name @@ -177,7 +182,7 @@ async def async_remove_metadata(item: str | ItemRegistryItem, namespace: str): return ret.status < 300 -async def async_set_metadata(item: str | ItemRegistryItem, namespace: str, value: str, config: dict): +async def async_set_metadata(item: str | ItemRegistryItem, namespace: str, value: str, config: dict) -> bool: # noinspection PyProtectedMember item = item if isinstance(item, str) else item._name @@ -191,7 +196,7 @@ async def async_set_metadata(item: str | ItemRegistryItem, namespace: str, value if ret.status == 404: raise ItemNotFoundError.from_name(item) - elif ret.status == 405: + if ret.status == 405: raise MetadataNotEditableError.create_text(item, namespace) return ret.status < 300 @@ -199,11 +204,11 @@ async def async_set_metadata(item: str | ItemRegistryItem, namespace: str, value # ---------------------------------------------------------------------------------------------------------------------- # /things # ---------------------------------------------------------------------------------------------------------------------- -async def async_get_things() -> list[ThingResp]: +async def async_get_things() -> tuple[ThingResp, ...]: resp = await get('/rest/things') body = await resp.read() - return decode_struct(body, type=list[ThingResp]) + return ThingRespList.validate_json(body) async def async_get_thing(thing: str | ItemRegistryItem) -> ThingResp: @@ -215,7 +220,7 @@ async def async_get_thing(thing: str | ItemRegistryItem) -> ThingResp: raise ThingNotFoundError.from_uid(thing) body = await resp.read() - return decode_struct(body, type=ThingResp) + return ThingResp.model_validate_json(body) async def async_set_thing_cfg(thing: str | ItemRegistryItem, cfg: dict[str, Any]): @@ -259,7 +264,8 @@ async def async_set_thing_enabled(thing: str | ItemRegistryItem, enabled: bool): async def async_purge_links(): resp = await post('/rest/purge') if resp.status != 200: - raise LinkRequestError('Unexpected error') + msg = 'Unexpected error' + raise LinkRequestError(msg) async def async_remove_obj_links(name: str | ItemRegistryItem) -> bool: @@ -277,14 +283,15 @@ async def async_remove_obj_links(name: str | ItemRegistryItem) -> bool: return True -async def async_get_links() -> list[ItemChannelLinkResp]: +async def async_get_links() -> tuple[ItemChannelLinkResp, ...]: resp = await get('/rest/links') if resp.status != 200: - raise LinkRequestError('Unexpected error') + msg = 'Unexpected error' + raise LinkRequestError(msg) body = await resp.read() - return decode_struct(body, type=list[ItemChannelLinkResp]) + return ItemChannelLinkRespList.validate_json(body) def __get_item_link_url(item: str | ItemRegistryItem, channel: str) -> str: @@ -302,7 +309,7 @@ async def async_get_link(item: str | ItemRegistryItem, channel: str) -> ItemChan resp = await get(__get_item_link_url(item, channel), log_404=False) if resp.status == 200: body = await resp.read() - return decode_struct(body, type=ItemChannelLinkResp) + return ItemChannelLinkResp.model_validate_json(body) if resp.status == 404: raise LinkNotFoundError.from_names(item, channel) @@ -357,25 +364,25 @@ async def async_remove_link(item: str | ItemRegistryItem, channel: str): # ---------------------------------------------------------------------------------------------------------------------- # /transformations # ---------------------------------------------------------------------------------------------------------------------- -async def async_get_transformations() -> list[TransformationResp]: +async def async_get_transformations() -> tuple[TransformationResp, ...]: resp = await get('/rest/transformations') if resp.status >= 300: raise TransformationsRequestError() body = await resp.read() - return decode_struct(body, type=list[TransformationResp]) + return TransformationRespList.validate_json(body) # ---------------------------------------------------------------------------------------------------------------------- # /persistence # ---------------------------------------------------------------------------------------------------------------------- -async def async_get_persistence_services() -> list[PersistenceServiceResp]: +async def async_get_persistence_services() -> tuple[PersistenceServiceResp, ...]: resp = await get('/rest/persistence') if resp.status >= 300: raise PersistenceRequestError() body = await resp.read() - return decode_struct(body, type=list[PersistenceServiceResp]) + return PersistenceServiceRespList.validate_json(body) async def async_get_persistence_data(item: str | ItemRegistryItem, persistence: str | None, @@ -399,7 +406,7 @@ async def async_get_persistence_data(item: str | ItemRegistryItem, persistence: raise PersistenceRequestError() body = await resp.read() - return decode_struct(body, type=ItemHistoryResp) + return ItemHistoryResp.model_validate_json(body) async def async_set_persistence_data(item: str | ItemRegistryItem, persistence: str | None, diff --git a/src/HABApp/openhab/connection/plugins/plugin_things/plugin_things.py b/src/HABApp/openhab/connection/plugins/plugin_things/plugin_things.py index 162710f8..5061d3f9 100644 --- a/src/HABApp/openhab/connection/plugins/plugin_things/plugin_things.py +++ b/src/HABApp/openhab/connection/plugins/plugin_things/plugin_things.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Any -import msgspec - import HABApp import HABApp.openhab.events from HABApp.core.connections import BaseConnectionPlugin @@ -81,7 +79,7 @@ async def clean_items(self) -> None: async def load_thing_data(self, always: bool) -> list[dict[str, Any]]: if always or not self.cache_cfg or time.time() - self.cache_ts > 20: - self.cache_cfg = [msgspec.to_builtins(k) for k in await HABApp.openhab.interface_async.async_get_things()] + self.cache_cfg = [k.model_dump(mode='json') for k in await HABApp.openhab.interface_async.async_get_things()] self.cache_ts = time.time() return self.cache_cfg diff --git a/src/HABApp/openhab/definitions/rest/__init__.py b/src/HABApp/openhab/definitions/rest/__init__.py index e60351bb..944dca4d 100644 --- a/src/HABApp/openhab/definitions/rest/__init__.py +++ b/src/HABApp/openhab/definitions/rest/__init__.py @@ -1,8 +1,7 @@ -from .items import ShortItemResp, ItemResp -from .things import ThingResp, ChannelResp -from .links import ItemChannelLinkResp - +from .items import ItemResp, ItemRespList, ShortItemResp, ShortItemRespList +from .links import ItemChannelLinkResp, ItemChannelLinkRespList +from .persistence import ItemHistoryResp, PersistenceServiceResp, PersistenceServiceRespList from .root import RootResp from .systeminfo import SystemInfoRootResp -from .transformations import TransformationResp -from .persistence import ItemHistoryResp, PersistenceServiceResp +from .things import ChannelResp, ThingResp, ThingRespList +from .transformations import TransformationResp, TransformationRespList diff --git a/src/HABApp/openhab/definitions/rest/items.py b/src/HABApp/openhab/definitions/rest/items.py index 1ff72dfa..60a4d5a6 100644 --- a/src/HABApp/openhab/definitions/rest/items.py +++ b/src/HABApp/openhab/definitions/rest/items.py @@ -2,70 +2,76 @@ from typing import Any -from msgspec import Struct, field +from pydantic import BaseModel, Field, TypeAdapter # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core/src/main/java/org/openhab/core/types/StateOption.java -class StateOptionResp(Struct): +class StateOptionResp(BaseModel): value: str label: str | None = None # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core/src/main/java/org/openhab/core/types/StateDescription.java -class StateDescriptionResp(Struct, kw_only=True): +class StateDescriptionResp(BaseModel): minimum: int | float | None = None maximum: int | float | None = None step: int | float | None = None pattern: str | None = None - read_only: bool = field(name='readOnly') - options: list[StateOptionResp] + read_only: bool = Field(alias='readOnly') + options: tuple[StateOptionResp, ...] # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core/src/main/java/org/openhab/core/types/CommandOption.java -class CommandOptionResp(Struct): +class CommandOptionResp(BaseModel): command: str label: str | None = None -class CommandDescriptionResp(Struct): - command_options: list[CommandOptionResp] = field(name='commandOptions') +class CommandDescriptionResp(BaseModel): + command_options: tuple[CommandOptionResp, ...] = Field(alias='commandOptions') # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/GroupFunctionDTO.java -class GroupFunctionResp(Struct): +class GroupFunctionResp(BaseModel): name: str - params: list[str] = [] + params: tuple[str, ...] = () -class ItemResp(Struct, kw_only=True): +class ItemResp(BaseModel): # ItemDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core/src/main/java/org/openhab/core/items/dto/ItemDTO.java type: str name: str label: str | None = None category: str | None = None - tags: list[str] - groups: list[str] = field(name='groupNames') + tags: tuple[str, ...] + groups: tuple[str, ...] = Field(alias='groupNames') # EnrichedItemDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedItemDTO.java link: str | None = None state: str - transformed_state: str | None = field(default=None, name='transformedState') - state_description: StateDescriptionResp | None = field(default=None, name='stateDescription') - unit: str | None = field(default=None, name='unitSymbol') - command_description: CommandDescriptionResp | None = field(default=None, name='commandDescription') + transformed_state: str | None = Field(default=None, alias='transformedState') + state_description: StateDescriptionResp | None = Field(default=None, alias='stateDescription') + unit: str | None = Field(default=None, alias='unitSymbol') + command_description: CommandDescriptionResp | None = Field(default=None, alias='commandDescription') metadata: dict[str, Any] = {} editable: bool = True # EnrichedGroupItemDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/item/EnrichedGroupItemDTO.java - members: list[ItemResp] = [] - group_type: str | None = field(default=None, name='groupType') - group_function: GroupFunctionResp | None = field(default=None, name='function') + members: tuple[ItemResp, ...] = () + group_type: str | None = Field(default=None, alias='groupType') + group_function: GroupFunctionResp | None = Field(default=None, alias='function') -class ShortItemResp(Struct): +ItemRespList = TypeAdapter(tuple[ItemResp, ...]) + + +class ShortItemResp(BaseModel): type: str name: str state: str + + +ShortItemRespList = TypeAdapter(tuple[ShortItemResp, ...]) diff --git a/src/HABApp/openhab/definitions/rest/links.py b/src/HABApp/openhab/definitions/rest/links.py index 8fa39efd..b78f3e08 100644 --- a/src/HABApp/openhab/definitions/rest/links.py +++ b/src/HABApp/openhab/definitions/rest/links.py @@ -1,18 +1,21 @@ from typing import Any -from msgspec import Struct, field +from pydantic import BaseModel, Field, TypeAdapter -class ItemChannelLinkResp(Struct, kw_only=True): +class ItemChannelLinkResp(BaseModel): # AbstractLinkDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/dto/AbstractLinkDTO.java - item: str = field(name='itemName') + item: str = Field(alias='itemName') # ItemChannelLinkDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/link/dto/ItemChannelLinkDTO.java - channel: str = field(name='channelUID') + channel: str = Field(alias='channelUID') configuration: dict[str, Any] = {} # EnrichedItemChannelLinkDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/link/EnrichedItemChannelLinkDTO.java editable: bool + + +ItemChannelLinkRespList = TypeAdapter(tuple[ItemChannelLinkResp, ...]) diff --git a/src/HABApp/openhab/definitions/rest/persistence.py b/src/HABApp/openhab/definitions/rest/persistence.py index f0475028..313d38e6 100644 --- a/src/HABApp/openhab/definitions/rest/persistence.py +++ b/src/HABApp/openhab/definitions/rest/persistence.py @@ -1,14 +1,17 @@ -from msgspec import Struct, field +from pydantic import BaseModel, Field, TypeAdapter # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/PersistenceServiceDTO.java -class PersistenceServiceResp(Struct): +class PersistenceServiceResp(BaseModel): id: str label: str | None = None type: str | None = None -class DataPoint(Struct): +PersistenceServiceRespList = TypeAdapter(tuple[PersistenceServiceResp, ...]) + + +class DataPoint(BaseModel): time: int state: str @@ -16,8 +19,8 @@ class DataPoint(Struct): # ItemHistoryDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.persistence/src/main/java/org/openhab/core/persistence/dto/ItemHistoryDTO.java -class ItemHistoryResp(Struct): +class ItemHistoryResp(BaseModel): name: str - total_records: str | None = field(default=None, name='totalrecords') - data_points: str | None = field(default=None, name='datapoints') + total_records: str | None = Field(default=None, alias='totalrecords') + data_points: str | None = Field(default=None, alias='datapoints') data: list[DataPoint] = [] diff --git a/src/HABApp/openhab/definitions/rest/root.py b/src/HABApp/openhab/definitions/rest/root.py index 35c0fb09..c2b53ced 100644 --- a/src/HABApp/openhab/definitions/rest/root.py +++ b/src/HABApp/openhab/definitions/rest/root.py @@ -1,22 +1,22 @@ -from msgspec import Struct +from pydantic import BaseModel, Field # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/RootBean.java -class RuntimeResp(Struct, rename='camel'): +class RuntimeResp(BaseModel): version: str - build_string: str + build_string: str = Field(alias='buildString') -class LinkResp(Struct): +class LinkResp(BaseModel): type: str url: str -class RootResp(Struct, rename='camel'): +class RootResp(BaseModel): version: str locale: str - measurement_system: str - runtime_info: RuntimeResp + measurement_system: str = Field(alias='measurementSystem') + runtime_info: RuntimeResp = Field(alias='runtimeInfo') links: list[LinkResp] diff --git a/src/HABApp/openhab/definitions/rest/systeminfo.py b/src/HABApp/openhab/definitions/rest/systeminfo.py index 667528cd..d0ddee07 100644 --- a/src/HABApp/openhab/definitions/rest/systeminfo.py +++ b/src/HABApp/openhab/definitions/rest/systeminfo.py @@ -1,11 +1,14 @@ -from msgspec import Struct +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/SystemInfoBean.java -class SystemInfoResp(Struct, rename='camel', kw_only=True): +class SystemInfoResp(BaseModel): + model_config = ConfigDict(alias_generator=to_camel) + config_folder: str userdata_folder: str log_folder: str | None = None @@ -23,5 +26,5 @@ class SystemInfoResp(Struct, rename='camel', kw_only=True): uptime: int = -1 # TODO: remove default if we go OH4.1 only -class SystemInfoRootResp(Struct, rename='camel'): - system_info: SystemInfoResp +class SystemInfoRootResp(BaseModel): + system_info: SystemInfoResp = Field(alias='systemInfo') diff --git a/src/HABApp/openhab/definitions/rest/things.py b/src/HABApp/openhab/definitions/rest/things.py index c4911c2c..55ca5fbd 100644 --- a/src/HABApp/openhab/definitions/rest/things.py +++ b/src/HABApp/openhab/definitions/rest/things.py @@ -1,58 +1,61 @@ from typing import Any -from msgspec import Struct, field +from pydantic import BaseModel, Field, TypeAdapter from HABApp.openhab.definitions import ThingStatusDetailEnum, ThingStatusEnum -class ChannelResp(Struct, kw_only=True): +class ChannelResp(BaseModel): # ChannelDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/dto/ChannelDTO.java uid: str id: str - channel_type: str | None = field(default=None, name='channelTypeUID') - item_type: str | None = field(default=None, name='itemType') + channel_type: str | None = Field(default=None, alias='channelTypeUID') + item_type: str | None = Field(default=None, alias='itemType') kind: str label: str = '' description: str = '' - default_tags: list[str] = field(default=list, name='defaultTags') + default_tags: tuple[str, ...] = Field(default=tuple, alias='defaultTags') properties: dict[str, Any] = {} configuration: dict[str, Any] = {} - auto_update_policy: str = field(default='', name='autoUpdatePolicy') + auto_update_policy: str = Field(default='', alias='autoUpdatePolicy') # EnrichedChannelDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedChannelDTO.java - linked_items: list[str] = field(name='linkedItems') + linked_items: tuple[str, ...] = Field(alias='linkedItems') # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/ThingStatusInfo.java -class ThingStatusResp(Struct): +class ThingStatusResp(BaseModel): status: ThingStatusEnum - detail: ThingStatusDetailEnum = field(name='statusDetail') + detail: ThingStatusDetailEnum = Field(alias='statusDetail') description: str | None = None # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/firmware/dto/FirmwareStatusDTO.java -class FirmwareStatusResp(Struct): +class FirmwareStatusResp(BaseModel): status: str - updatable_version: str | None = field(default=None, name='updatableVersion') + updatable_version: str | None = Field(default=None, alias='updatableVersion') -class ThingResp(Struct, kw_only=True): +class ThingResp(BaseModel): # AbstractThingDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/dto/AbstractThingDTO.java label: str = '' - bridge_uid: str | None = field(default=None, name='bridgeUID') + bridge_uid: str | None = Field(default=None, alias='bridgeUID') configuration: dict[str, Any] = {} properties: dict[str, str] = {} - uid: str = field(name='UID') - thing_type: str = field(name='thingTypeUID') + uid: str = Field(alias='UID') + thing_type: str = Field(alias='thingTypeUID') location: str = '' # EnrichedThingDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.core/src/main/java/org/openhab/core/io/rest/core/thing/EnrichedThingDTO.java - channels: list[ChannelResp] = [] - status: ThingStatusResp = field(name='statusInfo') + channels: tuple[ChannelResp, ...] = [] + status: ThingStatusResp = Field(alias='statusInfo') firmware_status: FirmwareStatusResp | None = None editable: bool + + +ThingRespList = TypeAdapter(tuple[ThingResp, ...]) diff --git a/src/HABApp/openhab/definitions/rest/transformations.py b/src/HABApp/openhab/definitions/rest/transformations.py index 21265153..6ebc5f85 100644 --- a/src/HABApp/openhab/definitions/rest/transformations.py +++ b/src/HABApp/openhab/definitions/rest/transformations.py @@ -1,12 +1,15 @@ -from msgspec import Struct +from pydantic import BaseModel, TypeAdapter # Documentation of TransformationDTO # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/TransformationDTO.java -class TransformationResp(Struct): +class TransformationResp(BaseModel): uid: str label: str type: str configuration: dict[str, str] editable: bool + + +TransformationRespList = TypeAdapter(tuple[TransformationResp, ...]) diff --git a/tests/test_openhab/test_plugins/test_broken_links.py b/tests/test_openhab/test_plugins/test_broken_links.py index a82b2914..a268c8cf 100644 --- a/tests/test_openhab/test_plugins/test_broken_links.py +++ b/tests/test_openhab/test_plugins/test_broken_links.py @@ -13,14 +13,14 @@ async def _mock_things() -> list[ThingResp]: return [ ThingResp( - uid='thing_type:uid', thing_type='thing_type', - status=ThingStatusResp(status='ONLINE', detail='ONLINE'), + UID='thing_type:uid', thingTypeUID='thing_type', + statusInfo=ThingStatusResp(status='ONLINE', statusDetail='NONE'), editable=False, channels=[ - ChannelResp(uid='thing_type:uid:channel1', id='channel1', channel_type='channel1_type', - item_type='String', kind='STATE', linked_items=[]), - ChannelResp(uid='thing_type:uid:channel2', id='channel2', channel_type='channel2_type', - item_type='String', kind='STATE', linked_items=[]) + ChannelResp(uid='thing_type:uid:channel1', id='channel1', channelTypeUID='channel1_type', + itemType='String', kind='STATE', linkedItems=[]), + ChannelResp(uid='thing_type:uid:channel2', id='channel2', channelTypeUID='channel2_type', + itemType='String', kind='STATE', linkedItems=[]) ] ) ] @@ -28,10 +28,10 @@ async def _mock_things() -> list[ThingResp]: async def _mock_links() -> list[ItemChannelLinkResp]: return [ - ItemChannelLinkResp(item='item1', channel='thing_type:uid:channel1', editable=True), # okay - ItemChannelLinkResp(item='item2', channel='thing_type:uid:channel1', editable=True), # item does not exist - ItemChannelLinkResp(item='item1', channel='thing_type:uid:channel3', editable=True), # channel does not exist - ItemChannelLinkResp(item='item1', channel='other_thing:uid:channel1', editable=True), # thing does not exist + ItemChannelLinkResp(itemName='item1', channelUID='thing_type:uid:channel1', editable=True), # okay + ItemChannelLinkResp(itemName='item2', channelUID='thing_type:uid:channel1', editable=True), # item does not exist + ItemChannelLinkResp(itemName='item1', channelUID='thing_type:uid:channel3', editable=True), # channel does not exist + ItemChannelLinkResp(itemName='item1', channelUID='other_thing:uid:channel1', editable=True), # thing does not exist ] diff --git a/tests/test_openhab/test_plugins/test_load_items.py b/tests/test_openhab/test_plugins/test_load_items.py index 8226228a..6b7221ae 100644 --- a/tests/test_openhab/test_plugins/test_load_items.py +++ b/tests/test_openhab/test_plugins/test_load_items.py @@ -1,14 +1,12 @@ import logging -from json import dumps -import msgspec.json from whenever import Instant import HABApp.openhab.connection.plugins.load_items as load_items_module from HABApp.core.internals import ItemRegistry from HABApp.openhab.connection.connection import OpenhabContext from HABApp.openhab.connection.plugins import LoadOpenhabItemsPlugin -from HABApp.openhab.definitions.rest import ItemResp, ShortItemResp, ThingResp +from HABApp.openhab.definitions.rest import ItemRespList, ShortItemResp, ThingResp from HABApp.openhab.definitions.rest.things import ThingStatusResp from HABApp.openhab.items import Thing @@ -56,13 +54,13 @@ async def _mock_get_all_items(): }, ] - return msgspec.json.decode(dumps(resp), type=list[ItemResp]) + return ItemRespList.validate_python(resp) async def _mock_get_all_items_state(): return [ - ShortItemResp('Number:Length', 'ItemLength', '5 m'), - ShortItemResp('Number', 'ItemPlain', '3.14') + ShortItemResp(type='Number:Length', name='ItemLength', state='5 m'), + ShortItemResp(type='Number', name='ItemPlain', state='3.14') ] @@ -106,14 +104,14 @@ async def _mock_ret(): monkeypatch.setattr(load_items_module, 'async_get_things', _mock_ret) t1 = ThingResp( - uid='thing_1', thing_type='thing_type_1', editable=True, status=ThingStatusResp( - status='ONLINE', detail='NONE', description='' + UID='thing_1', thingTypeUID='thing_type_1', editable=True, statusInfo=ThingStatusResp( + status='ONLINE', statusDetail='NONE', description='' ) ) t2 = ThingResp( - uid='thing_2', thing_type='thing_type_2', editable=True, status=ThingStatusResp( - status='OFFLINE', detail='NONE', description='' + UID='thing_2', thingTypeUID='thing_type_2', editable=True, statusInfo=ThingStatusResp( + status='OFFLINE', statusDetail='NONE', description='' ) ) diff --git a/tests/test_openhab/test_rest/test_grp_func.py b/tests/test_openhab/test_rest/test_grp_func.py index 121bf80f..a7225acc 100644 --- a/tests/test_openhab/test_rest/test_grp_func.py +++ b/tests/test_openhab/test_rest/test_grp_func.py @@ -1,7 +1,3 @@ -from json import dumps - -from msgspec.json import decode - from HABApp.openhab.definitions.rest.items import GroupFunctionResp @@ -13,13 +9,14 @@ def test_or() -> None: 'OFF' ] } - o = decode(dumps(_in), type=GroupFunctionResp) + + o = GroupFunctionResp.model_validate(_in) assert o.name == 'OR' - assert o.params == ['ON', 'OFF'] + assert o.params == ('ON', 'OFF') def test_eq() -> None: _in = {'name': 'EQUALITY'} - o = decode(dumps(_in), type=GroupFunctionResp) + o = GroupFunctionResp.model_validate(_in) assert o.name == 'EQUALITY' - assert o.params == [] + assert o.params == () diff --git a/tests/test_openhab/test_rest/test_items.py b/tests/test_openhab/test_rest/test_items.py index f3208c43..29c1a847 100644 --- a/tests/test_openhab/test_rest/test_items.py +++ b/tests/test_openhab/test_rest/test_items.py @@ -1,5 +1,3 @@ -from msgspec import convert - from HABApp.openhab.definitions.rest.items import CommandOptionResp, ItemResp, StateOptionResp @@ -28,14 +26,14 @@ def test_item_1() -> None: 'tags': ['Tag1'], 'groupNames': ['Group1', 'Group2'] } - item = convert(_in, type=ItemResp) + item = ItemResp.model_validate(_in) assert item.name == 'Item1Name' assert item.label == 'Item1Label' assert item.state == 'CLOSED' assert item.transformed_state == 'zu' - assert item.tags == ['Tag1'] - assert item.groups == ['Group1', 'Group2'] + assert item.tags == ('Tag1', ) + assert item.groups == ('Group1', 'Group2') def test_item_2() -> None: @@ -58,7 +56,8 @@ def test_item_2() -> None: 'label': 'Senderliste', 'category': None, 'tags': [], 'groupNames': []} - item = convert(_in, type=ItemResp) + + item = ItemResp.model_validate(_in) assert item.name == 'iSbPlayer_Favorit' assert item.label == 'Senderliste' @@ -68,10 +67,10 @@ def test_item_2() -> None: desc = item.state_description assert desc.pattern == '%s' assert desc.read_only is False - assert desc.options == [StateOptionResp('0', d1), StateOptionResp('1', d2)] + assert desc.options == (StateOptionResp(value='0', label=d1), StateOptionResp(value='1', label=d2)) desc = item.command_description - assert desc.command_options == [CommandOptionResp('0', d1), CommandOptionResp('1', d2)] + assert desc.command_options == (CommandOptionResp(command='0', label=d1), CommandOptionResp(command='1', label=d2)) def test_group_item() -> None: @@ -81,7 +80,7 @@ def test_group_item() -> None: 'link': 'http://ip:port/rest/items/christmasTree', 'state': '100', 'stateDescription': { - 'minimum': 0, 'maximum': 100, 'step': 1, 'pattern': '%d%%', 'readOnly': False, 'options': [] + 'minimum': 0, 'maximum': 100, 'step': 1, 'pattern': '%d%%', 'readOnly': False, 'options': () }, 'type': 'Dimmer', 'name': 'christmasTree', @@ -93,13 +92,13 @@ def test_group_item() -> None: { 'link': 'http://ip:port/rest/items/frontgardenPower', 'state': 'OFF', - 'stateDescription': {'pattern': '%s', 'readOnly': False, 'options': []}, + 'stateDescription': {'pattern': '%s', 'readOnly': False, 'options': ()}, 'type': 'Switch', 'name': 'frontgardenPower', 'label': 'Outside Power', 'category': 'poweroutlet', 'tags': [], - 'groupNames': ['Group1', 'Group2'], + 'groupNames': ('Group1', 'Group2'), } ], 'groupType': 'Switch', @@ -122,10 +121,11 @@ def test_group_item() -> None: 'ALL_TOPICS' ] } - item = convert(_in, type=ItemResp) + + item = ItemResp.model_validate(_in) assert item.name == 'SwitchGroup' assert isinstance(item.members[0], ItemResp) assert item.members[0].name == 'christmasTree' assert item.group_function.name == 'OR' - assert item.group_function.params == ['ON', 'OFF'] + assert item.group_function.params == ('ON', 'OFF') diff --git a/tests/test_openhab/test_rest/test_links.py b/tests/test_openhab/test_rest/test_links.py index 1a02d043..8bd63dff 100644 --- a/tests/test_openhab/test_rest/test_links.py +++ b/tests/test_openhab/test_rest/test_links.py @@ -1,5 +1,3 @@ -from msgspec import convert - from HABApp.openhab.definitions.rest import ItemChannelLinkResp @@ -10,7 +8,7 @@ def test_simple() -> None: 'itemName': 'ZWaveItem1', 'editable': False, } - o = convert(_in, type=ItemChannelLinkResp) + o = ItemChannelLinkResp.model_validate(_in) assert o.channel == 'zwave:device:controller:node15:sensor_luminance' assert o.item == 'ZWaveItem1' @@ -25,7 +23,7 @@ def test_configuration() -> None: 'itemName': 'ZWaveItem1', 'editable': False, } - o = convert(_in, type=ItemChannelLinkResp) + o = ItemChannelLinkResp.model_validate(_in) assert o.channel == 'zwave:device:controller:node15:sensor_luminance' assert o.item == 'ZWaveItem1' assert o.configuration == {'profile': 'follow', 'offset': 1} diff --git a/tests/test_openhab/test_rest/test_things.py b/tests/test_openhab/test_rest/test_things.py index 90dec511..157c7614 100644 --- a/tests/test_openhab/test_rest/test_things.py +++ b/tests/test_openhab/test_rest/test_things.py @@ -1,7 +1,3 @@ -from json import dumps - -from msgspec.json import decode - from HABApp.openhab.definitions.rest.things import ThingResp @@ -17,7 +13,7 @@ def test_thing_summary() -> None: 'thingTypeUID': 'astro:sun' } - thing = decode(dumps(_in), type=ThingResp) + thing = ThingResp.model_validate(_in) assert thing.editable is True assert thing.uid == 'astro:sun:d522ba4b56' @@ -93,14 +89,14 @@ def test_thing_full() -> None: 'thingTypeUID': 'astro:sun' } - thing = decode(dumps(_in), type=ThingResp) + thing = ThingResp.model_validate(_in) c0, c1, c2 = thing.channels - assert c0.linked_items == ['LinkedItem1', 'LinkedItem2'] + assert c0.linked_items == ('LinkedItem1', 'LinkedItem2') assert c0.configuration == {'offset': 0} - assert c1.linked_items == [] + assert c1.linked_items == () assert c1.configuration == {} assert thing.status.status == 'UNINITIALIZED'