Skip to content

Commit

Permalink
0.14.0 (#156)
Browse files Browse the repository at this point in the history
0.14.0
- completely reworked openhab connection
- updated docs
- added plugin that shows an overview for Things and Z-Wave
  • Loading branch information
spacemanspiff2007 authored Jul 1, 2020
1 parent f639e3d commit e984df4
Show file tree
Hide file tree
Showing 53 changed files with 1,877 additions and 1,386 deletions.
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ exclude =
conf,
__init__.py,
tests/context.py

# the interfaces will throw unused imports
HABApp/openhab/interface.py
HABApp/openhab/interface_async.py
2 changes: 1 addition & 1 deletion HABApp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.13.2'
__version__ = '0.14.0'
17 changes: 9 additions & 8 deletions HABApp/core/const/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import asyncio


# otherwise creating a subprocess does not work on windows
if sys.platform == "win32":
# This is the default from 3.8 so we don't have to set it ourselves
if sys.version_info < (3, 8):
# setup everything so we can create a subprocess, only required for older versions
if sys.version_info < (3, 8):
if sys.platform == "win32":
# This is the default from 3.8 so we don't have to set it ourselves
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
else:
# Must be called once in the main thread so creating a subprocess works properly
# https://docs.python.org/3/library/asyncio-subprocess.html#subprocess-and-threads
asyncio.get_child_watcher()
else:
# Must be called once in the main thread so creating a subprocess works properly
# https://docs.python.org/3/library/asyncio-subprocess.html#subprocess-and-threads
asyncio.get_child_watcher()


loop = asyncio.get_event_loop()
loop.set_debug(True)
Expand Down
9 changes: 5 additions & 4 deletions HABApp/openhab/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# no external dependencies
import HABApp.openhab.exceptions
import HABApp.openhab.events

from .oh_interface import OpenhabInterface, get_openhab_interface
from .oh_connection import OpenhabConnection
from .http_connection import HttpConnection
import HABApp.openhab.interface_async
import HABApp.openhab.interface

import HABApp.openhab.events
# items use the interface for the convenience functions
import HABApp.openhab.items
from HABApp.openhab.map_items import map_items
1 change: 1 addition & 0 deletions HABApp/openhab/connection_handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import http_connection, func_async, func_sync
229 changes: 229 additions & 0 deletions HABApp/openhab/connection_handler/func_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import datetime
import traceback
import typing
from typing import Any, Optional, Dict, List
from urllib.parse import quote as quote_url

from HABApp.core.const.json import load_json
from HABApp.core.items import BaseValueItem
from HABApp.openhab.exceptions import OpenhabDisconnectedError, OpenhabNotReadyYet, ThingNotEditableError, \
ThingNotFoundError, ItemNotEditableError, ItemNotFoundError
from HABApp.openhab.definitions.rest import ItemChannelLinkDefinition, LinkNotFoundError, OpenhabThingDefinition
from .http_connection import delete, get, post, put, log


def convert_to_oh_type(_in: Any) -> str:
if isinstance(_in, datetime.datetime):
# Add timezone (if not yet defined) to string, then remote anything below ms.
# 2018-11-19T09:47:38.284000+0100 -> 2018-11-19T09:47:38.284+0100
out = _in.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S.%f%z')
return f'{out[:-8]}{out[-5:]}'
# elif isinstance(_in, HABApp.openhab.items.ColorItem):
# return f'{_in.hue:.1f},{_in.saturation:.1f},{_in.value:.1f}'
elif isinstance(_in, BaseValueItem):
return str(_in.value)
elif isinstance(_in, (set, list, tuple, frozenset)):
return ','.join(str(k) for k in _in)
elif _in is None:
return 'NULL'

return str(_in)


async def async_post_update(item, state: Any):
if not isinstance(state, str):
state = convert_to_oh_type(state)
await put(f'items/{item:s}/state', data=state)


async def async_send_command(item, state: Any):
if not isinstance(state, str):
state = convert_to_oh_type(state)
await post(f'items/{item:s}', data=state)


async def async_item_exists(item) -> bool:
ret = await get(f'items/{item:s}', log_404=False)
return ret.status == 200


async def async_get_items(include_habapp_meta=False) -> Optional[List[Dict[str, Any]]]:
params = {'recursive': 'false', 'fields': 'state,type,name,editable'}
if include_habapp_meta:
params['fields'] += ',metadata'
params['metadata'] = 'HABApp'

try:
resp = await get('items')
return load_json(await resp.text(encoding='utf-8'))
except Exception as e:
# sometimes uuid already works but items not - so we ignore these errors here, too
if not isinstance(e, (OpenhabDisconnectedError, OpenhabNotReadyYet)):
for line in traceback.format_exc().splitlines():
log.error(line)
return None


async def async_get_item(item: str, include_habapp_meta=False) -> dict:
params = None if not include_habapp_meta else {'metadata': 'HABApp'}
ret = await get(f'items/{item:s}', params=params, log_404=False)
if ret.status == 404:
raise ItemNotFoundError.from_name(item)
if ret.status >= 300:
return {}
else:
return await ret.json(encoding='utf-8')


async def async_get_things() -> Optional[List[Dict[str, Any]]]:

try:
resp = await get('things')
return load_json(await resp.text(encoding='utf-8'))
except Exception as e:
# sometimes uuid and items already works but things not - so we ignore these errors here, too
if not isinstance(e, (OpenhabDisconnectedError, OpenhabNotReadyYet)):
for line in traceback.format_exc().splitlines():
log.error(line)
return None


async def async_get_thing(uid: str) -> OpenhabThingDefinition:
ret = await get(f'things/{uid:s}')
if ret.status >= 300:
raise ThingNotFoundError.from_uid(uid)

return OpenhabThingDefinition.parse_obj(await ret.json(encoding='utf-8'))


async def async_get_persistence_data(item_name: str, persistence: typing.Optional[str],
start_time: typing.Optional[datetime.datetime],
end_time: typing.Optional[datetime.datetime]) -> dict:

params = {}
if persistence:
params['serviceId'] = persistence
if start_time is not None:
params['starttime'] = start_time.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S.%f%z')
if end_time is not None:
params['endtime'] = end_time.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S.%f%z')
if not params:
params = None

ret = await get(f'persistence/items/{item_name:s}', params=params)
if ret.status >= 300:
return {}
else:
return await ret.json(encoding='utf-8')


async def async_create_item(item_type, name, label="", category="", tags=[], groups=[],
group_type=None, group_function=None, group_function_params=[]) -> bool:

payload = {'type': item_type, 'name': name}
if label:
payload['label'] = label
if category:
payload['category'] = category
if tags:
payload['tags'] = tags
if groups:
payload['groupNames'] = groups # CamelCase!

# we create a group
if group_type:
payload['groupType'] = group_type # CamelCase!
if group_function:
payload['function'] = {}
payload['function']['name'] = group_function
if group_function_params:
payload['function']['params'] = group_function_params

ret = await put(f'items/{name:s}', json=payload)
if ret is None:
return False

if ret.status == 404:
raise ItemNotFoundError.from_name(name)
elif ret.status == 405:
raise ItemNotEditableError.from_name(name)
return ret.status < 300


async def async_remove_item(item):
await delete(f'items/{item:s}')


async def async_remove_metadata(item: str, namespace: str):
ret = await delete(f'items/{item:s}/metadata/{namespace:s}')
if ret is None:
return False
return ret.status < 300


async def async_set_metadata(item: str, namespace: str, value: str, config: dict):
payload = {
'value': value,
'config': config
}
ret = await put(f'items/{item:s}/metadata/{namespace:s}', json=payload)
if ret is None:
return False
return ret.status < 300


async def async_set_thing_cfg(uid: str, cfg: typing.Dict[str, typing.Any]):
ret = await put(f'things/{uid:s}/config', json=cfg)
if ret is None:
return None

if ret.status == 404:
raise ThingNotFoundError.from_uid(uid)
elif ret.status == 409:
raise ThingNotEditableError.from_uid(uid)
elif ret.status >= 300:
raise ValueError('Something went wrong')


# ---------------------------------------------------------------------------------------------------------------------
# Link handling is experimental
# ---------------------------------------------------------------------------------------------------------------------

def __get_link_url(channel_uid: str, item_name: str) -> str:
# rest/links/ endpoint needs the channel to be url encoded
# (AAAA:BBBB:CCCC:0#NAME -> AAAA%3ABBBB%3ACCCC%3A0%23NAME)
# otherwise the REST-api returns HTTP-Status 500 InternalServerError
return 'links/' + quote_url(f"{item_name}/{channel_uid}")


async def async_remove_channel_link(channel_uid: str, item_name: str) -> bool:
ret = await delete(__get_link_url(channel_uid, item_name))
if ret is None:
return False
return ret.status == 200


async def async_get_channel_link(channel_uid: str, item_name: str) -> ItemChannelLinkDefinition:
ret = await get(__get_link_url(channel_uid, item_name), log_404=False)
if ret.status == 404:
raise LinkNotFoundError(f'Link {item_name} -> {channel_uid} not found!')
if ret.status >= 300:
return None
else:
return ItemChannelLinkDefinition(**await ret.json(encoding='utf-8'))


async def async_channel_link_exists(channel_uid: str, item_name: str) -> bool:
ret = await get(__get_link_url(channel_uid, item_name), log_404=False)
return ret.status == 200


async def async_create_channel_link(link_def: ItemChannelLinkDefinition) -> bool:
# if the passed item doesn't exist OpenHAB creates a new empty item item
if not await async_item_exists(link_def.item_name):
raise ItemNotFoundError.from_name(link_def.item_name)

ret = await put(__get_link_url(link_def.channel_uid, link_def.item_name), json=link_def.dict(by_alias=True))
if ret is None:
return False
return ret.status == 200
Loading

0 comments on commit e984df4

Please sign in to comment.