diff --git a/.travis.yml b/.travis.yml index e3a4d4e..d2d31a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ branches: python: - "3.5" - "3.6" # current default Python on Travis CI - - "3.7" - "3.8" # command to install dependencies install: diff --git a/config/default_config b/config/default_config index 793b779..f38c722 100644 --- a/config/default_config +++ b/config/default_config @@ -29,6 +29,8 @@ ssl_cert: # /path/to/fullchain.pem # Login on Google Home app and configuration interface auth_user: 'admin' auth_pass: 'admin' +# If you change authToken you need to disconnect and reconnect to Google Assistant +authToken: 'ZsokmCwKjdhk7qHLeYd2' # Google Assistant Settings: ClientID: 'clientid_from aog' diff --git a/const.py b/const.py index c17be4f..5afd6df 100644 --- a/const.py +++ b/const.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Constants for Google Assistant.""" -VERSION = '1.10.9' +VERSION = '1.22.25' PUBLIC_URL = 'https://[your public url]' CONFIGFILE = 'config/config.yaml' LOGFILE = 'dzga.log' @@ -22,6 +22,7 @@ DOMOTICZ_GET_SETTINGS_URL = '/json.htm?type=settings' DOMOTICZ_GET_CAMERAS_URL = '/json.htm?type=cameras' DOMOTICZ_GET_VERSION = '/json.htm?type=command¶m=getversion' +DOMOTICZ_SEND_COMMAND = 'json.htm?type=command¶m=' # https://developers.google.com/actions/smarthome/guides/ PREFIX_TYPES = 'action.devices.types.' @@ -34,6 +35,7 @@ TYPE_CURTAIN = PREFIX_TYPES + 'CURTAIN' TYPE_DISHWASHER = PREFIX_TYPES + 'DISHWASHER' TYPE_DOOR = PREFIX_TYPES + 'DOOR' +TYPE_DOORBELL = PREFIX_TYPES + 'DOORBELL' TYPE_DRYER = PREFIX_TYPES + 'DRYER' TYPE_FAN = PREFIX_TYPES + 'FAN' TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' @@ -75,16 +77,16 @@ ERR_VALUE_OUT_OF_RANGE = "valueOutOfRange" ERR_WRONG_PIN = 'pinIncorrect' -domains = { +DOMAINS = { 'ac_unit': 'AcUnit', 'bathtub': 'Bathtub', 'blinds': 'Blind', - 'blindsinv': 'BlindInverted', 'camera': 'Camera', 'coffeemaker': 'Coffeemaker', 'color': 'ColorSwitch', 'cooktop': 'Cooktop', 'door': 'DoorSensor', + 'doorbell': 'Doorbell', 'dishwasher': 'Dishwasher', 'dryer': 'Dryer', 'fan': 'Fan', @@ -129,47 +131,48 @@ ATTRS_PERCENTAGE = 1 ATTRS_FANSPEED = 1 ATTRS_VACUUM_MODES = 1 +ATTRS_HUMIDITY = 1 DOMOTICZ_TO_GOOGLE_TYPES = { - domains['ac_unit']: TYPE_AC_UNIT, - domains['bathtub']: TYPE_BATHTUB, - domains['blinds']: TYPE_BLINDS, - domains['blindsinv']: TYPE_BLINDS, - domains['camera']: TYPE_CAMERA, - domains['coffeemaker']: TYPE_COFFEE, - domains['color']: TYPE_LIGHT, - domains['cooktop']: TYPE_COOKTOP, - domains['dishwasher']: TYPE_DISHWASHER, - domains['door']: TYPE_DOOR, - domains['dryer']: TYPE_DRYER, - domains['fan']: TYPE_FAN, - domains['garage']: TYPE_GARAGE, - domains['gate']: TYPE_GATE, - domains['group']: TYPE_SWITCH, - domains['heater']: TYPE_HEATER, - domains['kettle']: TYPE_KETTLE, - domains['light']: TYPE_LIGHT, - domains['lock']: TYPE_LOCK, - domains['lockinv']: TYPE_LOCK, - domains['media']: TYPE_MEDIA, - domains['microwave']: TYPE_MICRO, - domains['mower']: TYPE_MOWER, - domains['outlet']: TYPE_OUTLET, - domains['oven']: TYPE_OVEN, - domains['push']: TYPE_SWITCH, - domains['scene']: TYPE_SCENE, - domains['screen']: TYPE_SCREEN, - domains['security']: TYPE_SECURITY, - domains['selector']: TYPE_SWITCH, - domains['sensor']: TYPE_SENSOR, - domains['smokedetector']: TYPE_SMOKE_DETECTOR, - domains['speaker']: TYPE_SPEAKER, - domains['switch']: TYPE_SWITCH, - domains['temperature']: TYPE_THERMOSTAT, - domains['thermostat']: TYPE_THERMOSTAT, - domains['vacuum']: TYPE_VACUUM, - domains['valve']: TYPE_VALVE, - domains['washer']: TYPE_WASHER, - domains['waterheater']: TYPE_WATERHEATER, - domains['window']: TYPE_WINDOW, + DOMAINS['ac_unit']: TYPE_AC_UNIT, + DOMAINS['bathtub']: TYPE_BATHTUB, + DOMAINS['blinds']: TYPE_BLINDS, + DOMAINS['camera']: TYPE_CAMERA, + DOMAINS['coffeemaker']: TYPE_COFFEE, + DOMAINS['color']: TYPE_LIGHT, + DOMAINS['cooktop']: TYPE_COOKTOP, + DOMAINS['dishwasher']: TYPE_DISHWASHER, + DOMAINS['door']: TYPE_DOOR, + DOMAINS['doorbell']: TYPE_DOORBELL, + DOMAINS['dryer']: TYPE_DRYER, + DOMAINS['fan']: TYPE_FAN, + DOMAINS['garage']: TYPE_GARAGE, + DOMAINS['gate']: TYPE_GATE, + DOMAINS['group']: TYPE_SWITCH, + DOMAINS['heater']: TYPE_HEATER, + DOMAINS['kettle']: TYPE_KETTLE, + DOMAINS['light']: TYPE_LIGHT, + DOMAINS['lock']: TYPE_LOCK, + DOMAINS['lockinv']: TYPE_LOCK, + DOMAINS['media']: TYPE_MEDIA, + DOMAINS['microwave']: TYPE_MICRO, + DOMAINS['mower']: TYPE_MOWER, + DOMAINS['outlet']: TYPE_OUTLET, + DOMAINS['oven']: TYPE_OVEN, + DOMAINS['push']: TYPE_SWITCH, + DOMAINS['scene']: TYPE_SCENE, + DOMAINS['screen']: TYPE_SCREEN, + DOMAINS['security']: TYPE_SECURITY, + DOMAINS['selector']: TYPE_SWITCH, + DOMAINS['sensor']: TYPE_SENSOR, + DOMAINS['smokedetector']: TYPE_SMOKE_DETECTOR, + DOMAINS['speaker']: TYPE_SPEAKER, + DOMAINS['switch']: TYPE_SWITCH, + DOMAINS['temperature']: TYPE_SENSOR, + DOMAINS['thermostat']: TYPE_THERMOSTAT, + DOMAINS['vacuum']: TYPE_VACUUM, + DOMAINS['valve']: TYPE_VALVE, + DOMAINS['washer']: TYPE_WASHER, + DOMAINS['waterheater']: TYPE_WATERHEATER, + DOMAINS['window']: TYPE_WINDOW, } diff --git a/helpers.py b/helpers.py index ec15beb..f4aa571 100644 --- a/helpers.py +++ b/helpers.py @@ -14,7 +14,14 @@ import google.auth.crypt import google.auth.jwt -from const import (CONFIGFILE, LOGFILE, KEYFILE, HOMEGRAPH_SCOPE, HOMEGRAPH_TOKEN_URL, PUBLIC_URL) +from const import ( + CONFIGFILE, + LOGFILE, + KEYFILE, + HOMEGRAPH_SCOPE, + HOMEGRAPH_TOKEN_URL, + PUBLIC_URL +) FILE_PATH = os.path.abspath(__file__) FILE_DIR = os.path.split(FILE_PATH)[0] @@ -116,6 +123,8 @@ def saveFile(filename, text): configuration['ClientID'] = 'sampleClientId' if 'ClientSecret' not in configuration: configuration['ClientSecret'] = 'sampleClientSecret' +if 'authToken' not in configuration: + configuration['authToken'] = 'ZsokmCwKjdhk7qHLeYd2' Auth = { 'clients': { @@ -125,17 +134,17 @@ def saveFile(filename, text): }, }, 'tokens': { - 'ZsokmCwKjdhk7qHLeYd2': { + configuration['authToken']: { 'uid': '1234', - 'accessToken': 'ZsokmCwKjdhk7qHLeYd2', - 'refreshToken': 'ZsokmCwKjdhk7qHLeYd2', + 'accessToken': configuration['authToken'], + 'refreshToken': configuration['authToken'], 'userAgentId': '1234', }, 'bfrrLnxxWdULSh3Y9IU2cA5pw8s4ub': { 'uid': '2345', 'accessToken': 'bfrrLnxxWdULSh3Y9IU2cA5pw8s4ub', 'refreshToken': 'bfrrLnxxWdULSh3Y9IU2cA5pw8s4ub', - 'userId': '2345' + 'userAgentId': '2345' }, }, 'users': { @@ -143,18 +152,11 @@ def saveFile(filename, text): 'uid': '1234', 'name': configuration['auth_user'], 'password': configuration['auth_pass'], - 'tokens': ['ZsokmCwKjdhk7qHLeYd2'], + 'tokens': [configuration['authToken']], }, - # '2345': { - # 'uid': '2345', - # 'name': configuration['auth_user_2'], - # 'password': configuration['auth_pass_2'], - # 'tokens': ['bfrrLnxxWdULSh3Y9IU2cA5pw8s4ub'], - # }, }, 'usernames': { configuration['auth_user']: '1234', - # configuration['auth_user_2']: '2345', } } @@ -206,6 +208,8 @@ def __init__(self): self.lastupdate = '' self.selectorLevelName = '' self.merge_thermo_idx = None + self.minThreehold = -20 + self.maxThreehold = 40 def uptime(): diff --git a/requirements/pip-requirements.txt b/requirements/pip-requirements.txt index 9b5f060..8a6407e 100644 --- a/requirements/pip-requirements.txt +++ b/requirements/pip-requirements.txt @@ -13,6 +13,6 @@ Jinja2>=2.11.3 PyChromecast==9.1.2 gTTS>=2.2.1 unicode-slugify>=0.1.3 -protobuf>=3.0.0 +protobuf==3.20.0 zeroconf>=0.25.1 casttube>=0.2.0 diff --git a/smarthome.py b/smarthome.py index fc09271..0dc9c40 100644 --- a/smarthome.py +++ b/smarthome.py @@ -6,6 +6,8 @@ import subprocess import sys import yaml +import random +import string from collections.abc import Mapping from itertools import product from pid import PidFile @@ -14,53 +16,67 @@ import trait from auth import * -from const import (DOMOTICZ_TO_GOOGLE_TYPES, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, - ERR_UNKNOWN_ERROR, ERR_CHALLENGE_NEEDED, DOMOTICZ_GET_ALL_DEVICES_URL, domains, - DOMOTICZ_GET_SETTINGS_URL, DOMOTICZ_GET_ONE_DEVICE_URL, DOMOTICZ_GET_SCENES_URL, CONFIGFILE, LOGFILE, - REQUEST_SYNC_BASE_URL, REPORT_STATE_BASE_URL, ATTRS_BRIGHTNESS, ATTRS_FANSPEED, ATTRS_VACUUM_MODES, - ATTRS_THERMSTATSETPOINT, ATTRS_COLOR_TEMP, ATTRS_PERCENTAGE, VERSION, DOMOTICZ_GET_VERSION) -from helpers import (configuration, readFile, saveFile, SmartHomeError, SmartHomeErrorNoChallenge, AogState, uptime, - getTunnelUrl, FILE_DIR, logger, ReportState, Auth, logfilepath) - -try: - from jinja2 import Environment, FileSystemLoader -except ImportError: - logger.info('Installing package jinja2') - subprocess.call(['pip3', 'install', 'jinja2']) - from jinja2 import Environment, FileSystemLoader +from const import ( + DOMOTICZ_TO_GOOGLE_TYPES, + ERR_FUNCTION_NOT_SUPPORTED, + ERR_PROTOCOL_ERROR, + ERR_DEVICE_OFFLINE, + ERR_UNKNOWN_ERROR, + ERR_CHALLENGE_NEEDED, + DOMOTICZ_GET_ALL_DEVICES_URL, + DOMAINS, + DOMOTICZ_GET_SETTINGS_URL, + DOMOTICZ_GET_ONE_DEVICE_URL, + DOMOTICZ_GET_SCENES_URL, + CONFIGFILE, + LOGFILE, + REQUEST_SYNC_BASE_URL, + REPORT_STATE_BASE_URL, + ATTRS_BRIGHTNESS, + ATTRS_FANSPEED, + ATTRS_VACUUM_MODES, + ATTRS_THERMSTATSETPOINT, + ATTRS_COLOR_TEMP, + ATTRS_PERCENTAGE, + ATTRS_HUMIDITY, + VERSION, + DOMOTICZ_GET_VERSION +) + +from helpers import ( + configuration, + readFile, + saveFile, + SmartHomeError, + SmartHomeErrorNoChallenge, + AogState, + uptime, + getTunnelUrl, + FILE_DIR, + logger, + ReportState, + Auth, + logfilepath +) +from jinja2 import Environment, FileSystemLoader if 'Chromecast_Name' in configuration and configuration['Chromecast_Name'] != 'add_chromecast_name': - try: - import pychromecast - except ImportError as e: - logger.error('Installing package pychromecast') - subprocess.call(['pip3', 'install', '--upgrade', '--force-reinstall', 'pychromecast']) - try: - from gtts import gTTS - except ImportError as e: - logger.error('Installing package gtts') - subprocess.call(['pip3', 'install', 'gtts']) - try: - from slugify import slugify - except ImportError as e: - logger.error('Installing package slugify') - subprocess.call(['pip3', 'install', 'slugify']) - import pychromecast - import socket ## + import socket from gtts import gTTS from slugify import slugify - logger.info("Starting up chromecasts") try: chromecasts, _ = pychromecast.get_chromecasts() cast = next(cc for cc in chromecasts if cc.device.friendly_name == configuration['Chromecast_Name']) + mc = cast.media_controller except Exception as e: logger.error('chromecasts init not succeeded, error : %s' % e) t = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) t.connect(("8.8.8.8", 80)) IP_Address = t.getsockname()[0] - t.close + t.close + IP_Port = str(configuration['port_number']) logger.info("IP_Address is : " + IP_Address) DOMOTICZ_URL = configuration['Domoticz']['ip'] + ':' + configuration['Domoticz']['port'] @@ -121,70 +137,70 @@ def checkupdate(): # some way to convert a domain type: Domoticz to google def AogGetDomain(device): if device["Type"] in ['Light/Switch', 'Lighting 1', 'Lighting 2', 'Lighting 5', 'RFY', 'Value']: - if device["SwitchType"] in ['Blinds', 'Venetian Blinds EU', 'Venetian Blinds US', + if device["SwitchType"] in ['Blinds', 'Blinds + Stop', 'Venetian Blinds EU', 'Venetian Blinds US', 'Blinds Percentage']: - return domains['blinds'] - elif device["SwitchType"] in ['Blinds Inverted', 'Blinds Percentage Inverted']: - return domains['blindsinv'] + return DOMAINS['blinds'] elif 'Door Lock' == device["SwitchType"]: - return domains['lock'] + return DOMAINS['lock'] elif 'Door Lock Inverted' == device["SwitchType"]: - return domains['lockinv'] + return DOMAINS['lockinv'] elif "Door Contact" == device["SwitchType"]: - return domains['door'] + return DOMAINS['door'] + elif "Doorbell" == device["SwitchType"]: + return DOMAINS['doorbell'] elif device["SwitchType"] in ['Push On Button', 'Push Off Button']: - return domains['push'] + return DOMAINS['push'] elif 'Motion Sensor' == device["SwitchType"]: - return domains['sensor'] + return DOMAINS['sensor'] elif 'Selector' == device["SwitchType"]: if device['Image'] == 'Fan': - return domains['fan'] + return DOMAINS['fan'] else: - return domains['selector'] + return DOMAINS['selector'] elif 'Smoke Detector' == device["SwitchType"]: - return domains['smokedetector'] + return DOMAINS['smokedetector'] elif 'Camera_Stream' in configuration and True == device["UsedByCamera"] and True == \ configuration['Camera_Stream']['Enabled']: - return domains['camera'] + return DOMAINS['camera'] elif device["Image"] == 'Generic': - return domains['switch'] + return DOMAINS['switch'] elif device["Image"] in ['Media', 'TV']: - return domains['media'] + return DOMAINS['media'] elif device["Image"] == 'WallSocket': - return domains['outlet'] + return DOMAINS['outlet'] elif device["Image"] == 'Speaker': - return domains['speaker'] + return DOMAINS['speaker'] elif device["Image"] == 'Fan': - return domains['fan'] + return DOMAINS['fan'] elif device["Image"] == 'Heating': - return domains['heater'] + return DOMAINS['heater'] else: - return domains['light'] + return DOMAINS['light'] elif 'Blinds' == device["Type"]: - return domains['blinds'] + return DOMAINS['blinds'] elif 'Group' == device["Type"]: - return domains['group'] + return DOMAINS['group'] elif 'Scene' == device["Type"]: - return domains['scene'] + return DOMAINS['scene'] elif device["Type"] in ['Temp', 'Temp + Humidity', 'Temp + Humidity + Baro']: - return domains['temperature'] + return DOMAINS['temperature'] elif 'Thermostat' == device['Type']: - return domains['thermostat'] + return DOMAINS['thermostat'] elif 'Color Switch' == device["Type"]: if "Dimmer" == device["SwitchType"]: - return domains['color'] + return DOMAINS['color'] elif "On/Off" == device["SwitchType"]: logger.info('%s (Idx: %s) is a color switch. To get all functions, set this device as Dimmer in Domoticz', device["Name"], device[ "idx"]) - return domains['light'] + return DOMAINS['light'] elif device["SwitchType"] in ['Push On Button', 'Push Off Button']: - return domains['push'] + return DOMAINS['push'] elif 'Security' == device["Type"]: - return domains['security'] + return DOMAINS['security'] return None def getDesc(state): - if state.domain in [domains['scene'], domains['group']]: + if state.domain in [DOMAINS['scene'], DOMAINS['group']]: if 'Scene_Config' in configuration and configuration['Scene_Config'] is not None: desc = configuration['Scene_Config'].get(int(state.id), None) return desc @@ -241,7 +257,7 @@ def getAog(device): aog.temp = device.get("Temp") aog.humidity = device.get("Humidity") aog.setpoint = device.get("SetPoint") - if aog.domain is domains['color']: + if aog.domain is DOMAINS['color']: aog.color = device.get("Color") aog.protected = device.get("Protected") aog.maxdimlevel = device.get("MaxDimLevel") @@ -269,18 +285,24 @@ def getAog(device): if desc is not None: dt = desc.get('devicetype', None) if dt is not None: - if aog.domain in [domains['blinds']]: + if aog.domain in [DOMAINS['blinds']]: if dt.lower() in ['window', 'gate', 'garage', 'door']: - aog.domain = domains[dt.lower()] - if aog.domain in [domains['light'], domains['switch']]: - if dt.lower() in ['window', 'door', 'gate', 'garage', 'light', 'ac_unit', 'bathtub', 'coffeemaker', 'dishwasher', 'dryer', 'fan', 'heater', 'kettle', 'media', 'microwave', 'outlet', 'oven', 'speaker', 'switch', 'vacuum', 'washer', 'waterheater']: - aog.domain = domains[dt.lower()] - if aog.domain in [domains['door']]: + aog.domain = DOMAINS[dt.lower()] + if aog.domain in [DOMAINS['light'], DOMAINS['switch']]: + if dt.lower() in ['window', 'door', 'doorbell', 'gate', 'garage', 'light', 'ac_unit', 'bathtub', 'coffeemaker', 'dishwasher', 'dryer', 'fan', 'heater', 'kettle', 'media', 'microwave', 'outlet', 'oven', 'speaker', 'switch', 'vacuum', 'washer', 'waterheater']: + aog.domain = DOMAINS[dt.lower()] + if aog.domain in [DOMAINS['door']]: if dt.lower() in ['window', 'gate', 'garage']: - aog.domain = domains[dt.lower()] - if aog.domain in [domains['selector']]: + aog.domain = DOMAINS[dt.lower()] + if aog.domain in [DOMAINS['selector']]: if dt.lower() in ['vacuum']: - aog.domain = domains[dt.lower()] + aog.domain = DOMAINS[dt.lower()] + if aog.domain in [DOMAINS['camera']]: + if dt.lower() in ['doorbell']: + aog.domain = DOMAINS[dt.lower()] + if aog.domain in [DOMAINS['push']]: + if dt.lower() in ['doorbell', 'outlet', 'light']: + aog.domain = DOMAINS[dt.lower()] pn = desc.get('name', None) if pn is not None: aog.name = pn @@ -298,65 +320,71 @@ def getAog(device): aog.report_state = False if not report_state: aog.report_state = report_state - if domains['thermostat'] == aog.domain: + if DOMAINS['thermostat'] == aog.domain: + minT = desc.get('minThreehold', None) + if minT is not None: + aog.minThreehold = minT + maxT = desc.get('maxThreehold', None) + if maxT is not None: + aog.maxThreehold = maxT at_idx = desc.get('actual_temp_idx', None) if at_idx is not None: aog.actual_temp_idx = at_idx try: - aog.state = str(aogDevs[domains['temperature'] + at_idx].temp) - aogDevs[domains['temperature'] + at_idx].domain = domains['merged'] + aog.id + ')' + aog.state = str(aogDevs[DOMAINS['temperature'] + at_idx].temp) + aogDevs[DOMAINS['temperature'] + at_idx].domain = DOMAINS['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find temperature device with idx %s', at_idx) modes_idx = desc.get('selector_modes_idx', None) if modes_idx is not None: aog.modes_idx = modes_idx try: - aog.level = aogDevs[domains['selector'] + modes_idx].level - aog.selectorLevelName = aogDevs[domains['selector'] + modes_idx].selectorLevelName - aogDevs[domains['selector'] + modes_idx].domain = domains['merged'] + aog.id + ')' + aog.level = aogDevs[DOMAINS['selector'] + modes_idx].level + aog.selectorLevelName = aogDevs[DOMAINS['selector'] + modes_idx].selectorLevelName + aogDevs[DOMAINS['selector'] + modes_idx].domain = DOMAINS['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find selector device with idx %s', modes_idx) - if aog.domain in [domains['heater'], domains['kettle'], domains['waterheater'], domains['oven']]: + if aog.domain in [DOMAINS['heater'], DOMAINS['kettle'], DOMAINS['waterheater'], DOMAINS['oven']]: tc_idx = desc.get('merge_thermo_idx', None) if tc_idx is not None: aog.merge_thermo_idx = tc_idx try: - aog.temp = aogDevs[domains['thermostat'] + tc_idx].state - aog.setpoint = aogDevs[domains['thermostat'] + tc_idx].setpoint - aogDevs[domains['thermostat'] + tc_idx].domain = domains['merged'] + aog.id + ')' + aog.temp = aogDevs[DOMAINS['thermostat'] + tc_idx].state + aog.setpoint = aogDevs[DOMAINS['thermostat'] + tc_idx].setpoint + aogDevs[DOMAINS['thermostat'] + tc_idx].domain = DOMAINS['merged'] + aog.id + ')' except: logger.error('Merge Error, Cant find thermostat device with idx %s', tc_idx) hide = desc.get('hide', False) if hide: - if aog.domain not in [domains['scene'], domains['group']]: - aog.domain = domains['hidden'] + if aog.domain not in [DOMAINS['scene'], DOMAINS['group']]: + aog.domain = DOMAINS['hidden'] else: logger.error('Scenes and Groups does not support function "hide" yet') - if aog.domain in [domains['camera']]: + if aog.domain in [DOMAINS['camera'], DOMAINS['doorbell']]: aog.report_state = False - if domains['light'] == aog.domain and "Dimmer" == device["SwitchType"]: + if DOMAINS['light'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS - if domains['fan'] == aog.domain and "Selector" == device["SwitchType"]: + if DOMAINS['fan'] == aog.domain and "Selector" == device["SwitchType"]: aog.attributes = ATTRS_FANSPEED - if domains['outlet'] == aog.domain and "Dimmer" == device["SwitchType"]: + if DOMAINS['outlet'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS - if domains['color'] == aog.domain and "Dimmer" == device["SwitchType"]: + if DOMAINS['color'] == aog.domain and "Dimmer" == device["SwitchType"]: aog.attributes = ATTRS_BRIGHTNESS - if domains['color'] == aog.domain and device["SubType"] in ["RGBWW", "RGBWZ", "White"]: + if DOMAINS['color'] == aog.domain and device["SubType"] in ["RGBWW", "RGBWZ", "White"]: aog.attributes = ATTRS_COLOR_TEMP - if domains['thermostat'] == aog.domain and "Thermostat" == device["Type"]: + if DOMAINS['thermostat'] == aog.domain and "Thermostat" == device["Type"]: aog.attributes = ATTRS_THERMSTATSETPOINT - if domains['blinds'] == aog.domain and "Blinds Percentage" == device["SwitchType"]: - aog.attributes = ATTRS_PERCENTAGE - if domains['blindsinv'] == aog.domain and "Blinds Percentage Inverted" == device["SwitchType"]: + if DOMAINS['blinds'] == aog.domain and ("Blinds Percentage" == device["SwitchType"] or "Blinds + Stop" == device["SwitchType"]): aog.attributes = ATTRS_PERCENTAGE - if domains['vacuum'] == aog.domain and "Selector" == device["SwitchType"]: + if DOMAINS['vacuum'] == aog.domain and "Selector" == device["SwitchType"]: aog.attributes = ATTRS_VACUUM_MODES + if DOMAINS['temperature'] == aog.domain and device["Type"] in ['Temp + Humidity', 'Temp + Humidity + Baro']: + aog.attributes = ATTRS_HUMIDITY if aog.room == None: - if aog.domain not in [domains['scene'], domains['group']]: + if aog.domain not in [DOMAINS['scene'], DOMAINS['group']]: if aog.plan != "0": aog.room = getPlans(aog.plan) @@ -408,7 +436,7 @@ def getDevices(devices="all", idx="0"): req[aog.name]['willReportState'] = aog.report_state logger.debug(json.dumps(req, indent=2, sort_keys=False, ensure_ascii=False)) - devlist = [(d.name, int(d.id), d.domain, d.state, d.room, d.nicknames, d.report_state) for d in aogDevs.values()] + devlist = [(d.name, int(d.id), d.domain, d.state, d.room, d.nicknames, d.report_state, d.entity_id) for d in aogDevs.values()] devlist.sort(key=takeSecond) deviceList = json.dumps(devlist) @@ -520,6 +548,7 @@ def sync_serialize(self, agent_user_id): device = { 'id': state.entity_id, + 'notificationSupportedByAgent': (True if state.domain == 'Doorbell' else False), 'name': { 'name': state.name }, @@ -545,6 +574,9 @@ def sync_serialize(self, agent_user_id): room = state.room if room: device['roomHint'] = room + + if state.domain == 'Doorbell': + device['traits'].append('action.devices.traits.ObjectDetection') return device @@ -560,7 +592,7 @@ def query_serialize(self): attrs = {'online': True} for trt in self.traits(): deep_update(attrs, trt.query_attributes()) - + return attrs def execute(self, command, params, challenge): @@ -579,9 +611,9 @@ def execute(self, command, params, challenge): else: protect = False - if protect or self.state.domain == domains['security']: + if protect or self.state.domain == DOMAINS['security']: pincode = configuration['Domoticz']['switchProtectionPass'] - if self.state.domain == domains['security']: + if self.state.domain == DOMAINS['security']: pincode = self.state.seccode acknowledge = False if challenge is None: @@ -596,12 +628,11 @@ def execute(self, command, params, challenge): raise SmartHomeErrorNoChallenge(ERR_CHALLENGE_NEEDED, 'challengeFailedPinNeeded', 'Unable to execute {} for {} - challenge needed '.format( command, self.state.entity_id)) - elif self.state.domain == domains['security'] and pincode != hashlib.md5( + elif self.state.domain == DOMAINS['security'] and pincode != hashlib.md5( str.encode(challenge.get('pin'))).hexdigest(): raise SmartHomeErrorNoChallenge(ERR_CHALLENGE_NEEDED, 'challengeFailedPinNeeded', 'Unable to execute {} for {} - challenge needed '.format( command, self.state.entity_id)) - if acknowledge: if challenge is None: raise SmartHomeErrorNoChallenge(ERR_CHALLENGE_NEEDED, 'ackNeeded', @@ -623,10 +654,11 @@ def execute(self, command, params, challenge): def async_update(self): """Update the entity with latest info from Domoticz.""" - if self.state.domain == domains['group'] or self.state.domain == domains['scene']: + if self.state.domain == DOMAINS['group'] or self.state.domain == DOMAINS['scene']: getDevices('scene') else: getDevices('id', self.state.id) + class SmartHomeReqHandler(OAuthReqHandler): global smarthomeControlMappings @@ -661,6 +693,8 @@ def smarthome_process(self, message, token): } handler = smarthomeControlMappings.get(inputs[0].get('intent')) + + logger.info("Google Assistant requests an " + inputs[0].get('intent')) if handler is None: return {'requestId': request_id, 'payload': {'errorCode': ERR_PROTOCOL_ERROR}} @@ -692,8 +726,8 @@ def smarthome_post(self, s): message = json.loads(s.body) self._request_id = message.get('requestId') - - logger.info("Request " + json.dumps(message, indent=2, sort_keys=True, ensure_ascii=False)) + + logger.info(json.dumps(message, indent=2, sort_keys=True, ensure_ascii=False)) response = self.smarthome_process(message, token) try: @@ -701,7 +735,66 @@ def smarthome_post(self, s): logger.error('Error handling message %s: %s' % (message, response['payload'])) except: pass + s.send_json(200, json.dumps(response, ensure_ascii=False).encode('utf-8'), True) + + def notification_post(self, s): + logger.debug(s.headers) + a = s.headers.get('Authorization', None) + + token = None + if a is not None: + types, tokenH = a.split() + if types.lower() == 'bearer': + token = Auth['tokens'].get(tokenH, None) + + if token is None: + raise SmartHomeError(ERR_PROTOCOL_ERROR, 'not authorized access!!') + + event_id = ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + + string.digits, k=10)) + + request_id = ''.join(random.choices(string.digits, k=20)) + + message = s.body + message = message.replace('|', ' ').split() + if '>>' in message: message.remove('>>') + devid = message[0] + state = message[1] + + aog = aogDevs.get(devid, None) + if aog != None: + if aog.domain in DOMAINS['doorbell']: + data = { + 'requestId': str(request_id), + 'agentUserId': token.get('userAgentId', None), + 'eventId': str(event_id), + 'payload': { + 'devices': { + 'states': { + devid: { + 'on': (True if state.lower() in ['on', 'pressed'] else False) + }, + }, + 'notifications': { + devid: { + "ObjectDetection": { + "objects": { + "unfamiliar": 1 + }, + "priority": 0, + "detectionTimestamp": time.time() + } + } + } + } + } + } + ReportState.call_homegraph_api(REPORT_STATE_BASE_URL, data) + else: + logger.info('Notification is not supported for ' + message[0]) + else: + logger.error('Something went wrong, check your notification settings!') def smarthome(self, s): s.send_message(500, "not supported") @@ -893,7 +986,6 @@ def smarthome_sync(self, payload, token): aogDevs.clear() getDevices() # sync all devices getSettings() - enableReport = ReportState.enable_report_state() agent_user_id = token.get('userAgentId', None) for state in aogDevs.values(): @@ -913,15 +1005,14 @@ def smarthome_sync(self, payload, token): def smarthome_query(self, payload, token): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery - """ - enableReport = ReportState.enable_report_state() + """ response = {} devices = {} - getDevices() + #getDevices() for device in payload.get('devices', []): devid = device['id'] - #_GoogleEntity(aogDevs.get(devid, None)).async_update() + _GoogleEntity(aogDevs.get(devid, None)).async_update() state = aogDevs.get(devid, None) if not state: # If we can't find a state, the device is offline @@ -936,24 +1027,24 @@ def smarthome_query(self, payload, token): devices[devid] = {"online": False} response = {'devices': devices} - logger.info("Response " + json.dumps(response, indent=2, sort_keys=True, ensure_ascii=False)) - - if state.report_state == True and enableReport == True: - self.report_state(devices, token) - + logger.info(json.dumps(response, indent=2, sort_keys=True, ensure_ascii=False)) + return {'devices': devices} def smarthome_exec(self, payload, token): """Handle action.devices.EXECUTE request. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute """ + enableReport = ReportState.enable_report_state() entities = {} results = {} + devices = {} for command in payload['commands']: for device, execution in product(command['devices'], command['execution']): entity_id = device['id'] + # Happens if error occurred. Skip entity for further processing if entity_id in results: continue @@ -964,10 +1055,11 @@ def smarthome_exec(self, payload, token): getSettings() state = aogDevs.get(entity_id, None) + if state is None: results[entity_id] = {'ids': [entity_id], 'status': 'ERROR', 'errorCode': ERR_DEVICE_OFFLINE} continue - + entities[entity_id] = _GoogleEntity(state) try: @@ -982,13 +1074,23 @@ def smarthome_exec(self, payload, token): 'challengeNeeded': {'type': err.desc}} logger.error(err) + if state.report_state == True and enableReport == True: + devices[entity_id] = execution.get('params', {}) + devices[entity_id].update({'online': True}) + if 'followUpToken' in devices[entity_id]: + devices[entity_id].pop('followUpToken') + self.report_state(devices, token) + final_results = list(results.values()) + for entity in entities.values(): if entity.entity_id in results: continue entity.async_update() - final_results.append({'ids': [entity.entity_id], 'status': 'SUCCESS', 'states': entity.query_serialize()}) - + newState = entity.query_serialize() + newState.update(execution.get('params', {})) + final_results.append({'ids': [entity.entity_id], 'status': 'SUCCESS', 'states': newState}) + return {'commands': final_results} def smarthome_disconnect(self, payload, token): @@ -997,8 +1099,14 @@ def smarthome_disconnect(self, payload, token): """ return None - def say(self, s): - itext = s.url.query.replace(" ","-") + def say(self, s): #command "/say?text-to-say/lang@volume@device" + answ, scomm, rdevice, rvol, rcontent, rtype, stime = SmartHomeReqHandler.read_input(s.url.query) + if answ=="Error": + if rcontent!="?": + answ, message = SmartHomeReqHandler.playmedia(rcontent, rtype, 'PLAYING', 40) + SmartHomeReqHandler.send_resp("Error", s.url.query, scomm, stime, s) + return + itext = scomm.replace(" ","-") itext=itext.split("/") text = itext[0] if not text: @@ -1008,53 +1116,212 @@ def say(self, s): else: lang = "en" slow = False - current_time = time.strftime("%d/%m/%y %H:%M:%S", time.localtime()) - message="say command on " + current_time + ", text : " + str(text) + ", lang : " + lang tts = gTTS(text=text, lang=lang, slow=slow) - filename = slugify(text+"-"+lang+"-"+str(slow)) + ".mp3" + filename = slugify(text+"-"+lang+"-"+str(slow), only_ascii=True) + ".mp3" cache_filename = FILE_DIR + "/sound/cache/" + filename tts_file = Path(cache_filename) if not tts_file.is_file(): logger.info(tts) tts.save(cache_filename) - mp3_url = "http://" + IP_Address + ":" + str(s.server.server_port) + "/sound?cache/" + filename - #make a query request for Get /sound - logger.info(message) - SmartHomeReqHandler.play_mp3(mp3_url) - s.send_message(200, "OK " + message + "\n") - - def play(self, s): - filename = s.url.query + mp3_url = "http://" + IP_Address + ":" + IP_Port + "/sound?cache/" + filename #make a query request for Get /sound + rstatus, rmessage = SmartHomeReqHandler.playmedia(mp3_url,'audio/mp3','IDLE', 20) + if rvol!="?": + answ, message = SmartHomeReqHandler.setvolume(str(round(rvol*100))) + rmessage = rmessage + " restore volume " + str(round(rvol*100)) + if rcontent!="?": + answ, message = SmartHomeReqHandler.playmedia(rcontent, rtype, 'PLAYING', 40) + rmessage = rmessage + " restore stream : " + rcontent + if rdevice!="?": + answ, message = SmartHomeReqHandler.switchdevice(rdevice) + rmessage = rmessage + " restore device '" + rdevice+ "'" + SmartHomeReqHandler.send_resp(rstatus, "say " + s.url.query, rmessage, stime, s) + + def play(self, s): #command "/play?soundfile.mp3@volume@device" + answ, scomm, rdevice, rvol, rcontent, rtype, stime = SmartHomeReqHandler.read_input(s.url.query) + if answ=="Error": + if rcontent!="?": + answ, message = SmartHomeReqHandler.playmedia(rcontent, rtype, 'PLAYING', 40) + SmartHomeReqHandler.send_resp("Error", s.url.query, scomm, stime, s) + return + filename = scomm mp3_filename = FILE_DIR + "/sound/" + filename mp3 = Path(mp3_filename) - current_time = time.strftime("%d/%m/%y %H:%M:%S", time.localtime()) - message = "play command on " + current_time + ", file : " + str(mp3_filename) - logger.info(message) if mp3.is_file(): - mp3_url = "http://" + IP_Address + ":" + str(s.server.server_port) + "/sound?" + filename + mp3_url = "http://" + IP_Address + ":" + IP_Port + "/sound?" + filename #make a query request for Get /sound - SmartHomeReqHandler.play_mp3(mp3_url) - s.send_message(200, "OK " + message + "\n") + rstatus, rmessage = SmartHomeReqHandler.playmedia(mp3_url,'audio/mp3','IDLE', 20) else: - s.send_message(200, "File not found\n") - - def play_mp3(mp3_url): - cast.wait() - mc = cast.media_controller - mc.play_media(mp3_url, 'audio/mp3') - logger.info("Play mp3 started") + rstatus="Error" + rmessage = str(mp3_filename) + ", file not found!" + if rvol!="?": + answ, message = SmartHomeReqHandler.setvolume(str(round(rvol*100))) + rmessage = rmessage + " restore volume " + str(round(rvol*100)) + if rcontent!="?": + answ, message = SmartHomeReqHandler.playmedia(rcontent, rtype, 'PLAYING', 40) + rmessage = rmessage + " restore stream : " + rcontent + if rdevice!="?": + answ, message = SmartHomeReqHandler.switchdevice(rdevice) + rmessage = rmessage + " restore device '" + rdevice+ "'" + SmartHomeReqHandler.send_resp(rstatus, "play " + s.url.query, rmessage, stime, s) def send_sound(self, s): filename = s.url.query cache_filename = FILE_DIR + "/sound/" + filename - logger.info("sound : " + cache_filename) + logger.debug("Request for soundfile received, file = " + str(cache_filename)) f = open(cache_filename, 'rb') - s.send_response(200) # send_message (later in server.py) + s.send_response(200) s.send_header('Content-type', 'audio/mpeg3') s.end_headers() s.wfile.write(f.read()) f.close() + logger.debug("File returned succesfully") + + def send_resp(rstatus, rcommand, rmessage, stime, s): + # time.sleep(1) + etime = time.strftime("%d/%m/%y %H:%M:%S", time.localtime()) + rvolume = "{:.0%}".format(cast.status.volume_level) + rcontent = mc.status.content_id + rtype = mc.status.content_type + rpstate = mc.status.player_state + if rpstate == "UNKNOWN": + rcontent="?" + rtype="?" + message='{"device":"'+ cast.device.friendly_name + '","status":"' + rstatus + '","command":"' + rcommand + '","volume":"' +rvolume +'","starttime":"' + stime + '","endtime":"' + etime + '","playstate":"' + rpstate + '","content":"' + rcontent + '","type":"' + rtype + '","message":"' + rmessage+ '"}' + s.send_json(200, message, False) + logger.info(message) + def read_input(ctext): + global cast, mc, chromecasts + stime = time.strftime("%d/%m/%y %H:%M:%S", time.localtime()) + answ="OK" + message="" + rdevice = "?" + rvol = "?" + rcontent = "?" + rtype = "?" + ctext = ctext.split("@") + try: + svol=ctext[1] + except: + svol="" + try: + sdevice=ctext[2] + except: + sdevice="" + if sdevice!="": + rdevice = cast.device.friendly_name + answ, message = SmartHomeReqHandler.switchdevice(sdevice) + if answ == "Error": + return answ, message, rdevice, rvol, rcontent, rtype, stime + rpstate = mc.status.player_state + if rpstate != "UNKNOWN" and rpstate != "IDLE": + mc.stop() + rcontent = mc.status.content_id + rtype = mc.status.content_type + else: + rcontent = "?" + rtype = "?" + if svol!="": + cast.wait() + rvol = cast.status.volume_level + answ, message = SmartHomeReqHandler.setvolume(svol) + if answ == "Error": + return answ, message, rdevice, rvol, rcontent, rtype, stime + return answ, ctext[0], rdevice, rvol, rcontent, rtype, stime + + def switchdevice(sdevice): + global cast, mc, chromecasts + sdevice = sdevice.replace("%20"," ") + try: + cast = next(cc for cc in chromecasts if cc.device.friendly_name == sdevice) + cast.wait() + mc = cast.media_controller + return "OK","Switched to device " + str(cast.device.friendly_name) + except Exception as e: + logger.error('chromecasts init not succeeded, error : %s' % e) + return "Error","Not switched to device " + str(sdevice) + + def setvolume(svol): + global cast, mc, chromecasts + svol=svol.replace("%","") + try: + cast.wait() + cast.set_volume(int(svol)/100) + time.sleep(1) + cast.wait() + return "OK","Volume level set to : " + svol +"%" + except Exception as e: + logger.error('Chromecast setvolume unsuccesfull, error : %s' % e) + return "Error","Volume level not set to : " + svol +"%" + + def playmedia(pmedia,ptype, wstate, tmax): + try: + mc.play_media(pmedia, ptype) + mc.block_until_active() + cast.wait() + pstate = "?" + i=1 #max x seconds + while (mc.status.player_state != wstate or pstate != wstate) and iDevice list
- + diff --git a/templates/js/functions.js b/templates/js/functions.js index 48c7f0f..8625a23 100644 --- a/templates/js/functions.js +++ b/templates/js/functions.js @@ -124,7 +124,7 @@ function readDevices(devicelist){ if (devicelist[i][5] == undefined) { nicknames = " "; }else{ nicknames = " (" + devicelist[i][5] + ")"} - xl += "" + xl += "" xl += ""; xl += ""; if (devicelist[i][3] == "Off" | devicelist[i][3] == "Closed"){ @@ -269,6 +269,12 @@ $("#ngrok_tunnel").click(function(){ } }); +$(function() { + $("#logout").click(function(e) { + document.cookie = 'aog_session_id=; Max-Age=0; path=/'; + }); + }); + function getIssues(){ $.ajax({ diff --git a/templates/settings.html b/templates/settings.html index ff8c686..9a7c8e9 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -302,6 +302,15 @@

Advanced Settings

Change pidfile path and name. + +
+
+
+ + + This is your authToken to use with notifications. +
+
diff --git a/trait.py b/trait.py index c61c12f..39ca212 100644 --- a/trait.py +++ b/trait.py @@ -2,65 +2,80 @@ import base64 import json -from datetime import datetime - import requests -from const import (ATTRS_BRIGHTNESS, ATTRS_THERMSTATSETPOINT, ATTRS_COLOR, ATTRS_COLOR_TEMP, ATTRS_PERCENTAGE, - ATTRS_VACUUM_MODES, domains, ERR_ALREADY_IN_STATE, ERR_WRONG_PIN, ERR_NOT_SUPPORTED, - ATTRS_FANSPEED) - +from datetime import datetime from helpers import SmartHomeError, configuration, logger, tempConvert +from const import ( + ATTRS_BRIGHTNESS, + ATTRS_THERMSTATSETPOINT, + ATTRS_COLOR, + ATTRS_COLOR_TEMP, + ATTRS_PERCENTAGE, + ATTRS_VACUUM_MODES, + DOMAINS, + ERR_ALREADY_IN_STATE, + ERR_WRONG_PIN, + ERR_NOT_SUPPORTED, + ATTRS_FANSPEED +) DOMOTICZ_URL = configuration['Domoticz']['ip'] + ':' + configuration['Domoticz']['port'] PREFIX_TRAITS = 'action.devices.traits.' -TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff' -TRAIT_DOCK = PREFIX_TRAITS + 'Dock' -TRAIT_STARTSTOP = PREFIX_TRAITS + 'StartStop' -TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness' -TRAIT_COLOR_SETTING = PREFIX_TRAITS + 'ColorSetting' -TRAIT_SCENE = PREFIX_TRAITS + 'Scene' -TRAIT_TEMPERATURE_CONTROL = PREFIX_TRAITS + 'TemperatureControl' -TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' -TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' -TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' -TRAIT_MODES = PREFIX_TRAITS + 'Modes' -TRAIT_OPEN_CLOSE = PREFIX_TRAITS + 'OpenClose' -TRAIT_ARM_DISARM = PREFIX_TRAITS + 'ArmDisarm' -TRAIT_VOLUME = PREFIX_TRAITS + 'Volume' -TRAIT_CAMERA_STREAM = PREFIX_TRAITS + 'CameraStream' -TRAIT_TOGGLES = PREFIX_TRAITS + 'Toggles' -TRAIT_TIMER = PREFIX_TRAITS + 'Timer' -TRAIT_ENERGY = PREFIX_TRAITS + 'EnergyStorage' +TRAIT_ONOFF = f'{PREFIX_TRAITS}OnOff' +TRAIT_DOCK = f'{PREFIX_TRAITS}Dock' +TRAIT_STARTSTOP = f'{PREFIX_TRAITS}StartStop' +TRAIT_BRIGHTNESS = f'{PREFIX_TRAITS}Brightness' +TRAIT_COLOR_SETTING = f'{PREFIX_TRAITS}ColorSetting' +TRAIT_SCENE = f'{PREFIX_TRAITS}Scene' +TRAIT_TEMPERATURE_CONTROL = f'{PREFIX_TRAITS}TemperatureControl' +TRAIT_TEMPERATURE_SETTING = f'{PREFIX_TRAITS}TemperatureSetting' +TRAIT_LOCKUNLOCK = f'{PREFIX_TRAITS}LockUnlock' +TRAIT_FANSPEED = f'{PREFIX_TRAITS}FanSpeed' +TRAIT_MODES = f'{PREFIX_TRAITS}Modes' +TRAIT_OPEN_CLOSE = f'{PREFIX_TRAITS}OpenClose' +TRAIT_ARM_DISARM = f'{PREFIX_TRAITS}ArmDisarm' +TRAIT_VOLUME = f'{PREFIX_TRAITS}Volume' +TRAIT_CAMERA_STREAM = f'{PREFIX_TRAITS}CameraStream' +TRAIT_TOGGLES = f'{PREFIX_TRAITS}Toggles' +TRAIT_TIMER = f'{PREFIX_TRAITS}Timer' +TRAIT_ENERGY = f'{PREFIX_TRAITS}EnergyStorage' +TRAIT_HUMIDITY = f'{PREFIX_TRAITS}HumiditySetting' +TRAIT_OBJECTDETECTION = f'{PREFIX_TRAITS}ObjectDetection' +TRAIT_SENSOR_STATE = f'{PREFIX_TRAITS}SensorState' PREFIX_COMMANDS = 'action.devices.commands.' -COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' -COMMAND_DOCK = PREFIX_COMMANDS + 'Dock' -COMMAND_STARTSTOP = PREFIX_COMMANDS + 'StartStop' -COMMAND_PAUSEUNPAUSE = PREFIX_COMMANDS + 'PauseUnpause' -COMMAND_BRIGHTNESS_ABSOLUTE = PREFIX_COMMANDS + 'BrightnessAbsolute' -COMMAND_COLOR_ABSOLUTE = PREFIX_COMMANDS + 'ColorAbsolute' -COMMAND_ACTIVATE_SCENE = PREFIX_COMMANDS + 'ActivateScene' +COMMAND_ONOFF = f'{PREFIX_COMMANDS}OnOff' +COMMAND_DOCK = f'{PREFIX_COMMANDS}Dock' +COMMAND_STARTSTOP = f'{PREFIX_COMMANDS}StartStop' +COMMAND_PAUSEUNPAUSE = f'{PREFIX_COMMANDS}PauseUnpause' +COMMAND_BRIGHTNESS_ABSOLUTE = f'{PREFIX_COMMANDS}BrightnessAbsolute' +COMMAND_COLOR_ABSOLUTE = f'{PREFIX_COMMANDS}ColorAbsolute' +COMMAND_ACTIVATE_SCENE = f'{PREFIX_COMMANDS}ActivateScene' COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT = ( - PREFIX_COMMANDS + 'ThermostatTemperatureSetpoint') + f'{PREFIX_COMMANDS}ThermostatTemperatureSetpoint' + ) COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = ( - PREFIX_COMMANDS + 'ThermostatTemperatureSetRange') -COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' -COMMAND_THERMOSTAT_TEMPERATURE_RELATIVE = PREFIX_COMMANDS + 'TemperatureRelative' -COMMAND_SET_TEMPERATURE = PREFIX_COMMANDS + 'SetTemperature' -COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' -COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' -COMMAND_MODES = PREFIX_COMMANDS + 'SetModes' -COMMAND_OPEN_CLOSE = PREFIX_COMMANDS + 'OpenClose' -COMMAND_ARM_DISARM = PREFIX_COMMANDS + 'ArmDisarm' -COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume' -COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative' -COMMAND_GET_CAMERA_STREAM = PREFIX_COMMANDS + 'GetCameraStream' -COMMAND_TOGGLES = PREFIX_COMMANDS + 'SetToggles' -COMMAND_TIMER_START = PREFIX_COMMANDS + 'TimerStart' -COMMAND_TIMER_CANCEL = PREFIX_COMMANDS + 'TimerCancel' -COMMAND_CHARGE = PREFIX_COMMANDS + 'Charge' + f'{PREFIX_COMMANDS}ThermostatTemperatureSetRange' + ) +COMMAND_THERMOSTAT_SET_MODE = f'{PREFIX_COMMANDS}ThermostatSetMode' +COMMAND_THERMOSTAT_TEMPERATURE_RELATIVE = f'{PREFIX_COMMANDS}TemperatureRelative' +COMMAND_SET_TEMPERATURE = f'{PREFIX_COMMANDS}SetTemperature' +COMMAND_LOCKUNLOCK = f'{PREFIX_COMMANDS}LockUnlock' +COMMAND_FANSPEED = f'{PREFIX_COMMANDS}SetFanSpeed' +COMMAND_MODES = f'{PREFIX_COMMANDS}SetModes' +COMMAND_OPEN_CLOSE = f'{PREFIX_COMMANDS}OpenClose' +COMMAND_ARM_DISARM = f'{PREFIX_COMMANDS}ArmDisarm' +COMMAND_SET_VOLUME = f'{PREFIX_COMMANDS}setVolume' +COMMAND_VOLUME_RELATIVE = f'{PREFIX_COMMANDS}volumeRelative' +COMMAND_GET_CAMERA_STREAM = f'{PREFIX_COMMANDS}GetCameraStream' +COMMAND_TOGGLES = f'{PREFIX_COMMANDS}SetToggles' +COMMAND_TIMER_START = f'{PREFIX_COMMANDS}TimerStart' +COMMAND_TIMER_CANCEL = f'{PREFIX_COMMANDS}TimerCancel' +COMMAND_CHARGE = f'{PREFIX_COMMANDS}Charge' +COMMAND_SET_HUMIDITY = f'{PREFIX_COMMANDS}SetHumidity' +COMMAND_SET_HUMIDITY_RELATIVE = f'{PREFIX_COMMANDS}HumidityRelative' TRAITS = [] @@ -122,38 +137,39 @@ def supported(domain, features): """Test if state is supported.""" return domain in ( - domains['ac_unit'], - domains['bathtub'], - domains['coffeemaker'], - domains['color'], - domains['cooktop'], - domains['dishwasher'], - domains['dryer'], - domains['fan'], - domains['group'], - domains['heater'], - domains['kettle'], - domains['light'], - domains['media'], - domains['microwave'], - domains['mower'], - domains['outlet'], - domains['oven'], - domains['push'], - domains['sensor'], - domains['smokedetector'], - domains['speaker'], - domains['switch'], - #domains['vacuum'], - domains['washer'], - domains['waterheater'], + DOMAINS['ac_unit'], + DOMAINS['bathtub'], + DOMAINS['coffeemaker'], + DOMAINS['color'], + DOMAINS['cooktop'], + DOMAINS['dishwasher'], + DOMAINS['doorbell'], + DOMAINS['dryer'], + DOMAINS['fan'], + DOMAINS['group'], + DOMAINS['heater'], + DOMAINS['kettle'], + DOMAINS['light'], + DOMAINS['media'], + DOMAINS['microwave'], + DOMAINS['mower'], + DOMAINS['outlet'], + DOMAINS['oven'], + DOMAINS['push'], + DOMAINS['sensor'], + DOMAINS['smokedetector'], + DOMAINS['speaker'], + DOMAINS['switch'], + #DOMAINS['vacuum'], + DOMAINS['washer'], + DOMAINS['waterheater'], ) def sync_attributes(self): """Return OnOff attributes for a sync request.""" domain = self.state.domain response = {} - if domain in [domains['sensor'], domains['smokedetector']]: + if domain in [DOMAINS['sensor'], DOMAINS['smokedetector'], DOMAINS['doorbell']]: response['queryOnlyOnOff'] = True return response @@ -163,11 +179,11 @@ def query_attributes(self): domain = self.state.domain response = {} - if domain == domains['push']: + if domain == DOMAINS['push']: response['on'] = False else: response['on'] = self.state.state != 'Off' - if domain != domains['group'] and self.state.battery <= configuration['Low_battery_limit']: + if domain != DOMAINS['group'] and self.state.battery <= configuration['Low_battery_limit']: response['exceptionCode'] = 'lowBattery' return response @@ -177,8 +193,8 @@ def execute(self, command, params): domain = self.state.domain protected = self.state.protected - if domain not in [domains['sensor'], domains['smokedetector']]: - if domain == domains['group']: + if domain not in [DOMAINS['sensor'], DOMAINS['smokedetector']]: + if domain == DOMAINS['group']: url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchscene&idx=' + self.state.id + '&switchcmd=' + ( 'On' if params['on'] else 'Off') else: @@ -212,7 +228,7 @@ class SceneTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in domains['scene'] + return domain in DOMAINS['scene'] def sync_attributes(self): """Return scene attributes for a sync request.""" @@ -256,7 +272,7 @@ class BrightnessTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - if domain in (domains['light'], domains['color'], domains['outlet']): + if domain in (DOMAINS['light'], DOMAINS['color'], DOMAINS['outlet']): return features & ATTRS_BRIGHTNESS return False @@ -322,18 +338,23 @@ class OpenCloseTrait(_Trait): def supported(domain, features): """Test if state is supported.""" return domain in ( - domains['blinds'], - domains['blindsinv'], - domains['door'], - domains['window'], - domains['gate'], - domains['garage'], - domains['valve'] + DOMAINS['blinds'], + DOMAINS['door'], + DOMAINS['window'], + DOMAINS['gate'], + DOMAINS['garage'], + DOMAINS['valve'] ) def sync_attributes(self): """Return OpenClose attributes for a sync request.""" - return {} + features = self.state.attributes + response = {} + + if features & ATTRS_PERCENTAGE != True: + response['discreteOnlyOpenClose'] = True + + return response def query_attributes(self): """Return OpenClose query attributes.""" @@ -343,12 +364,6 @@ def query_attributes(self): if features & ATTRS_PERCENTAGE: response['openPercent'] = self.state.level - - elif domain == domains['blindsinv']: - if self.state.state in ['Open', 'Off']: - response['openPercent'] = 0 - else: - response['openPercent'] = 100 else: if self.state.state in ['Open', 'Off']: response['openPercent'] = 100 @@ -368,39 +383,24 @@ def execute(self, command, params): domain = self.state.domain if features & ATTRS_PERCENTAGE: - if domain == domains['blindsinv']: + if domain == DOMAINS['blinds']: url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=Set%20Level&level=' + str( params['openPercent']) - else: - url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=Set%20Level&level=' + str( - 100 - params['openPercent']) else: p = params.get('openPercent', 50) url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=' - if domain == domains['blindsinv']: - if p == 0 and state in ['Closed', 'Stopped', 'On']: - # open - url += 'Off' - elif p == 100 and state in ['Open', 'Stopped', 'Off']: - # close - url += 'On' - else: - raise SmartHomeError(ERR_ALREADY_IN_STATE, - 'Unable to execute {} for {}. Already in state '.format(command, - self.state.entity_id)) - else: - if p == 100 and state in ['Closed', 'Stopped', 'On']: - # open - url += 'Off' - elif p == 0 and state in ['Open', 'Stopped', 'Off']: - # close - url += 'On' - else: - raise SmartHomeError(ERR_ALREADY_IN_STATE, - 'Unable to execute {} for {}. Already in state '.format(command, - self.state.entity_id)) + if p == 100 and state in ['Closed', 'Stopped', 'On']: + # open + url += 'Open' + elif p == 0 and state in ['Open', 'Stopped', 'Off']: + # close + url += 'Close' + else: + raise SmartHomeError(ERR_ALREADY_IN_STATE, + 'Unable to execute {} for {}. Already in state '.format(command, + self.state.entity_id)) if protected: url = url + '&passcode=' + configuration['Domoticz']['switchProtectionPass'] @@ -426,13 +426,13 @@ class StartStopTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - if domain == domains['blinds']: + if domain == DOMAINS['blinds']: if features & ATTRS_PERCENTAGE: return False else: - return domain in domains['blinds'] + return domain in DOMAINS['blinds'] else: - return domain in domains['vacuum'] + return domain in DOMAINS['vacuum'] def sync_attributes(self): """Return StartStop attributes for a sync request.""" @@ -442,7 +442,7 @@ def query_attributes(self): """Return StartStop query attributes.""" domain = self.state.domain response = {} - if domain == domains['blinds']: + if domain == DOMAINS['blinds']: response['isRunning'] = True else: response['isRunning'] = self.state.state != 'Off' @@ -458,7 +458,7 @@ def execute(self, command, params): protected = self.state.protected if command == COMMAND_STARTSTOP: - if domain == domains['blinds']: + if domain == DOMAINS['blinds']: if params['start'] is False: url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=Stop' else: @@ -478,89 +478,6 @@ def execute(self, command, params): self.state.entity_id)) -# @register_trait -# class FanSpeedTrait(_Trait): - # """Trait to control speed of Fan. - # https://developers.google.com/actions/smarthome/traits/fanspeed - # """ - - # name = TRAIT_FANSPEED - # commands = [COMMAND_FANSPEED] - - # speed_synonyms = { - # 'off': ["stop", "off"], - # 'speed_low': ["slow", "low", "slowest", "lowest"], - # 'speed_medium': ["medium", "mid", "middle"], - # 'speed_high': ["high", "max", "fast", "highest", "fastest", "maximum"], - # } - - # modes = ['off', 'speed_low', 'speed_medium','speed_high'] - - # @staticmethod - # def supported(domain, features): - # """Test if state is supported.""" - # if domain in [ - # domains['fan'] - # ]: - # return features & ATTRS_FANSPEED - - # return False - - # def sync_attributes(self): - # """Return speed point and modes attributes for a sync request.""" - # modes = self.modes - # speeds = [] - # for mode in modes: - # if mode not in self.speed_synonyms: - # continue - # speed = { - # "speed_name": mode, - # "speed_values": [ - # {"speed_synonym": self.speed_synonyms.get(mode), "lang": "en"} - # ], - # } - # speeds.append(speed) - - # return { - # "availableFanSpeeds": {"speeds": speeds, "ordered": True}, - # } - - # def query_attributes(self): - # """Return speed point and modes query attributes.""" - # response = {} - - # speed = self.state.state - # if speed is not None: - # response["on"] = speed != 'Off' - # response["online"] = True - # response["currentFanSpeedSetting"] = speed.lower() - - # return response - - # def execute(self, command, params): - # """Execute an SetFanSpeed command.""" - # modes = self.modes - # protected = self.state.protected - # for key in params['fanSpeed']: - # if key in modes: - # level = str(modes.index(key) * 10) - - # url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=Set%20Level&level=' + level - - # if protected: - # url = url + '&passcode=' + configuration['Domoticz']['switchProtectionPass'] - - # r = requests.get(url, auth=CREDITS) - - # if protected: - # status = r.json() - # err = status.get('status') - # if err == 'ERROR': - # raise SmartHomeError(ERR_WRONG_PIN, - # 'Unable to execute {} for {} check your settings'.format(command, - # self.state.entity_id)) - - @register_trait class TemperatureSettingTrait(_Trait): """Trait to offer handling both temperature point and modes functionality. @@ -579,25 +496,28 @@ class TemperatureSettingTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - if domain == domains['thermostat']: + if domain == DOMAINS['thermostat']: return features & ATTRS_THERMSTATSETPOINT else: - return domain in domains['temperature'] + return domain in [DOMAINS['temperature']] def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" domain = self.state.domain units = self.state.tempunit + minThree = self.state.minThreehold + maxThree = self.state.maxThreehold + response = {"thermostatTemperatureUnit": _google_temp_unit(units)} - # response["thermostatTemperatureRange"] = { - # 'minThresholdCelsius': -20, - # 'maxThresholdCelsius': 40} + response["thermostatTemperatureRange"] = { + 'minThresholdCelsius': minThree, + 'maxThresholdCelsius': maxThree} - if domain == domains['temperature']: + if domain in [DOMAINS['temperature']]: response["queryOnlyTemperatureSetting"] = True - response["availableThermostatModes"] = 'heat' + response["availableThermostatModes"] = 'heat, cool' - if domain == domains['thermostat']: + if domain == DOMAINS['thermostat']: if self.state.modes_idx is not None: response["availableThermostatModes"] = 'off,heat,cool,auto,eco' else: @@ -613,18 +533,20 @@ def query_attributes(self): if self.state.battery <= configuration['Low_battery_limit']: response['exceptionCode'] = 'lowBattery' - if domain == domains['temperature']: - response['thermostatMode'] = 'heat' + if domain in [DOMAINS['temperature']]: current_temp = float(self.state.temp) if current_temp is not None: - test_temp = round(tempConvert(current_temp, _google_temp_unit(units)), 1) + if round(tempConvert(current_temp, _google_temp_unit(units)),1) <= 3: + response['thermostatMode'] = 'cool' + else: + response['thermostatMode'] = 'heat' response['thermostatTemperatureAmbient'] = round(tempConvert(current_temp, _google_temp_unit(units)), 1) response['thermostatTemperatureSetpoint'] = round(tempConvert(current_temp, _google_temp_unit(units)), 1) - current_humidity = self.state.humidity - if current_humidity is not None: - response['thermostatHumidityAmbient'] = current_humidity + # current_humidity = self.state.humidity + # if current_humidity is not None: + # response['thermostatHumidityAmbient'] = current_humidity - if domain == domains['thermostat']: + if domain == DOMAINS['thermostat']: if self.state.modes_idx is not None: levelName = base64.b64decode(self.state.selectorLevelName).decode('UTF-8').split("|") level = self.state.level @@ -638,12 +560,17 @@ def query_attributes(self): setpoint = float(self.state.setpoint) if setpoint is not None: response['thermostatTemperatureSetpoint'] = round(tempConvert(setpoint, _google_temp_unit(units)), 1) + current_humidity = self.state.humidity + if current_humidity is not None: + response['thermostatHumidityAmbient'] = current_humidity + return response def execute(self, command, params): """Execute a temperature point or mode command.""" # All sent in temperatures are always in Celsius + if command == COMMAND_THERMOSTAT_SET_MODE: if self.state.modes_idx is not None: levels = base64.b64decode(self.state.selectorLevelName).decode('UTF-8').split("|") @@ -694,20 +621,23 @@ class TemperatureControlTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in [domains['heater'], domains['kettle'], domains['waterheater'], domains['oven']] + return domain in [DOMAINS['heater'], DOMAINS['kettle'], DOMAINS['waterheater'], DOMAINS['oven']] def sync_attributes(self): """Return temperature point attributes for a sync request.""" domain = self.state.domain units = self.state.tempunit + minThree = -100 + maxThree = 100 response = {} + response = {"temperatureUnitForUX": _google_temp_unit(units)} + response["temperatureRange"] = { + 'minThresholdCelsius': minThree, + 'maxThresholdCelsius': maxThree} + if self.state.merge_thermo_idx is not None: - response = {"temperatureUnitForUX": _google_temp_unit(units)} response = {"temperatureStepCelsius": 1} - response["temperatureRange"] = { - 'minThresholdCelsius': 30, - 'maxThresholdCelsius': 300} - + return response def query_attributes(self): @@ -715,6 +645,7 @@ def query_attributes(self): domain = self.state.domain units = self.state.tempunit response = {} + if self.state.merge_thermo_idx is not None: if self.state.battery <= configuration['Low_battery_limit']: response['exceptionCode'] = 'lowBattery' @@ -754,8 +685,8 @@ class LockUnlockTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in (domains['lock'], - domains['lockinv']) + return domain in (DOMAINS['lock'], + DOMAINS['lockinv']) def sync_attributes(self): """Return LockUnlock attributes for a sync request.""" @@ -777,7 +708,7 @@ def execute(self, command, params): state = self.state.state protected = self.state.protected - if domain == domains['lock']: + if domain == DOMAINS['lock']: if params['lock'] == True and state == 'Unlocked': url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=On' elif params['lock'] == False and state == 'Locked': @@ -825,7 +756,7 @@ class ColorSettingTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - if domain == domains['color']: + if domain == DOMAINS['color']: return (features & ATTRS_COLOR or features & ATTRS_COLOR_TEMP) @@ -898,7 +829,7 @@ class ArmDisarmTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in domains['security'] + return domain in DOMAINS['security'] def sync_attributes(self): """Return ArmDisarm attributes for a sync request.""" @@ -990,7 +921,7 @@ class VolumeTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in domains['speaker'] + return domain in DOMAINS['speaker'] def sync_attributes(self): """Return volume attributes for a sync request.""" @@ -1030,7 +961,7 @@ def execute(self, command, params): self._execute_volume_relative(params) else: raise SmartHomeError(ERR_NOT_SUPPORTED, - 'Unable to execute {} for {} '.format(command, self.state.entity_id)) + 'Unable to execute {} for {} '.format(command, self.state.entity_id)) @register_trait @@ -1049,14 +980,15 @@ class CameraStreamTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in domains['camera'] + if configuration['Camera_Stream']['Enabled']: + return domain in [DOMAINS['camera'], DOMAINS['doorbell']] + + return False def sync_attributes(self): """Return stream attributes for a sync request.""" return { - 'cameraStreamSupportedProtocols': [ - "hls", - ], + 'cameraStreamSupportedProtocols': ['hls'], 'cameraStreamNeedAuthToken': False, 'cameraStreamNeedDrmEncryption': False, } @@ -1086,10 +1018,10 @@ class TooglesTrait(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - if domain == domains['vacuum']: + if domain == DOMAINS['vacuum']: return features & ATTRS_VACUUM_MODES else: - return domain in domains['selector'] + return domain in DOMAINS['selector'] def sync_attributes(self): """Return mode attributes for a sync request.""" @@ -1164,12 +1096,12 @@ class Timer(_Trait): @staticmethod def supported(domain, features): """Test if state is supported.""" - return domain in [domains['light'], - domains['color'], - domains['switch'], - domains['heater'], - domains['kettle'], - domains['fan'], + return domain in [DOMAINS['light'], + DOMAINS['color'], + DOMAINS['switch'], + DOMAINS['heater'], + DOMAINS['kettle'], + DOMAINS['fan'], ] def sync_attributes(self): @@ -1212,21 +1144,22 @@ class EnergyStorageTrait(_Trait): def supported(domain, features): """Test if state is supported.""" return domain in ( - domains['vacuum'], - domains['blinds'], - domains['smokedetector'], - domains['sensor'], - domains['mower'], - domains['thermostat'], - domains['temperature'] + DOMAINS['vacuum'], + DOMAINS['blinds'], + DOMAINS['smokedetector'], + DOMAINS['sensor'], + DOMAINS['mower'], + DOMAINS['thermostat'], + DOMAINS['temperature'] ) def sync_attributes(self): """Return EnergyStorge attributes for a sync request.""" battery = self.state.battery response = {} - if battery is not None or battery is not 255: + if battery is not None or battery != 255: response['queryOnlyEnergyStorage'] = True + response['isRechargeable'] = False return response @@ -1234,14 +1167,24 @@ def query_attributes(self): """Return EnergyStorge query attributes.""" battery = self.state.battery response = {} - if battery is not None or battery is not 255: - if battery <= 99: - response['capacityRemaining'] = [{ - 'unit': 'PERCENTAGE', - 'rawValue': battery - }] - else: - response['descriptiveCapacityRemaining'] = 'FULL' + if battery == 255: + return {} + if battery == 100: + descriptive_capacity_remaining = "FULL" + elif 75 <= battery < 100: + descriptive_capacity_remaining = "HIGH" + elif 50 <= battery < 75: + descriptive_capacity_remaining = "MEDIUM" + elif 25 <= battery < 50: + descriptive_capacity_remaining = "LOW" + elif 0 <= battery < 25: + descriptive_capacity_remaining = "CRITICALLY_LOW" + if battery is not None: + response['descriptiveCapacityRemaining'] = descriptive_capacity_remaining + response['capacityRemaining'] = [{ + 'unit': 'PERCENTAGE', + 'rawValue': battery + }] return response @@ -1250,7 +1193,7 @@ def execute(self, command, params): # domain = self.state.domain # protected = self.state.protected - # if domain in (domains['vacuum'], domains['mower']): + # if domain in (DOMAINS['vacuum'], DOMAINS['mower']): # url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=' + ( # 'On' if params['charge'] else 'Off') @@ -1265,3 +1208,188 @@ def execute(self, command, params): # raise SmartHomeError(ERR_WRONG_PIN, # 'Unable to execute {} for {} check your settings'.format(command, # self.state.entity_id)) + +@register_trait +class HumiditySettingTrait(_Trait): + """Trait to offer scene functionality. + https://developers.google.com/actions/smarthome/traits/scene + """ + + name = TRAIT_HUMIDITY + commands = [ + COMMAND_SET_HUMIDITY, + COMMAND_SET_HUMIDITY_RELATIVE + ] + + @staticmethod + def supported(domain, features): + """Test if state is supported.""" + if domain == DOMAINS['temperature']: + return features & ATTRS_THERMSTATSETPOINT + + def sync_attributes(self): + """Return humidity attributes for a sync request.""" + response = {} + response["humiditySetpointRange"] = { + "minPercent": 25, + "maxPercent": 75 + } + response['queryOnlyHumiditySetting'] = True + + return response + + def query_attributes(self): + """Return humidity query attributes.""" + current_humidity = self.state.humidity + response = {} + response['humidityAmbientPercent'] = current_humidity + #response['humiditySetpointPercent'] = current_humidity + + return response + + def execute(self, command, params): + """Execute a humidity command.""" + # domain = self.state.domain + # protected = self.state.protected + + # if domain == DOMAINS['humidity']: + # url = DOMOTICZ_URL + '/json.htm?type=command¶m=setsetpoint&idx=' + self.state.id + '&setpoint=' + str(params['humidity']) + + # if protected: + # url = url + '&passcode=' + configuration['Domoticz']['switchProtectionPass'] + + # r = requests.get(url, auth=CREDITS) + # if protected: + # status = r.json() + # err = status.get('status') + # if err == 'ERROR': + # raise SmartHomeError(ERR_WRONG_PIN, + # 'Unable to execute {} for {} check your settings'.format(command, + # self.state.entity_id)) + +@register_trait +class SensorStateTrait(_Trait): + """Trait to get sensor state. + https://developers.google.com/actions/smarthome/traits/sensorstate + """ + + name = TRAIT_SENSOR_STATE + commands = [] + + @staticmethod + def supported(domain, features): + """Test if state is supported.""" + return domain in [DOMAINS['smokedetector']] + + def sync_attributes(self): + """Return attributes for a sync request.""" + domain = self.state.domain + if domain == DOMAIN['smokedetector']: + return { + "sensorStatesSupported": [ + { + "name": "SmokeLevel", + "descriptiveCapabilities": { + "availableStates": [ + "smoke detected", + "no smoke detected" + ] + } + } + ] + } + + def query_attributes(self): + """Return the attributes of this trait for this entity.""" + domain = self.state.domain + if self.state.state is not None: + return { + "currentSensorStateData": [ + { + "name": "SmokeLevel", + "currentSensorState": "smoke detected", + } + ] + } + +# @register_trait +# class FanSpeedTrait(_Trait): + # """Trait to control speed of Fan. + # https://developers.google.com/actions/smarthome/traits/fanspeed + # """ + + # name = TRAIT_FANSPEED + # commands = [COMMAND_FANSPEED] + + # speed_synonyms = { + # 'off': ["stop", "off"], + # 'speed_low': ["slow", "low", "slowest", "lowest"], + # 'speed_medium': ["medium", "mid", "middle"], + # 'speed_high': ["high", "max", "fast", "highest", "fastest", "maximum"], + # } + + # modes = ['off', 'speed_low', 'speed_medium','speed_high'] + + # @staticmethod + # def supported(domain, features): + # """Test if state is supported.""" + # if domain in [ + # DOMAINS['fan'] + # ]: + # return features & ATTRS_FANSPEED + + # return False + + # def sync_attributes(self): + # """Return speed point and modes attributes for a sync request.""" + # modes = self.modes + # speeds = [] + # for mode in modes: + # if mode not in self.speed_synonyms: + # continue + # speed = { + # "speed_name": mode, + # "speed_values": [ + # {"speed_synonym": self.speed_synonyms.get(mode), "lang": "en"} + # ], + # } + # speeds.append(speed) + + # return { + # "availableFanSpeeds": {"speeds": speeds, "ordered": True}, + # } + + # def query_attributes(self): + # """Return speed point and modes query attributes.""" + # response = {} + + # speed = self.state.state + # if speed is not None: + # response["on"] = speed != 'Off' + # response["online"] = True + # response["currentFanSpeedSetting"] = speed.lower() + + # return response + + # def execute(self, command, params): + # """Execute an SetFanSpeed command.""" + # modes = self.modes + # protected = self.state.protected + # for key in params['fanSpeed']: + # if key in modes: + # level = str(modes.index(key) * 10) + + # url = DOMOTICZ_URL + '/json.htm?type=command¶m=switchlight&idx=' + self.state.id + '&switchcmd=Set%20Level&level=' + level + + # if protected: + # url = url + '&passcode=' + configuration['Domoticz']['switchProtectionPass'] + + # r = requests.get(url, auth=CREDITS) + + # if protected: + # status = r.json() + # err = status.get('status') + # if err == 'ERROR': + # raise SmartHomeError(ERR_WRONG_PIN, + # 'Unable to execute {} for {} check your settings'.format(command, + # self.state.entity_id))
IdxIdx (Entity id) Name (Nicknames) Type State
" + devicelist[i][1] + "
" + devicelist[i][1] + " (" + devicelist[i][7] + ")" + devicelist[i][0] + nicknames + "" + devicelist[i][2] + "