mirror of
https://github.com/home-assistant/core.git
synced 2025-05-20 22:07:08 +00:00

* Backend support for importing waypoints from owntracks as HA zones * Added test for Owntracks waypoints import * Backend support for importing waypoints from owntracks as HA zones * Added test for Owntracks waypoints import * Removed redundant assignment to CONF_WAYPOINT_IMPORT_USER * Fixed zone test break and code style issues * Fixed style issues * Fixed variable scope issues for entities * Fixed E302 * Do not install pip packages in tests * EventBus: return function to unlisten * Convert automation to entities with services * Refactored zone creation based on code review feedback, enhanced configuration * Added unit test to enhance waypoint_whitelist coverage * Fix JSON encoder issue in recorder * Fix tests docstring * * Improved zone naming in waypoint import * Added more test coverage for owntracks and zone * Back to 0.28.0.dev0 * Code review feedback from @pavoni * Added bitfield of features for flux_led since we are supporting effects * Host should be optional for apcupsd component (#3072) * Use voluptuous for file (#3049) * Zwave climate Bugfix: if some setpoints have different units, we should fetch the o… (#3078) * Bugfix: if some setpoints have different units, we should fetch the one that are active. * Move order of population for first time detection * Default to config if None unit_of_measurement * unit fix (#3083) * humidity slider (#3088) * If device was off target temp was null. Default to Heating setpoint (#3091) * Fix linting * Upgrade pyuserinput to 0.1.11 (#3068) * Upgrade pyowm to 2.4.0 (#3067) * improve isfile validation check (#3101) * Refactor notification titles to allow for them to be None, this also includes a change in Telegram to only include the title if it's present, and to use a Markdown parse mode for messages (#3100) * Fix broken test * rfxtrx sensor clean up * Bitcoin sensor use warning instead of error (#3103) * Use voluptuous for HDMI CEC & CONF_DEVICES constants (#3107) * Update voluptuous for nest (#3109) * Update configuration check * Extend platform * Fix for BLE device tracker (#3019) * Bug fix tracked devices * Added scan_duration configuration parameter * fix homematic climate implementation (#3114) * Allow 'None' MAC to be loaded from known_devices (#3102) * Use voluptuous for xmpp (#3127) * Use voluptuous for twitter (#3126) * Use voluptuous for Fritzbox and DDWRT (#3122) * Use Voluptuous for BT Home Hub (#3121) * Use voluptuous for syslog (#3120) * Use voluptuous for Aruba (#3119) * Use constants, update configuration check, and ordering (Pilight) (#3118) * Use contants, update configuration check, and ordering * Fix pylint issue * Migrate to voluptuous (#3113) * Fix typo (#3108) * Migrate to voluptuous (#3106) * Update voluptuous (#3104) * Climate and cover bugfix (#3097) * Avoid None comparison for zwave cover. * Just rely on unit from config for unit_of_measurement * Explicit return None * Mqtt (#11) * Explicit return None * Missing service and wrong service name defined * Mqtt state was inverted, and never triggering * Migrate to voluptuous (#3096) * Migrate to voluptuous (#3084) * Fixed Homematic cover (#3116) * Migrate to voluptuous (#3069) 🐬 * Migrate to voluptuous (#3066) 🐬 * snapcast update (#3012) * snapcast update * snapcast update * validate config * use conf constants * orvibo updates (#3006) 🐬 * Update frontend * move units to temperature for climate zwave. wrong state was sent to mqtt cove * Use voluptuous for instapush (#3132) * Use voluptuous for Octoprint (#3111) * Migrate to voluptuous * Fix pylint issues * Add missing docstrings (fix PEP257 issues) (#3098) * Add missing docstrings (fix PEP257 issues) * Finish sentence * Updated braviatv's braviarc version to 0.3.4 (#2997) * Updated braviarc version to 0.3.4 * Updated braviarc version to requirements_all.txt * Use voluptuous for Acer projector switch (#3077) 🐬 * Use voluptuous for twilio (#3134) * Use voluptuous for webostv (#3135) * Use voluptuous for Command line platforms (#2968) * Migrate to voluptuous * Fix pylint issues * Remove FIXME * Split setup test * Test with bootstrap * Remove lon and lat * Fix pylint issues * Add coinmarketcap sensor (#3064) * Migrate to voluptuous (#3142) 🐬 * Back out insteon hub and fan changes (#3062) * Move details to docs (#3146) * Update frontend * Use constants (#3148) * Update ordering (#3149) * Migrate to voluptuous (#3092) * Display the error instead of the traceback (notify.slack) (#3079) * Display the error instead of the traceback * Remove name for check * Automatic ODB device tracker & device tracker attributes (#3035) * Migrate to voluptuous (#3173) * Add voluptuous for tomato and SNMP (#3172) * Improve voluptuous and login errors for Asus device tracker (#3170) * Add exclude option to nmap device tracker (#2983) * Add exclude option to nmap device tracker Adds an optional exclude paramater to nmap device tracker. Devices specified in the exclude list will never be scanned by nmap. This can help to reduce log spam. ex: ``` device_tracker: - platform: nmap_tracker hosts: 10.0.0.1/24 home_interval: 1 interval_seconds: 12 consider_home: 120 track_new_devices: yes exclude: - 10.0.0.2 - 10.0.0.1 ``` * Handle optional exclude * Style fixed * Added Xbox Live component (#3013) * Added Xbox Live component * Added Xbox Live sensor to coveralls * Added init success checks * Added entity id * Adding link_names to post.message call (#3167) If you do not turn link_names on, Slack will not highlight @channel and @username messages. * Allow https (fixes #3150) (#3155) * Use constants (#3156) * Bugfix: ctach Runtime errors (#3153) "RuntimeError: Disable scan failed" has been seen in a live installation * Migrate to voluptuous (#3166) 🐬 * Migrate to voluptuous (#3164) 🐬 * Migrate to voluptuous (#3163) 🐬 * Migrate to voluptuous (#3162) 🐬 and 🍪 for fixing quotes! * Exclude www_static from pydocstyle linting (#3175) 🐬 * Migrate to voluptuous (#3174) * Migrate to voluptuous (#3171) * Use voluptuous for mFi switch (#3168) * Migrate to voluptuous * Take change configuration into account * Migrate to voluptuous (#3144) 🐬 * Add the occupancy sensor_class (#3176) Such a complicated PR * Update frontend * Use voluptuous for Unifi, Ubus (#3125) * Using alert with Hue maintains prior state (#3147) * When using flash with hue, dont change the on/off state of the light so that it will naturally return to its previous state once flash is complete * ATTR_FLASH not ATTR_EFFECT * MQTT fan platform (#3095) * Add fan.mqtt, allow brightness to be passed and mapped to a fan speed for compatibility with emulated_hue * Pylint/Flake8 fixes * Remove brightness * Add more features, like custom oscillation/speed payloads and setting the speed list * Flake8 fixes * flake8/pylint fixes * Use constants * block fan.mqtt from coverage * Fix oscillating comment * Add Sphinx API doc generation (#3029) * add's sphinx project to docs/ dir * include core/helpers autodocs for API reference * Allow reloading automation without restarting HA (#3002) * Migrate to voluptuous (#3182) 🐬 * Migrate to voluptuous (#3179) 🐬 * Added scale and offset to the Temper component (#2853) 🐬 * Use voluptuous for BT and Owntracks device trackers (#3187) 🐬 * Correct binary_sensor.ecobee docs URL * Use voluptuous for Hikvisioncam switch (#3184) * Migrate to voluptuous * Use vol.Optional * Use voluptuous for Edimax (#3178) 🐬 * Use voluptuous for Bravia TV (#3165) 🐬 * Added support to 'effect: random' to Osram Lightify lights (#3192) * Added support to 'effect: random' to Osram Lightify lights * removed extra line not required * Use voluptuous for message_bird, sendgrid (#3136) * Try out the RTD theme * Doc updates * Update voluptuous for existing notify platforms (#3133) * Update voluptuous for exists notify platforms * fix constants * Simple trend sensor. (#3073) * First cut of trend sensor. * Tidy. * Migrate to voluptuous (#3193) * Migrate to voluptuous (#3194) 🐬 * Migrate to voluptuous (#3197) * Migrate to voluptuous (#3198) 🐬 * Use extend of PLATFORM_SCHEMA (#3199) * Migrate to voluptuous (#3202) 🐬 * Updated to use the occupancy sensor_class (#3204) 🐬 * Migrate to voluptuous (#3206) * Migrate to voluptuous (#3207) * Migrate to voluptuous (#3208) 🐬 * Migrate to voluptuous (#3209) 🐬 * Migrate to voluptuous (#3214) * Use voluptuous for SqueezeBox (#3212) * Migrate to voluptuous * Remove name * Migrate to voluptuous and upgrade uber_rides to 0.2.5 (#3181) * Migrate to voluptuous (#3200) 🐬 * Use Voluptuous for Luci and Netgear device trackers (#3123) * Use Voluptuous for Luci and NEtgear device trackers * str_schema shortcut * Undo str_schema * change update handling with variable for breack CCU2 (#3215) * Update ordering (#3216) * Docs update * Flake8/pylint * Add new docs requirements * Update email validation (#3228) 🐬 * Fix email validation (fixes #3138) (#3227) * Upgrade slacker to 0.9.25 (#3224) * Upgrade psutil to 4.3.1 (#3223) * Upgrade gps3 to 0.33.3 (#3222) * Upgrade Werkzeug to 0.11.11 (#3220) * Upgrade sendgrid to 3.4.0 (#3226) * Bluetooth: keep looking for new devices (#3201) * keep looking for new devices * Update bluetooth_tracker.py * change default value for tracking new devices * remove commented code * dlink switch added device state attributes and support for legacy firmware (#3211) * Use voluptuous for free mobile (#3236) * Use voluptuous for nma (#3241) * Improve 1-Wire device family detection and error checking. Use volupt… (#3233) * Improve 1-Wire device family detection and error checking. Use voluptuous * Fix detection of gpio connected devices * Replace rollershutter and garage door with cover, add fan (#3242) * Use voluptuous for Alarm.com (#3229) * Use voluptuous for gntp (#3237) * Use voluptuous for pushbullet, pushetta and pushover (#3240) * Migrate to voluptuous (#3230) 🐬 * Fix mFi sensors in uninitialized state (#3246) If mFi sensors are identified but not fully assigned they can have no tag value, and mficlient throws a ValueError to signal this. This patch handles that case by considering such devices to always be STATE_OFF. * Use voluptuous for PulseAudio Loopback (#3160) * Migrate to voluptuous * Fix conf var * Use voluptuous for Verisure (#3169) * Migrate to voluptuous * Update type and add missing config variable * thread safe modbus (#3188) * Upgraded fitbit to version 0.2.3 which fixed oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired) (#3244) * update ffmpeg version to 0.10 add get image to camera (#3235) * Migrate to voluptuous (#3234) * fix bugfix with unique_id (#3217) * Zwave climate fix and wink cover. (#3205) * Fixes setpoint get was done outside loop * zxt_120 * Wink not migrated to cover * Clarifying debug * too long line * Only add 1 device entity * Owntracks voluptuous fix (#3191) * Zwave set temperature fix (#3221) * If device was off set target temp would not work. * Changed to use a workaround just for Horstmann HRT4-ZW Zwave Thermostat * Wrong Horseman id * style changes * Change PR to suggestion on gitter (#3243) * Reload groups (#3203) * Allow reloading groups without restart * Test to make sure automation listeners are removed. * Remove unused imports for group tests * Simplify group config validation * Add prepare_reload function to entity component * Migrate group to use entity_component.prepare_reload * Migrate automation to use entity_component.prepare_reload * Clean up group.get_entity_ids * Use cv.boolean for group config validation * fix remove listener (#3196) * Add linux battery sensor (#3238) * protect service data for changes in calls (#3249) * protect service data for changes in calls * change handling * move MappingProxyType to service call * Fix issue #3250 (#3253) * Minor Ecobee changes (#3131) * Update configuration check, ordering, and constants * Make API key optional * issue #3250 * Add voluptuous to ecobee (#3257) * Use constants and update ordering (#3261) * Add support for complex template structures to data_template (#3255) * Improve yaml fault tolerance and handle check_config border cases (#3159) * Use voluptuous for nx584 alarm (#3231) * Migrate to voluptuous * Fix pylint issue * fastdotcom from pypi (#3269) * Use constants and update ordering (#3268) 🐬 * Use constants and update ordering (#3267) 🐬 * Add additional template for custom date formats (#3262) I can live with a few visual line breaks 🐬 * Use constants and update ordering (#3266) * Updated braviatv's braviarc version to 0.3.5 (#3271) * Use voluptuous for Device Sun Light Trigger (#3105) * Migrate to voluptuous * Use default * Point to master till archive is back (#3285) * Pi-Hole statistics sensor (#3158) * Add Pi-Hole sensor * Update docstrings and remove print() * Use None for payload * Added stuff for support range setting (#3189) * cleanup Homematic code (#3291) * cleanup old code * cleanup round 2 * remove unwanted platforms * Update frontend * Hotfix for #3100 (#3302) * Fix TP-Link Archer C7 long passwords (#3225) * Fix tplink C7 long passwords Fixes an issue where passwords longer than 15 chars could not log in to Archer C7 routers. * Truncate in correct place * Add comment about TP-Link C7 pass truncation * Fix lint error * Truncate comment at 79 chars not 80 * modbus write registers service (#3252) * Fix bloomsky platform discovery (#3303) * Remove dev tag
809 lines
27 KiB
Python
809 lines
27 KiB
Python
"""
|
|
Support for Homematic devices.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/homematic/
|
|
"""
|
|
import os
|
|
import time
|
|
import logging
|
|
from datetime import timedelta
|
|
from functools import partial
|
|
|
|
import voluptuous as vol
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
|
|
CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
|
|
ATTR_ENTITY_ID)
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.helpers import discovery
|
|
from homeassistant.config import load_yaml_config_file
|
|
from homeassistant.util import Throttle
|
|
|
|
DOMAIN = 'homematic'
|
|
REQUIREMENTS = ["pyhomematic==0.1.13"]
|
|
|
|
HOMEMATIC = None
|
|
HOMEMATIC_LINK_DELAY = 0.5
|
|
|
|
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
|
|
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60)
|
|
|
|
DISCOVER_SWITCHES = 'homematic.switch'
|
|
DISCOVER_LIGHTS = 'homematic.light'
|
|
DISCOVER_SENSORS = 'homematic.sensor'
|
|
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
|
|
DISCOVER_COVER = 'homematic.cover'
|
|
DISCOVER_CLIMATE = 'homematic.climate'
|
|
|
|
ATTR_DISCOVER_DEVICES = 'devices'
|
|
ATTR_PARAM = 'param'
|
|
ATTR_CHANNEL = 'channel'
|
|
ATTR_NAME = 'name'
|
|
ATTR_ADDRESS = 'address'
|
|
ATTR_VALUE = 'value'
|
|
|
|
EVENT_KEYPRESS = 'homematic.keypress'
|
|
EVENT_IMPULSE = 'homematic.impulse'
|
|
|
|
SERVICE_VIRTUALKEY = 'virtualkey'
|
|
SERVICE_SET_VALUE = 'set_value'
|
|
|
|
HM_DEVICE_TYPES = {
|
|
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
|
|
DISCOVER_LIGHTS: ['Dimmer'],
|
|
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
|
|
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
|
|
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
|
|
'LuxSensor', 'WeatherSensor', 'WeatherStation'],
|
|
DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
|
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
|
|
'MotionV2', 'RemoteMotion', 'WeatherSensor',
|
|
'TiltSensor'],
|
|
DISCOVER_COVER: ['Blind']
|
|
}
|
|
|
|
HM_IGNORE_DISCOVERY_NODE = [
|
|
'ACTUAL_TEMPERATURE',
|
|
'ACTUAL_HUMIDITY'
|
|
]
|
|
|
|
HM_ATTRIBUTE_SUPPORT = {
|
|
'LOWBAT': ['Battery', {0: 'High', 1: 'Low'}],
|
|
'ERROR': ['Sabotage', {0: 'No', 1: 'Yes'}],
|
|
'RSSI_DEVICE': ['RSSI', {}],
|
|
'VALVE_STATE': ['Valve', {}],
|
|
'BATTERY_STATE': ['Battery', {}],
|
|
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
|
|
'POWER': ['Power', {}],
|
|
'CURRENT': ['Current', {}],
|
|
'VOLTAGE': ['Voltage', {}],
|
|
'WORKING': ['Working', {0: 'No', 1: 'Yes'}],
|
|
}
|
|
|
|
HM_PRESS_EVENTS = [
|
|
'PRESS_SHORT',
|
|
'PRESS_LONG',
|
|
'PRESS_CONT',
|
|
'PRESS_LONG_RELEASE'
|
|
]
|
|
|
|
HM_IMPULSE_EVENTS = [
|
|
'SEQUENCE_OK'
|
|
]
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_RESOLVENAMES_OPTIONS = [
|
|
'metadata',
|
|
'json',
|
|
'xml',
|
|
False
|
|
]
|
|
|
|
CONF_LOCAL_IP = 'local_ip'
|
|
CONF_LOCAL_PORT = 'local_port'
|
|
CONF_REMOTE_IP = 'remote_ip'
|
|
CONF_REMOTE_PORT = 'remote_port'
|
|
CONF_RESOLVENAMES = 'resolvenames'
|
|
CONF_DELAY = 'delay'
|
|
CONF_VARIABLES = 'variables'
|
|
|
|
|
|
DEVICE_SCHEMA = vol.Schema({
|
|
vol.Required(CONF_PLATFORM): "homematic",
|
|
vol.Required(ATTR_NAME): cv.string,
|
|
vol.Required(ATTR_ADDRESS): cv.string,
|
|
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
|
vol.Optional(ATTR_PARAM): cv.string,
|
|
})
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: vol.Schema({
|
|
vol.Required(CONF_LOCAL_IP): cv.string,
|
|
vol.Optional(CONF_LOCAL_PORT, default=8943): cv.port,
|
|
vol.Required(CONF_REMOTE_IP): cv.string,
|
|
vol.Optional(CONF_REMOTE_PORT, default=2001): cv.port,
|
|
vol.Optional(CONF_RESOLVENAMES, default=False):
|
|
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
|
vol.Optional(CONF_USERNAME, default="Admin"): cv.string,
|
|
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
|
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float),
|
|
vol.Optional(CONF_VARIABLES, default=False): cv.boolean,
|
|
}),
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
|
|
vol.Required(ATTR_ADDRESS): cv.string,
|
|
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
|
vol.Required(ATTR_PARAM): cv.string,
|
|
})
|
|
|
|
SCHEMA_SERVICE_SET_VALUE = vol.Schema({
|
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
|
vol.Required(ATTR_VALUE): cv.match_all,
|
|
})
|
|
|
|
|
|
def virtualkey(hass, address, channel, param):
|
|
"""Send virtual keypress to homematic controlller."""
|
|
data = {
|
|
ATTR_ADDRESS: address,
|
|
ATTR_CHANNEL: channel,
|
|
ATTR_PARAM: param,
|
|
}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
|
|
|
|
|
|
def set_value(hass, entity_id, value):
|
|
"""Change value of homematic system variable."""
|
|
data = {
|
|
ATTR_ENTITY_ID: entity_id,
|
|
ATTR_VALUE: value,
|
|
}
|
|
|
|
hass.services.call(DOMAIN, SERVICE_SET_VALUE, data)
|
|
|
|
|
|
# pylint: disable=unused-argument,too-many-locals
|
|
def setup(hass, config):
|
|
"""Setup the Homematic component."""
|
|
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
|
from pyhomematic import HMConnection
|
|
|
|
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
|
|
|
local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
|
|
local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
|
|
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
|
|
remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
|
|
resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
|
|
username = config[DOMAIN].get(CONF_USERNAME)
|
|
password = config[DOMAIN].get(CONF_PASSWORD)
|
|
HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY)
|
|
use_variables = config[DOMAIN].get(CONF_VARIABLES)
|
|
|
|
if remote_ip is None or local_ip is None:
|
|
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
|
return False
|
|
|
|
# Create server thread
|
|
bound_system_callback = partial(_system_callback_handler, hass, config)
|
|
HOMEMATIC = HMConnection(local=local_ip,
|
|
localport=local_port,
|
|
remote=remote_ip,
|
|
remoteport=remote_port,
|
|
systemcallback=bound_system_callback,
|
|
resolvenames=resolvenames,
|
|
rpcusername=username,
|
|
rpcpassword=password,
|
|
interface_id="homeassistant")
|
|
|
|
# Start server thread, connect to peer, initialize to receive events
|
|
HOMEMATIC.start()
|
|
|
|
# Stops server when Homeassistant is shutting down
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
|
|
hass.config.components.append(DOMAIN)
|
|
|
|
# regeister homematic services
|
|
descriptions = load_yaml_config_file(
|
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
|
|
|
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
|
_hm_service_virtualkey,
|
|
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
|
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
|
|
|
entities = []
|
|
|
|
##
|
|
# init HM variable
|
|
variables = HOMEMATIC.getAllSystemVariables() if use_variables else {}
|
|
hm_var_store = {}
|
|
if variables is not None:
|
|
for key, value in variables.items():
|
|
varia = HMVariable(key, value)
|
|
hm_var_store.update({key: varia})
|
|
entities.append(varia)
|
|
|
|
# add homematic entites
|
|
entities.append(HMHub(hm_var_store, use_variables))
|
|
component.add_entities(entities)
|
|
|
|
##
|
|
# register set_value service if exists variables
|
|
if not variables:
|
|
return True
|
|
|
|
def _service_handle_value(service):
|
|
"""Set value on homematic variable object."""
|
|
variable_list = component.extract_from_service(service)
|
|
|
|
value = service.data[ATTR_VALUE]
|
|
|
|
for hm_variable in variable_list:
|
|
hm_variable.hm_set(value)
|
|
|
|
hass.services.register(DOMAIN, SERVICE_SET_VALUE,
|
|
_service_handle_value,
|
|
descriptions[DOMAIN][SERVICE_SET_VALUE],
|
|
schema=SCHEMA_SERVICE_SET_VALUE)
|
|
|
|
return True
|
|
|
|
|
|
# pylint: disable=too-many-branches
|
|
def _system_callback_handler(hass, config, src, *args):
|
|
"""Callback handler."""
|
|
if src == 'newDevices':
|
|
_LOGGER.debug("newDevices with: %s", str(args))
|
|
# pylint: disable=unused-variable
|
|
(interface_id, dev_descriptions) = args
|
|
key_dict = {}
|
|
# Get list of all keys of the devices (ignoring channels)
|
|
for dev in dev_descriptions:
|
|
key_dict[dev['ADDRESS'].split(':')[0]] = True
|
|
|
|
# Register EVENTS
|
|
# Search all device with a EVENTNODE that include data
|
|
bound_event_callback = partial(_hm_event_handler, hass)
|
|
for dev in key_dict:
|
|
if dev not in HOMEMATIC.devices:
|
|
continue
|
|
|
|
hmdevice = HOMEMATIC.devices.get(dev)
|
|
# have events?
|
|
if len(hmdevice.EVENTNODE) > 0:
|
|
_LOGGER.debug("Register Events from %s", dev)
|
|
hmdevice.setEventCallback(callback=bound_event_callback,
|
|
bequeath=True)
|
|
|
|
# If configuration allows autodetection of devices,
|
|
# all devices not configured are added.
|
|
if key_dict:
|
|
for component_name, discovery_type in (
|
|
('switch', DISCOVER_SWITCHES),
|
|
('light', DISCOVER_LIGHTS),
|
|
('cover', DISCOVER_COVER),
|
|
('binary_sensor', DISCOVER_BINARY_SENSORS),
|
|
('sensor', DISCOVER_SENSORS),
|
|
('climate', DISCOVER_CLIMATE)):
|
|
# Get all devices of a specific type
|
|
found_devices = _get_devices(discovery_type, key_dict)
|
|
|
|
# When devices of this type are found
|
|
# they are setup in HA and an event is fired
|
|
if found_devices:
|
|
# Fire discovery event
|
|
discovery.load_platform(hass, component_name, DOMAIN, {
|
|
ATTR_DISCOVER_DEVICES: found_devices
|
|
}, config)
|
|
|
|
|
|
def _get_devices(device_type, keys):
|
|
"""Get the Homematic devices."""
|
|
device_arr = []
|
|
|
|
# pylint: disable=too-many-nested-blocks
|
|
for key in keys:
|
|
device = HOMEMATIC.devices[key]
|
|
class_name = device.__class__.__name__
|
|
metadata = {}
|
|
|
|
# is class supported by discovery type
|
|
if class_name not in HM_DEVICE_TYPES[device_type]:
|
|
continue
|
|
|
|
# Load metadata if needed to generate a param list
|
|
if device_type == DISCOVER_SENSORS:
|
|
metadata.update(device.SENSORNODE)
|
|
elif device_type == DISCOVER_BINARY_SENSORS:
|
|
metadata.update(device.BINARYNODE)
|
|
|
|
params = _create_params_list(device, metadata, device_type)
|
|
if params:
|
|
# Generate options for 1...n elements with 1...n params
|
|
for channel in range(1, device.ELEMENT + 1):
|
|
_LOGGER.debug("Handling %s:%i", key, channel)
|
|
if channel in params:
|
|
for param in params[channel]:
|
|
name = _create_ha_name(
|
|
name=device.NAME,
|
|
channel=channel,
|
|
param=param
|
|
)
|
|
device_dict = {
|
|
CONF_PLATFORM: "homematic",
|
|
ATTR_ADDRESS: key,
|
|
ATTR_NAME: name,
|
|
ATTR_CHANNEL: channel
|
|
}
|
|
if param is not None:
|
|
device_dict.update({ATTR_PARAM: param})
|
|
|
|
# Add new device
|
|
try:
|
|
DEVICE_SCHEMA(device_dict)
|
|
device_arr.append(device_dict)
|
|
except vol.MultipleInvalid as err:
|
|
_LOGGER.error("Invalid device config: %s",
|
|
str(err))
|
|
else:
|
|
_LOGGER.debug("Channel %i not in params", channel)
|
|
else:
|
|
_LOGGER.debug("Got no params for %s", key)
|
|
_LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr))
|
|
return device_arr
|
|
|
|
|
|
def _create_params_list(hmdevice, metadata, device_type):
|
|
"""Create a list from HMDevice with all possible parameters in config."""
|
|
params = {}
|
|
merge = False
|
|
|
|
# use merge?
|
|
if device_type in (DISCOVER_SENSORS, DISCOVER_BINARY_SENSORS):
|
|
merge = True
|
|
|
|
# Search in sensor and binary metadata per elements
|
|
for channel in range(1, hmdevice.ELEMENT + 1):
|
|
param_chan = []
|
|
for node, meta_chan in metadata.items():
|
|
try:
|
|
# Is this attribute ignored?
|
|
if node in HM_IGNORE_DISCOVERY_NODE:
|
|
continue
|
|
if meta_chan == 'c' or meta_chan is None:
|
|
# Only channel linked data
|
|
param_chan.append(node)
|
|
elif channel == 1:
|
|
# First channel can have other data channel
|
|
param_chan.append(node)
|
|
except (TypeError, ValueError):
|
|
_LOGGER.error("Exception generating %s (%s)",
|
|
hmdevice.ADDRESS, str(metadata))
|
|
|
|
# default parameter is merge is off
|
|
if len(param_chan) == 0 and not merge:
|
|
param_chan.append(None)
|
|
|
|
# Add to channel
|
|
if len(param_chan) > 0:
|
|
params.update({channel: param_chan})
|
|
|
|
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
|
|
str(params))
|
|
return params
|
|
|
|
|
|
def _create_ha_name(name, channel, param):
|
|
"""Generate a unique object name."""
|
|
# HMDevice is a simple device
|
|
if channel == 1 and param is None:
|
|
return name
|
|
|
|
# Has multiple elements/channels
|
|
if channel > 1 and param is None:
|
|
return "{} {}".format(name, channel)
|
|
|
|
# With multiple param first elements
|
|
if channel == 1 and param is not None:
|
|
return "{} {}".format(name, param)
|
|
|
|
# Multiple param on object with multiple elements
|
|
if channel > 1 and param is not None:
|
|
return "{} {} {}".format(name, channel, param)
|
|
|
|
|
|
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
|
|
add_callback_devices):
|
|
"""Helper to setup Homematic devices with discovery info."""
|
|
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
|
_LOGGER.debug("Add device %s from config: %s",
|
|
str(hmdevicetype), str(config))
|
|
|
|
# create object and add to HA
|
|
new_device = hmdevicetype(config)
|
|
new_device.link_homematic()
|
|
|
|
add_callback_devices([new_device])
|
|
|
|
return True
|
|
|
|
|
|
def _hm_event_handler(hass, device, caller, attribute, value):
|
|
"""Handle all pyhomematic device events."""
|
|
try:
|
|
channel = int(device.split(":")[1])
|
|
address = device.split(":")[0]
|
|
hmdevice = HOMEMATIC.devices.get(address)
|
|
except (TypeError, ValueError):
|
|
_LOGGER.error("Event handling channel convert error!")
|
|
return
|
|
|
|
# is not a event?
|
|
if attribute not in hmdevice.EVENTNODE:
|
|
return
|
|
|
|
_LOGGER.debug("Event %s for %s channel %i", attribute,
|
|
hmdevice.NAME, channel)
|
|
|
|
# keypress event
|
|
if attribute in HM_PRESS_EVENTS:
|
|
hass.bus.fire(EVENT_KEYPRESS, {
|
|
ATTR_NAME: hmdevice.NAME,
|
|
ATTR_PARAM: attribute,
|
|
ATTR_CHANNEL: channel
|
|
})
|
|
return
|
|
|
|
# impulse event
|
|
if attribute in HM_IMPULSE_EVENTS:
|
|
hass.bus.fire(EVENT_KEYPRESS, {
|
|
ATTR_NAME: hmdevice.NAME,
|
|
ATTR_CHANNEL: channel
|
|
})
|
|
return
|
|
|
|
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
|
|
|
|
|
def _hm_service_virtualkey(call):
|
|
"""Callback for handle virtualkey services."""
|
|
address = call.data.get(ATTR_ADDRESS)
|
|
channel = call.data.get(ATTR_CHANNEL)
|
|
param = call.data.get(ATTR_PARAM)
|
|
|
|
if address not in HOMEMATIC.devices:
|
|
_LOGGER.error("%s not found for service virtualkey!", address)
|
|
return
|
|
hmdevice = HOMEMATIC.devices.get(address)
|
|
|
|
# if param exists for this device
|
|
if param not in hmdevice.ACTIONNODE:
|
|
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
|
return
|
|
|
|
# channel exists?
|
|
if channel > hmdevice.ELEMENT:
|
|
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
|
return
|
|
|
|
# call key
|
|
hmdevice.actionNodeData(param, 1, channel)
|
|
|
|
|
|
class HMHub(Entity):
|
|
"""The Homematic hub. I.e. CCU2/HomeGear."""
|
|
|
|
def __init__(self, variables_store, use_variables=False):
|
|
"""Initialize Homematic hub."""
|
|
self._state = STATE_UNKNOWN
|
|
self._store = variables_store
|
|
self._use_variables = use_variables
|
|
|
|
self.update()
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return 'Homematic'
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the entity."""
|
|
return self._state
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return device specific state attributes."""
|
|
return {}
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return the icon to use in the frontend, if any."""
|
|
return "mdi:gradient"
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true if device is available."""
|
|
return True if HOMEMATIC is not None else False
|
|
|
|
def update(self):
|
|
"""Update Hub data and all HM variables."""
|
|
self._update_hub_state()
|
|
self._update_variables_state()
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
|
|
def _update_hub_state(self):
|
|
"""Retrieve latest state."""
|
|
if HOMEMATIC is None:
|
|
return
|
|
state = HOMEMATIC.getServiceMessages()
|
|
self._state = STATE_UNKNOWN if state is None else len(state)
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
|
|
def _update_variables_state(self):
|
|
"""Retrive all variable data and update hmvariable states."""
|
|
if HOMEMATIC is None or not self._use_variables:
|
|
return
|
|
variables = HOMEMATIC.getAllSystemVariables()
|
|
if variables is not None:
|
|
for key, value in variables.items():
|
|
if key in self._store:
|
|
self._store.get(key).hm_update(value)
|
|
|
|
|
|
class HMVariable(Entity):
|
|
"""The Homematic system variable."""
|
|
|
|
def __init__(self, name, state):
|
|
"""Initialize Homematic hub."""
|
|
self._state = state
|
|
self._name = name
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the entity."""
|
|
return self._state
|
|
|
|
@property
|
|
def icon(self):
|
|
"""Return the icon to use in the frontend, if any."""
|
|
return "mdi:code-string"
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return false. Homematic Hub object update variable."""
|
|
return False
|
|
|
|
def hm_update(self, value):
|
|
"""Update variable over Hub object."""
|
|
if value != self._state:
|
|
self._state = value
|
|
self.update_ha_state()
|
|
|
|
def hm_set(self, value):
|
|
"""Set variable on homematic controller."""
|
|
if HOMEMATIC is not None:
|
|
if isinstance(self._state, bool):
|
|
value = cv.boolean(value)
|
|
else:
|
|
value = float(value)
|
|
HOMEMATIC.setSystemVariable(self._name, value)
|
|
self._state = value
|
|
self.update_ha_state()
|
|
|
|
|
|
class HMDevice(Entity):
|
|
"""The Homematic device base object."""
|
|
|
|
# pylint: disable=too-many-instance-attributes
|
|
def __init__(self, config):
|
|
"""Initialize a generic Homematic device."""
|
|
self._name = config.get(ATTR_NAME)
|
|
self._address = config.get(ATTR_ADDRESS)
|
|
self._channel = config.get(ATTR_CHANNEL)
|
|
self._state = config.get(ATTR_PARAM)
|
|
self._data = {}
|
|
self._hmdevice = None
|
|
self._connected = False
|
|
self._available = False
|
|
|
|
# Set param to uppercase
|
|
if self._state:
|
|
self._state = self._state.upper()
|
|
|
|
# Generate name
|
|
if not self._name:
|
|
self._name = _create_ha_name(name=self._address,
|
|
channel=self._channel,
|
|
param=self._state)
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return false. Homematic states are pushed by the XML RPC Server."""
|
|
return False
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def assumed_state(self):
|
|
"""Return true if unable to access real state of the device."""
|
|
return not self._available
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true if device is available."""
|
|
return self._available
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return device specific state attributes."""
|
|
attr = {}
|
|
|
|
# no data available to create
|
|
if not self.available:
|
|
return attr
|
|
|
|
# Generate an attributes list
|
|
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
|
# Is an attributes and exists for this object
|
|
if node in self._data:
|
|
value = data[1].get(self._data[node], self._data[node])
|
|
attr[data[0]] = value
|
|
|
|
# static attributes
|
|
attr['ID'] = self._hmdevice.ADDRESS
|
|
|
|
return attr
|
|
|
|
def link_homematic(self):
|
|
"""Connect to Homematic."""
|
|
# device is already linked
|
|
if self._connected:
|
|
return True
|
|
|
|
# pyhomematic is loaded
|
|
if HOMEMATIC is None:
|
|
return False
|
|
|
|
# Does a HMDevice from pyhomematic exist?
|
|
if self._address in HOMEMATIC.devices:
|
|
# Init
|
|
self._hmdevice = HOMEMATIC.devices[self._address]
|
|
self._connected = True
|
|
|
|
# Check if Homematic class is okay for HA class
|
|
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
|
try:
|
|
# Init datapoints of this object
|
|
self._init_data()
|
|
if HOMEMATIC_LINK_DELAY:
|
|
# We delay / pause loading of data to avoid overloading
|
|
# of CCU / Homegear when doing auto detection
|
|
time.sleep(HOMEMATIC_LINK_DELAY)
|
|
self._load_data_from_hm()
|
|
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
|
|
|
# Link events from pyhomatic
|
|
self._subscribe_homematic_events()
|
|
self._available = not self._hmdevice.UNREACH
|
|
_LOGGER.debug("%s linking done", self._name)
|
|
# pylint: disable=broad-except
|
|
except Exception as err:
|
|
self._connected = False
|
|
_LOGGER.error("Exception while linking %s: %s",
|
|
self._address, str(err))
|
|
else:
|
|
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
|
|
|
def _hm_event_callback(self, device, caller, attribute, value):
|
|
"""Handle all pyhomematic device events."""
|
|
_LOGGER.debug("%s received event '%s' value: %s", self._name,
|
|
attribute, value)
|
|
have_change = False
|
|
|
|
# Is data needed for this instance?
|
|
if attribute in self._data:
|
|
# Did data change?
|
|
if self._data[attribute] != value:
|
|
self._data[attribute] = value
|
|
have_change = True
|
|
|
|
# If available it has changed
|
|
if attribute is 'UNREACH':
|
|
self._available = bool(value)
|
|
have_change = True
|
|
|
|
# If it has changed data point, update HA
|
|
if have_change:
|
|
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
|
attribute)
|
|
self.update_ha_state()
|
|
|
|
def _subscribe_homematic_events(self):
|
|
"""Subscribe all required events to handle job."""
|
|
channels_to_sub = {}
|
|
|
|
# Push data to channels_to_sub from hmdevice metadata
|
|
for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE,
|
|
self._hmdevice.ATTRIBUTENODE,
|
|
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
|
self._hmdevice.ACTIONNODE):
|
|
for node, channel in metadata.items():
|
|
# Data is needed for this instance
|
|
if node in self._data:
|
|
# chan is current channel
|
|
if channel == 'c' or channel is None:
|
|
channel = self._channel
|
|
# Prepare for subscription
|
|
try:
|
|
if int(channel) >= 0:
|
|
channels_to_sub.update({int(channel): True})
|
|
except (ValueError, TypeError):
|
|
_LOGGER("Invalid channel in metadata from %s",
|
|
self._name)
|
|
|
|
# Set callbacks
|
|
for channel in channels_to_sub:
|
|
_LOGGER.debug("Subscribe channel %s from %s",
|
|
str(channel), self._name)
|
|
self._hmdevice.setEventCallback(callback=self._hm_event_callback,
|
|
bequeath=False,
|
|
channel=channel)
|
|
|
|
def _load_data_from_hm(self):
|
|
"""Load first value from pyhomematic."""
|
|
if not self._connected:
|
|
return False
|
|
|
|
# Read data from pyhomematic
|
|
for metadata, funct in (
|
|
(self._hmdevice.ATTRIBUTENODE,
|
|
self._hmdevice.getAttributeData),
|
|
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
|
|
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
|
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)):
|
|
for node in metadata:
|
|
if node in self._data:
|
|
self._data[node] = funct(name=node, channel=self._channel)
|
|
|
|
return True
|
|
|
|
def _hm_set_state(self, value):
|
|
"""Set data to main datapoint."""
|
|
if self._state in self._data:
|
|
self._data[self._state] = value
|
|
|
|
def _hm_get_state(self):
|
|
"""Get data from main datapoint."""
|
|
if self._state in self._data:
|
|
return self._data[self._state]
|
|
return None
|
|
|
|
def _init_data(self):
|
|
"""Generate a data dict (self._data) from the Homematic metadata."""
|
|
# Add all attributes to data dict
|
|
for data_note in self._hmdevice.ATTRIBUTENODE:
|
|
self._data.update({data_note: STATE_UNKNOWN})
|
|
|
|
# init device specified data
|
|
self._init_data_struct()
|
|
|
|
def _init_data_struct(self):
|
|
"""Generate a data dict from the Homematic device metadata."""
|
|
raise NotImplementedError
|