diff --git a/group_vars/all/vault b/group_vars/all/vault index b01da06..b53c666 100644 --- a/group_vars/all/vault +++ b/group_vars/all/vault @@ -1,17 +1,24 @@ $ANSIBLE_VAULT;1.1;AES256 -30303535613532623633306661646164653037323038343838386437643463633937656664653634 -6439643934616538373936396465323466323833303633640a326630646662376237376532313761 -65643938613863333235353536323939353761373936303965316262366465633163626234653132 -6161363266613162360a656433306536663230316631333430373362643034393930353439626134 -39356135633861646539626661623062326531656539623030623637363634396639323935306230 -63383637393438326434626137666637373466363735306334303366646134623332353830633137 -66363531633636313739613264616164646366396634313934333566613936303865623162336463 -63346330366438393965663134326135633436346532383162316234623065623661613138353838 -34666530303838633161643532396535343432633064323938393933353562623366333862333731 -39313562623762313437333738396534646466333461393536316239636134313866393234363931 -63373934363961376634623966616135333835353066656236666139363965643934376162313439 -34653438633630353663363130653434636331376536643765653232323266313462373965343266 -34666639656264313933646264623931626230613636313030346637383361393934653964333565 -34343432613336373834646136306537303463643463653463353066663062323735653631643132 -32303639616533353735336437636634383430623534623935333364323631393536363661303163 -33633939613138336637 +65343063363130666335313366396139653130333535653437376464666230653230656662663738 +3239336161333434336264386436393738653637346561370a626437323632323866366139613339 +36343964666562636666663766613032333133303931356537353334353635333236396630323963 +6133666333633837370a376266346364396264626136363766383735383362343366373134616231 +33303732316632376563373330336534623934393166346233633666343136653735653363653538 +38613733393366303730323466383136346563386531376338333731643762326232373631653563 +64333965313730363133663663613563396664613463333936363833396333363131313164646463 +62393861633133366539656662643037616362633964626335373338383563663865306339616438 +31373063623635316232313262353563646331346438376538343635373966313235623038643763 +35633139363636663837323166393563616132663633633331363136326634363562376138356437 +33303166306562663061306437353566386563633030623835376633393865303238313866656262 +32653632643765343062363264623338336664656432373934656433663639313635383364646430 +37653037386664333437663737626535373463656564623262613638313333643336613663393835 +32613663393865643665393931323235653937626533366363326266663666393438623937643265 +66626330363238663866393662636561623934633232646536393831623735343162303339313238 +30376536343766333234396539386237333132356637623336313535356564356437303763383332 +31383437313963306231343166623532383064383938636433313365363333646636383631326330 +36653630653733383232663639303762653237306333393564323335333130356639393535613030 +32386338326264343333396233633138363633663234346535346138326661643931306439316261 +62373831373462326263316232613338626132353564383262643332623563626465363938623932 +62303436363863386464343135306362636232363833303237393562393037663436353136336538 +66646333653031306362393539363836333063353765313464366363353361616464333733303463 +376331376261323236333164343761663362 diff --git a/group_vars/all/vault.sample b/group_vars/all/vault.sample index 7c92c33..536919e 100644 --- a/group_vars/all/vault.sample +++ b/group_vars/all/vault.sample @@ -2,17 +2,16 @@ # uncomment the variables and add info -#vault_graphite_server: -#vault_openhab_config_repo: +vault_openhab_config_repo: "ssh://>@:/path/repo.git" -#vault_nginx_user: user -#vault_nginx_password: passwd +vault_nginx_user: +vault_nginx_password: -#vault_dynv6_name: "myhost.dynv6.net" -#vault_dynv6_device: "" -#vault_dynv6_token: 'mytoken' +vault_dynv6_device: "" +vault_dynv6_token: '' -#vault_letsencrypt_email: myname@domain.com -#vault_fqdn: "example.com" +vault_gardena_user: 'user_name' +vault_gardena_password: 'password' +vault_gardena_api_key: '' diff --git a/roles/openhab2/tasks/gardena.yml b/roles/openhab2/tasks/gardena.yml index 166e139..a85efb6 100644 --- a/roles/openhab2/tasks/gardena.yml +++ b/roles/openhab2/tasks/gardena.yml @@ -1,10 +1,34 @@ - name: install gardena service file template: - src: "gardena.service" + src: "gardena/gardena.service" dest: "/etc/systemd/system/gardena.service" mode: u=rw,g=rw,o=r +- name: install gardena service script + template: + src: "gardena/gardena_monitor_collector.py" + dest: "/etc/openhab2/automation/gardena/gardena_monitor_collector.py" + mode: u=rwx,g=rx,o=rx + group: root + owner: root + +- name: install gardena service config + template: + src: "gardena/gardena.yml" + dest: "/etc/openhab2/automation/gardena/gardena.yml" + mode: u=rw,g=r,o=r + group: openhab + owner: openhab + +- name: install gardena jsr223 script + template: + src: "gardena/gardena.py" + dest: "/etc/openhab2/automation/jsr223/gardena.py" + mode: u=rw,g=r,o=r + group: openhab + owner: openhab + - name: enable gardena service systemd: daemon_reload: yes diff --git a/roles/openhab2/templates/gardena/gardena.py b/roles/openhab2/templates/gardena/gardena.py new file mode 100644 index 0000000..0e188e5 --- /dev/null +++ b/roles/openhab2/templates/gardena/gardena.py @@ -0,0 +1,180 @@ +# Copyright (c) 2019 by Christian Schnidrig. + +# https://github.com/TooTallNate/Java-WebSocket + +# jython imports +from org.slf4j import LoggerFactory +import uuid +import math +import sys +import traceback +import time +import json +import jsonmerge + +# java imports +#from org.eclipse.smarthome.core.scheduler import CronExpression +import profile +from org.yaml.snakeyaml import Yaml +from java.nio.file.StandardWatchEventKinds import ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY +try: + from org.openhab.core.service import AbstractWatchService +except: + from org.eclipse.smarthome.core.service import AbstractWatchService + +####################################################### +####################################################### +####################################################### +# constants + +module_name = "gardena" +logger_name = "jython." + module_name +module_prefix = module_name + "_" + +# location of script +openhab_base_dir = '/etc/openhab2' +automationDir = openhab_base_dir + '/automation' +gardenaDir = automationDir + '/gardena' +gardena_config_file_name = 'gardena.yml' +gardena_config_file = gardenaDir + '/' + gardena_config_file_name +gardena_data_file_name = 'gardena.json' +gardena_data_file = gardenaDir + '/' + gardena_data_file_name + +####################################################### +# some globals +config = None +data = None + +# default logger +logger = LoggerFactory.getLogger(logger_name) + +####################################################### +####################################################### +####################################################### +# config +class Config(): + def __init__(self): + self.logger = LoggerFactory.getLogger(logger_name + ".Config") + self.gardenaConfig = Yaml().load(open(gardena_config_file)) + self.logger.info("Config loaded") + + def getDeviceMapping(self): + return self.gardenaConfig['device_mapping'] + + def getItemNamePrefix(self): + return self.gardenaConfig['item_name_prefix'] + + def getValueMapping(self): + return self.gardenaConfig['value_mapping'] + +####################################################### +####################################################### +####################################################### +# gardena monitor + +def gardena_monitor(): + logger = LoggerFactory.getLogger(logger_name + ".gardena_monitor") + config = Config() + + device_mapping = config.getDeviceMapping() + + data = {} + + with open (gardena_data_file, "r") as data_file: + lines=data_file.readlines() + for line in lines: + json_line = json.loads(line) + if 'attributes' in json_line.keys(): + data = jsonmerge.merge(data, {json_line['type']: { json_line['id']: json_line['attributes'] }}) + + logger.debug(json.dumps(data, indent=4)) + value_mapping = config.getValueMapping() + prefix = config.getItemNamePrefix() + for type in value_mapping: + for value_set in data[type]: + valve_number = None + id = value_set + if type == "VALVE": + id, valve_number = id.split(':') + if id in device_mapping: + device_name = device_mapping[id] + if type == "VALVE": + device_name = device_name + "_" + str(valve_number) + logger.debug("Found device: " + device_name + " of type: " + type) + for value_name in value_mapping[type]: + if not value_name.endswith('_map'): + if value_name in data[type][value_set]: + item_suffix = value_mapping[type][value_name] + item_name = prefix + "_" + device_name + "_" + item_suffix + item = ir.get(item_name) + if item == None: + logger.info("Item not found: " + item_name) + else: + value = str(data[type][value_set][value_name]['value']) + if value_name + '_map' in value_mapping[type]: + value = str(value_mapping[type][value_name + '_map'][value]) + logger.info("Set item " + item_name + " = " + value) + events.postUpdate(item_name, value) + +####################################################### +####################################################### +####################################################### +# fileWatcher + +class FileWatcher(AbstractWatchService): + def __init__(self, path, event_kinds=[ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY], watch_subdirectories=False): + AbstractWatchService.__init__(self, path) + self.logger = LoggerFactory.getLogger(logger_name + ".FileWatcher") + self.event_kinds = event_kinds + self.watch_subdirectories = watch_subdirectories + self.logger.debug("new fileWatcher for " + str(path) + " created.") + + def getWatchEventKinds(self, path): + return self.event_kinds + + def watchSubDirectories(self): + return self.watch_subdirectories + + def processWatchEvent(self, event, kind, path): + try: + self.logger.debug(event.toString()) + self.logger.debug(kind.toString()) + self.logger.debug(path.toString()) + if str(path.toString()) == gardena_config_file or str(path.toString()) == gardena_data_file: + logger.info("File " + str(path.toString()) + " changed. Reloading.") + try: + gardena_monitor() + except: + logger.error("gardena_monitor failed.") + logger.error(traceback.format_exc()) + except: + self.logger.error("processWatchEvent callback failed.") + self.logger.error(traceback.format_exc()) + self.deactivate() + self.activate() + +####################################################### +####################################################### +####################################################### +# __main__ + +fileWatcherGardena = None + +####################################################### +# script load/unload hooks +def scriptLoaded(id): + try: + logger.info("scriptLoaded()") + fileWatcherGardena = FileWatcher(gardenaDir) + fileWatcherGardena.activate() + gardena_monitor() + except: + logger.error(traceback.format_exc()) + if fileWatcherGardena is not None: + fileWatcherGardena.deactivate() + +def scriptUnloaded(): + logger.info("scriptUnloaded()") + if fileWatcherGardena is not None: + fileWatcherGardena.deactivate() + diff --git a/roles/openhab2/templates/gardena.service b/roles/openhab2/templates/gardena/gardena.service similarity index 59% rename from roles/openhab2/templates/gardena.service rename to roles/openhab2/templates/gardena/gardena.service index 8f57993..cecc498 100644 --- a/roles/openhab2/templates/gardena.service +++ b/roles/openhab2/templates/gardena/gardena.service @@ -2,7 +2,7 @@ Description=Service monitoring gardena web service [Service] -ExecStart=/etc/openhab2/automation/gardena_monitor_collector.py +ExecStart=/etc/openhab2/automation/gardena/gardena_monitor_collector.py [Install] WantedBy=multi-user.target diff --git a/roles/openhab2/templates/gardena/gardena.yml b/roles/openhab2/templates/gardena/gardena.yml new file mode 100644 index 0000000..513a300 --- /dev/null +++ b/roles/openhab2/templates/gardena/gardena.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2019 by Christian Schnidrig. + +######################## +device_mapping: + 164f4132-08e0-4d5f-b7f7-85048dd88281: sensor1 + ab9633cd-9a2a-4937-ac38-4f58717493b7: ic24 + +item_name_prefix: "gardena" + +value_mapping: + SENSOR: + soilHumidity: soil_humidity + soilTemperature: soil_temperature + lightIntensity: light_intensity + ambientTemperature: ambient_temperature + VALVE: + activity: state + activity_map: + CLOSED: "CLOSED" + MANUAL_WATERING: "OPEN" + SCHEDULED_WATERING: "OPEN" + name: name + COMMON: + batteryLevel: battery_level + rfLinkLevel: link_level + diff --git a/roles/openhab2/templates/gardena/gardena_monitor_collector.py b/roles/openhab2/templates/gardena/gardena_monitor_collector.py new file mode 100755 index 0000000..d25ec22 --- /dev/null +++ b/roles/openhab2/templates/gardena/gardena_monitor_collector.py @@ -0,0 +1,135 @@ +#!/usr/bin/python3 + +import websocket +from threading import Thread +import time +import sys +import requests +import logging +import datetime + +logging.basicConfig(level=logging.DEBUG) + +############################## +# account specific values +USERNAME = '{{ vault_gardena_user }}' +PASSWORD = '{{ vault_gardena_password }}' +API_KEY = '{{ vault_gardena_api_key }}' + +############################## +# other constants +AUTHENTICATION_HOST = 'https://api.authentication.husqvarnagroup.dev' +SMART_HOST = 'https://api.smart.gardena.dev' + +dataFileName = "/etc/openhab2/automation/gardena/gardena.json" +logFileName = "/etc/openhab2/automation/gardena/gardena.json.log" + +############################## +module_name = "monitor" +logger_name = "gardena." + module_name +# default logger +logger = logging.getLogger(logger_name) + +############################## +class Client: + + def __init__(self, dataFile, logFile): + self.dataFileName = dataFileName + self.logFile = logFile + self.logger = logging.getLogger(logger_name + '.Client') + self.dataFile = None + + def on_message(self, message): + if self.dataFile != None: + self.dataFile.write(message) + self.dataFile.write('\n') + self.dataFile.flush() + logFile.write(message) + logFile.write('\n') + logFile.flush() + + def on_error(self, error): + self.logger.error(error) + + def on_close(self): + self.live = False + self.logger.info("### closed ###") + self.dataFile.close() + + def on_open(self): + self.logger.info("### connected ###") + self.dataFile = open(dataFileName, "w") + + self.live = True + + def run(*args): + while self.live: + time.sleep(1) + + Thread(target=run).start() + + + +############################## +if __name__ == "__main__": + + while True: + + try: + start = time.time() + logger.info(datetime.datetime.now()) + logFile = open(logFileName, "a") + + payload = {'grant_type': 'password', 'username': USERNAME, 'password': PASSWORD, + 'client_id': API_KEY} + + logger.debug("Logging into gardena system...") + r = requests.post('{}/v1/oauth2/token'.format(AUTHENTICATION_HOST), data=payload) + assert r.status_code == 200, r + auth_token = r.json()["access_token"] + logger.debug("Got token: {}".format(auth_token)) + + headers = { + "Content-Type": "application/vnd.api+json", + "x-api-key": API_KEY, + "Authorization-Provider": "husqvarna", + "Authorization": "Bearer " + auth_token + } + + r = requests.get('{}/v1/locations'.format(SMART_HOST), headers=headers) + assert r.status_code == 200, r + assert len(r.json()["data"]) > 0, 'location missing - user has not setup system' + location_id = r.json()["data"][0]["id"] + + payload = { + "data": { + "type": "WEBSOCKET", + "attributes": { + "locationId": location_id + }, + "id": "does-not-matter" + } + } + logger.debug("Logged in (%s), getting WebSocket ID..." % auth_token) + r = requests.post('{}/v1/websocket'.format(SMART_HOST), json=payload, headers=headers) + + assert r.status_code == 201, r + logger.info("WebSocket ID obtained, connecting...") + response = r.json() + websocket_url = response["data"]["attributes"]["url"] + + # websocket.enableTrace(True) + client = Client(dataFileName, logFile) + ws = websocket.WebSocketApp( + websocket_url, + on_message=client.on_message, + on_error=client.on_error, + on_close=client.on_close) + ws.on_open = client.on_open + ws.run_forever(ping_interval=150, ping_timeout=1) + + except: + delay = 15 * 60 - (time.time() - start) + if (delay > 0): + logger.info("Sleeping for: {} seconds before retrying.".format(delay)) + time.sleep(delay)