Skip to content

Commit

Permalink
0.12.8
Browse files Browse the repository at this point in the history
* Added possibility to manually specify thing configuration (#144)
  • Loading branch information
spacemanspiff2007 authored Apr 13, 2020
1 parent 67b25d6 commit 7b4c5fb
Show file tree
Hide file tree
Showing 13 changed files with 432 additions and 97 deletions.
2 changes: 1 addition & 1 deletion HABApp/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__VERSION__ = '0.12.7'
__VERSION__ = '0.12.8'
16 changes: 10 additions & 6 deletions HABApp/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
from pathlib import Path

from EasyCo import ConfigFile, PathContainer
from EasyCo import ConfigFile, PathContainer, ConfigEntry

from ._conf_location import Location
from ._conf_mqtt import Mqtt
Expand All @@ -12,10 +12,11 @@


class Directories(PathContainer):
logging: Path = Path('log')
rules: Path = Path('rules')
lib: Path = Path('lib')
param: Path = Path('param')
logging: Path = ConfigEntry(Path('log'), description='Folder where the logs will be written to')
rules: Path = ConfigEntry(Path('rules'), description='Folder from which the rule files will be loaded')
param: Path = ConfigEntry(Path('param'), description='Folder from which the parameter files will be loaded')
config: Path = ConfigEntry(Path('config'), description='Folder from which configuration files will be loaded')
lib: Path = ConfigEntry(Path('lib'), description='Folder where additional libraries can be placed')

def on_all_values_set(self):
try:
Expand All @@ -24,13 +25,16 @@ def on_all_values_set(self):
self.rules.mkdir()
if not self.logging.is_dir():
self.logging.mkdir()
if not self.config.is_dir():
log.info(f'Manual thing configuration disabled! Folder {self.config} does not exist!')


# add path for libraries
if self.lib.is_dir():
lib_path = str(self.lib)
if lib_path not in sys.path:
sys.path.insert(0, lib_path)
log.debug(f'Added library folder "{lib_path}" to path')
log.debug(f'Added library folder "{lib_path}" to system path')
except Exception as e:
log.error(e)
print(e)
Expand Down
2 changes: 1 addition & 1 deletion HABApp/config/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def load_log(self):
p = Path(handler_cfg['filename'])
if not p.is_absolute():
# Our log folder ist not yet converted to path -> it is not loaded
if not CONFIG.directories.logging.is_absolute():
if not isinstance(CONFIG.directories.logging, Path):
raise AbsolutePathExpected()

# Use defined parent folder
Expand Down
2 changes: 1 addition & 1 deletion HABApp/core/Items.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_item(name: str) -> __BaseItem:
try:
return _ALL_ITEMS[name]
except KeyError:
raise ItemNotFoundException(f'Item {name} does not exist!')
raise ItemNotFoundException(f'Item {name} does not exist!') from None


def get_all_items() -> typing.List[__BaseItem]:
Expand Down
104 changes: 104 additions & 0 deletions HABApp/openhab/definitions/rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import dataclasses
import typing


@dataclasses.dataclass
class OpenhabItemDefinition:
"""
:ivar str type: item type
:ivar str name: item name
:ivar typing.Any state: item state
:ivar str state: item label
:ivar str category: item category
:ivar bool editable: item can changed through Rest API
:ivar typing.List[str] tags: item tags
:ivar typing.List[str] groups: groups the item is in
:ivar typing.List[OpenhabItemDefinition] members: If the item is a group this contains the members
"""
type: str
name: str
state: typing.Any
label: str = ''
category: str = ''
editable: bool = True
tags: typing.List[str] = dataclasses.field(default_factory=list)
groups: typing.List[str] = dataclasses.field(default_factory=list)
members: 'typing.List[OpenhabItemDefinition]' = dataclasses.field(default_factory=list)

@classmethod
def from_dict(cls, data) -> 'OpenhabItemDefinition':
assert isinstance(data, dict), type(dict)
data['groups'] = data.pop('groupNames', [])

# remove link
data.pop('link', None)

# map states, quick n dirty
state = data['state']
if state == 'NULL':
state = None
else:
try:
state = int(state)
except ValueError:
try:
state = float(state)
except ValueError:
pass
data['state'] = state

for i, item in enumerate(data.get('members', [])):
data['members'][i] = cls.from_dict(item)

# Important, sometimes OpenHAB returns more than in the schema spec, so we remove those items otherwise we
# get e.g.: TypeError: __init__() got an unexpected keyword argument 'stateDescription'
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})


@dataclasses.dataclass
class OpenhabThingChannelDefinition:
uid: typing.Optional[str] = None
id: typing.Optional[str] = None
channelTypeUID: typing.Optional[str] = None
itemType: typing.Optional[str] = None
kind: typing.Optional[str] = None
label: typing.Optional[str] = None
description: typing.Optional[str] = None
defaultTags: typing.Optional[typing.List[str]] = None
properties: typing.Optional[typing.Dict[str, typing.Any]] = None
configuration: typing.Optional[typing.Dict[str, typing.Any]] = None



@dataclasses.dataclass
class OpenhabThingDefinition:
label: typing.Optional[str] = None
bridgeUID: typing.Optional[str] = None
configuration: typing.Dict[str, typing.Any] = None
properties: typing.Dict[str, typing.Any] = None
UID: typing.Optional[str] = None
thingTypeUID: typing.Optional[str] = None
channels: typing.Optional[typing.List[OpenhabThingChannelDefinition]] = None
location: typing.Optional[str] = None
statusInfo: typing.Optional[typing.Dict[str, str]] = None
firmwareStatus: typing.Optional[typing.Dict[str, str]] = None
editable: typing.Optional[bool] = None

@classmethod
def from_dict(cls, data) -> 'OpenhabThingDefinition':
assert isinstance(data, dict), type(dict)

# convert channel objs to dataclasses
channels = data.get('channels')
if channels is not None:
data['channels'] = [OpenhabThingChannelDefinition(**kw) for kw in channels]

return cls(**data)


class ThingNotFoundError(Exception):
pass


class ThingNotEditableError(Exception):
pass
23 changes: 23 additions & 0 deletions HABApp/openhab/http_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import HABApp.openhab.events
from ..config import Openhab as OpenhabConfig
from ..core.const.json import dump_json, load_json
from .definitions.rest import OpenhabThingDefinition, ThingNotFoundError, ThingNotEditableError

log = logging.getLogger('HABApp.openhab.connection')
log_events = logging.getLogger('HABApp.EventBus.openhab')
Expand Down Expand Up @@ -399,3 +400,25 @@ async def get_persistence_data(self,
return {}
else:
return await ret.json(encoding='utf-8')

async def async_get_thing(self, uid: str) -> OpenhabThingDefinition:
fut = self.__session.get(
self.__get_openhab_url('rest/things/{:s}', uid),
)
ret = await self._check_http_response(fut)
if ret.status >= 300:
raise ThingNotFoundError(f'Thing {uid} not found!')

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

async def async_set_thing_cfg(self, uid: str, cfg: typing.Dict[str, typing.Any]):
fut = self.__session.put(
self.__get_openhab_url('rest/things/{:s}/config', uid), json=cfg
)
ret = await self._check_http_response(fut)
if ret.status == 404:
raise ThingNotFoundError(f'Thing "{uid}" not found!')
elif ret.status == 409:
raise ThingNotEditableError(f'Thing "{uid}" is not editable!')
elif ret.status >= 300:
raise ValueError('Something went wrong')
135 changes: 134 additions & 1 deletion HABApp/openhab/oh_connection.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import asyncio
import logging
import re
import time
import typing
from pathlib import Path

from bidict import bidict

import HABApp
import HABApp.core
import HABApp.openhab.events
from HABApp.core.wrapper import ignore_exception, log_exception
from HABApp.openhab.map_events import get_event
from HABApp.core.wrapper import log_exception, ignore_exception
from .definitions.rest import ThingNotFoundError
from .http_connection import HttpConnection, HttpConnectionEventHandler
from .oh_interface import get_openhab_interface
from ..config import Openhab as OpenhabConfig
Expand Down Expand Up @@ -59,6 +65,14 @@ def on_connected(self):
# start ping
self.__async_ping = asyncio.ensure_future(self.async_ping())

# todo: move this somewhere proper
async def wait_and_update():
await asyncio.sleep(2)
for f in HABApp.CONFIG.directories.config.iterdir():
if f.name.endswith('.yml'):
await self.update_thing_config(f)
asyncio.ensure_future(wait_and_update())

@log_exception
def on_disconnected(self):

Expand Down Expand Up @@ -206,3 +220,122 @@ async def update_all_items(self):
log.info(f'Updated {len(data):d} Things')

return None

@HABApp.core.wrapper.log_exception
async def update_thing_config(self, path: Path):
if path.name.lower() != 'thingconfig.yml':
return None

log = logging.getLogger('HABApp.openhab.Config')
if not path.is_file():
log.debug(f'File {path} does not exist -> skipping Thing configuration!')
return None

yml = HABApp.parameters.parameter_files._yml_setup
with path.open(mode='r', encoding='utf-8') as file:
manu_cfg = yml.load(file)

if not manu_cfg or not isinstance(manu_cfg, dict):
log.warning(f'File {path} is empty!')
return None

for uid, target_cfg in manu_cfg.items():
try:
thing = await self.connection.async_get_thing(uid)
except ThingNotFoundError:
log.error(f'Thing with uid "{uid}" does not exist!')
continue

if thing.configuration is None:
log.error(f'Thing can not be configured!')
continue

cfg = ThingConfigChanger.from_dict(thing.configuration)

log.info(f'Checking {uid}: {thing.label}')
keys_ok = True
for k in target_cfg:
if k in cfg:
continue
keys_ok = False
log.error(f' - Config value "{k}" does not exist!')

if not keys_ok:
# show list with available entries
log.error(f' Available:')
for k, v in sorted(filter(lambda x: isinstance(x[0], str), cfg.items())):
if k.startswith('action_') or k in ('node_id', 'wakeup_node'):
continue
log.error(f' - {k}: {v}')
for k, v in sorted(filter(lambda x: isinstance(x[0], int), cfg.items())):
log.error(f' - {k:3d}: {v}')

# don't process entries with invalid keys
continue

# Check the current value
for k, target_val in target_cfg.items():
current_val = cfg[k]
if current_val == target_val:
log.info(f' - {k} is already {target_val}')
continue

log.info(f' - Set {k} to {target_val}')
cfg[k] = target_val

if not cfg.new or self.connection.is_read_only:
continue

try:
await self.connection.async_set_thing_cfg(uid, cfg=cfg.new)
except Exception as e:
log.error(f'Could not set new config: {e}!')
continue
log.info('Config successfully updated!')


class ThingConfigChanger:
zw_param = re.compile(r'config_(?P<p>\d+)_(?P<w>\d+)')
zw_group = re.compile(r'group_(\d+)')

@classmethod
def from_dict(cls, _in: dict) -> 'ThingConfigChanger':
c = cls()
c.org = _in
for k in _in:
# Z-Wave Params -> 0
m = ThingConfigChanger.zw_param.fullmatch(k)
if m:
c.alias[int(m.group(1))] = k
continue

# Z-Wave Groups to Group1
m = ThingConfigChanger.zw_group.fullmatch(k)
if m:
c.alias[f'Group{m.group(1)}'] = k
continue
return c

def __init__(self):
self.alias = bidict()
self.org: typing.Dict[str, typing.Any] = {}
self.new: typing.Dict[str, typing.Any] = {}

def __getitem__(self, key):
return self.org[self.alias.get(key, key)]

def __setitem__(self, key, item):
self.new[self.alias.get(key, key)] = item

def __contains__(self, key):
return self.alias.get(key, key) in self.org

def keys(self):
return (self.alias.inverse.get(k, k) for k in self.org.keys())

def values(self):
return self.org.values()

def items(self):
for k, v in zip(self.keys(), self.values()):
yield k, v
Loading

0 comments on commit 7b4c5fb

Please sign in to comment.