mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 16:17:20 +00:00
commit
8b6a94b0f5
10
.coveragerc
10
.coveragerc
@ -28,6 +28,9 @@ omit =
|
|||||||
homeassistant/components/envisalink.py
|
homeassistant/components/envisalink.py
|
||||||
homeassistant/components/*/envisalink.py
|
homeassistant/components/*/envisalink.py
|
||||||
|
|
||||||
|
homeassistant/components/google.py
|
||||||
|
homeassistant/components/*/google.py
|
||||||
|
|
||||||
homeassistant/components/insteon_hub.py
|
homeassistant/components/insteon_hub.py
|
||||||
homeassistant/components/*/insteon_hub.py
|
homeassistant/components/*/insteon_hub.py
|
||||||
|
|
||||||
@ -98,6 +101,9 @@ omit =
|
|||||||
homeassistant/components/netatmo.py
|
homeassistant/components/netatmo.py
|
||||||
homeassistant/components/*/netatmo.py
|
homeassistant/components/*/netatmo.py
|
||||||
|
|
||||||
|
homeassistant/components/neato.py
|
||||||
|
homeassistant/components/*/neato.py
|
||||||
|
|
||||||
homeassistant/components/homematic.py
|
homeassistant/components/homematic.py
|
||||||
homeassistant/components/*/homematic.py
|
homeassistant/components/*/homematic.py
|
||||||
|
|
||||||
@ -144,12 +150,14 @@ omit =
|
|||||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||||
|
homeassistant/components/device_tracker/cisco_ios.py
|
||||||
homeassistant/components/device_tracker/fritz.py
|
homeassistant/components/device_tracker/fritz.py
|
||||||
homeassistant/components/device_tracker/icloud.py
|
homeassistant/components/device_tracker/icloud.py
|
||||||
homeassistant/components/device_tracker/luci.py
|
homeassistant/components/device_tracker/luci.py
|
||||||
homeassistant/components/device_tracker/netgear.py
|
homeassistant/components/device_tracker/netgear.py
|
||||||
homeassistant/components/device_tracker/nmap_tracker.py
|
homeassistant/components/device_tracker/nmap_tracker.py
|
||||||
homeassistant/components/device_tracker/snmp.py
|
homeassistant/components/device_tracker/snmp.py
|
||||||
|
homeassistant/components/device_tracker/swisscom.py
|
||||||
homeassistant/components/device_tracker/thomson.py
|
homeassistant/components/device_tracker/thomson.py
|
||||||
homeassistant/components/device_tracker/tomato.py
|
homeassistant/components/device_tracker/tomato.py
|
||||||
homeassistant/components/device_tracker/tplink.py
|
homeassistant/components/device_tracker/tplink.py
|
||||||
@ -278,6 +286,7 @@ omit =
|
|||||||
homeassistant/components/sensor/openweathermap.py
|
homeassistant/components/sensor/openweathermap.py
|
||||||
homeassistant/components/sensor/pi_hole.py
|
homeassistant/components/sensor/pi_hole.py
|
||||||
homeassistant/components/sensor/plex.py
|
homeassistant/components/sensor/plex.py
|
||||||
|
homeassistant/components/sensor/pvoutput.py
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
homeassistant/components/sensor/scrape.py
|
homeassistant/components/sensor/scrape.py
|
||||||
homeassistant/components/sensor/serial_pm.py
|
homeassistant/components/sensor/serial_pm.py
|
||||||
@ -306,7 +315,6 @@ omit =
|
|||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
homeassistant/components/switch/hikvisioncam.py
|
homeassistant/components/switch/hikvisioncam.py
|
||||||
homeassistant/components/switch/mystrom.py
|
homeassistant/components/switch/mystrom.py
|
||||||
homeassistant/components/switch/neato.py
|
|
||||||
homeassistant/components/switch/netio.py
|
homeassistant/components/switch/netio.py
|
||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
homeassistant/components/switch/pilight.py
|
homeassistant/components/switch/pilight.py
|
||||||
|
2
.hound.yml
Normal file
2
.hound.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
python:
|
||||||
|
enabled: true
|
@ -4,7 +4,7 @@ import logging
|
|||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from collections import defaultdict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Optional, Dict
|
from typing import Any, Optional, Dict
|
||||||
@ -57,7 +57,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str,
|
|||||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
config = defaultdict(dict)
|
config = {}
|
||||||
|
|
||||||
components = loader.load_order_component(domain)
|
components = loader.load_order_component(domain)
|
||||||
|
|
||||||
@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant,
|
|||||||
async_comp = hasattr(component, 'async_setup')
|
async_comp = hasattr(component, 'async_setup')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
_LOGGER.info("Setting up %s", domain)
|
||||||
if async_comp:
|
if async_comp:
|
||||||
result = yield from component.async_setup(hass, config)
|
result = yield from component.async_setup(hass, config)
|
||||||
else:
|
else:
|
||||||
@ -165,15 +166,6 @@ def _async_setup_component(hass: core.HomeAssistant,
|
|||||||
|
|
||||||
hass.config.components.append(component.DOMAIN)
|
hass.config.components.append(component.DOMAIN)
|
||||||
|
|
||||||
# Assumption: if a component does not depend on groups
|
|
||||||
# it communicates with devices
|
|
||||||
if (not async_comp and
|
|
||||||
'group' not in getattr(component, 'DEPENDENCIES', [])):
|
|
||||||
if hass.pool is None:
|
|
||||||
hass.async_init_pool()
|
|
||||||
if hass.pool.worker_count <= 10:
|
|
||||||
hass.pool.add_worker()
|
|
||||||
|
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||||
)
|
)
|
||||||
@ -353,7 +345,7 @@ def from_config_dict(config: Dict[str, Any],
|
|||||||
|
|
||||||
# run task
|
# run task
|
||||||
future = asyncio.Future(loop=hass.loop)
|
future = asyncio.Future(loop=hass.loop)
|
||||||
hass.loop.create_task(_async_init_from_config_dict(future))
|
hass.async_add_job(_async_init_from_config_dict(future))
|
||||||
hass.loop.run_until_complete(future)
|
hass.loop.run_until_complete(future)
|
||||||
|
|
||||||
return future.result()
|
return future.result()
|
||||||
@ -373,6 +365,12 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
Dynamically loads required components and its dependencies.
|
Dynamically loads required components and its dependencies.
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
|
setup_lock = hass.data.get('setup_lock')
|
||||||
|
if setup_lock is None:
|
||||||
|
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
|
||||||
|
|
||||||
|
yield from setup_lock.acquire()
|
||||||
|
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -396,10 +394,12 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
yield from hass.loop.run_in_executor(None, loader.prepare, hass)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
# Convert it to defaultdict so components can always have config dict
|
# Use OrderedDict in case original one was one.
|
||||||
# Convert values to dictionaries if they are None
|
# Convert values to dictionaries if they are None
|
||||||
config = defaultdict(
|
new_config = OrderedDict()
|
||||||
dict, {key: value or {} for key, value in config.items()})
|
for key, value in config.items():
|
||||||
|
new_config[key] = value or {}
|
||||||
|
config = new_config
|
||||||
|
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
components = set(key.split(' ')[0] for key in config.keys()
|
components = set(key.split(' ')[0] for key in config.keys()
|
||||||
@ -425,6 +425,8 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||||||
for domain in loader.load_order_components(components):
|
for domain in loader.load_order_components(components):
|
||||||
yield from _async_setup_component(hass, domain, config)
|
yield from _async_setup_component(hass, domain, config)
|
||||||
|
|
||||||
|
setup_lock.release()
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ def async_setup(hass, config):
|
|||||||
tasks.append(hass.services.async_call(
|
tasks.append(hass.services.async_call(
|
||||||
domain, service.service, data, blocking))
|
domain, service.service, data, blocking))
|
||||||
|
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||||
|
@ -4,20 +4,45 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||||
"""
|
"""
|
||||||
|
from os import path
|
||||||
import logging
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.components.envisalink import (
|
from homeassistant.components.envisalink import (
|
||||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['envisalink']
|
DEPENDENCIES = ['envisalink']
|
||||||
|
|
||||||
|
DEVICES = []
|
||||||
|
|
||||||
|
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||||
|
ATTR_KEYPRESS = 'keypress'
|
||||||
|
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
vol.Required(ATTR_KEYPRESS): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def alarm_keypress_handler(service):
|
||||||
|
"""Map services to methods on Alarm."""
|
||||||
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
|
keypress = service.data.get(ATTR_KEYPRESS)
|
||||||
|
|
||||||
|
_target_devices = [device for device in DEVICES
|
||||||
|
if device.entity_id in entity_ids]
|
||||||
|
|
||||||
|
for device in _target_devices:
|
||||||
|
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -35,8 +60,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
_panic_type,
|
_panic_type,
|
||||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||||
EVL_CONTROLLER)
|
EVL_CONTROLLER)
|
||||||
add_devices([_device])
|
DEVICES.append(_device)
|
||||||
|
|
||||||
|
add_devices(DEVICES)
|
||||||
|
|
||||||
|
# Register Envisalink specific services
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
path.join(path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
|
||||||
|
alarm_keypress_handler,
|
||||||
|
descriptions.get(SERVICE_ALARM_KEYPRESS),
|
||||||
|
schema=ALARM_KEYPRESS_SCHEMA)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -66,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""The characters if code is defined."""
|
"""Regex for code format or None if no code is required."""
|
||||||
return self._code
|
if self._code:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return '^\\d{4,6}$'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
|
state = STATE_UNKNOWN
|
||||||
|
|
||||||
if self._info['status']['alarm']:
|
if self._info['status']['alarm']:
|
||||||
return STATE_ALARM_TRIGGERED
|
state = STATE_ALARM_TRIGGERED
|
||||||
elif self._info['status']['armed_away']:
|
elif self._info['status']['armed_away']:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
state = STATE_ALARM_ARMED_AWAY
|
||||||
elif self._info['status']['armed_stay']:
|
elif self._info['status']['armed_stay']:
|
||||||
return STATE_ALARM_ARMED_HOME
|
state = STATE_ALARM_ARMED_HOME
|
||||||
|
elif self._info['status']['exit_delay']:
|
||||||
|
state = STATE_ALARM_PENDING
|
||||||
|
elif self._info['status']['entry_delay']:
|
||||||
|
state = STATE_ALARM_PENDING
|
||||||
elif self._info['status']['alpha']:
|
elif self._info['status']['alpha']:
|
||||||
return STATE_ALARM_DISARMED
|
state = STATE_ALARM_DISARMED
|
||||||
else:
|
return state
|
||||||
return STATE_UNKNOWN
|
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if self._code:
|
if code:
|
||||||
EVL_CONTROLLER.disarm_partition(
|
EVL_CONTROLLER.disarm_partition(str(code),
|
||||||
str(code), self._partition_number)
|
self._partition_number)
|
||||||
|
else:
|
||||||
|
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||||
|
self._partition_number)
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if self._code:
|
if code:
|
||||||
EVL_CONTROLLER.arm_stay_partition(
|
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||||
str(code), self._partition_number)
|
self._partition_number)
|
||||||
|
else:
|
||||||
|
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||||
|
self._partition_number)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if self._code:
|
if code:
|
||||||
EVL_CONTROLLER.arm_away_partition(
|
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||||
str(code), self._partition_number)
|
self._partition_number)
|
||||||
|
else:
|
||||||
|
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||||
|
self._partition_number)
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||||
if self._code:
|
|
||||||
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
EVL_CONTROLLER.panic_alarm(self._panic_type)
|
||||||
|
|
||||||
|
def alarm_keypress(self, keypress=None):
|
||||||
|
"""Send custom keypress."""
|
||||||
|
if keypress:
|
||||||
|
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||||
|
keypress)
|
||||||
|
@ -129,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
if self._pending_time:
|
if self._pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self._state_ts + self._pending_time)
|
self._state_ts + self._pending_time)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
if self._pending_time:
|
if self._pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self._state_ts + self._pending_time)
|
self._state_ts + self._pending_time)
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
if self._trigger_time:
|
if self._trigger_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self._state_ts + self._pending_time)
|
self._state_ts + self._pending_time)
|
||||||
|
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self._state_ts + self._pending_time + self._trigger_time)
|
self._state_ts + self._pending_time + self._trigger_time)
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
|
@ -41,3 +41,14 @@ alarm_trigger:
|
|||||||
code:
|
code:
|
||||||
description: An optional code to trigger the alarm control panel with
|
description: An optional code to trigger the alarm control panel with
|
||||||
example: 1234
|
example: 1234
|
||||||
|
|
||||||
|
envisalink_alarm_keypress:
|
||||||
|
description: Send custom keypresses to the alarm
|
||||||
|
|
||||||
|
fields:
|
||||||
|
entity_id:
|
||||||
|
description: Name of the alarm control panel to trigger
|
||||||
|
example: 'alarm_control_panel.downstairs'
|
||||||
|
keypress:
|
||||||
|
description: 'String to send to the alarm panel (1-6 characters)'
|
||||||
|
example: '*71'
|
||||||
|
@ -66,6 +66,7 @@ def _platform_validator(config):
|
|||||||
|
|
||||||
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||||
|
|
||||||
|
|
||||||
_TRIGGER_SCHEMA = vol.All(
|
_TRIGGER_SCHEMA = vol.All(
|
||||||
cv.ensure_list,
|
cv.ensure_list,
|
||||||
[
|
[
|
||||||
@ -165,7 +166,7 @@ def async_setup(hass, config):
|
|||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
tasks.append(entity.async_trigger(
|
tasks.append(entity.async_trigger(
|
||||||
service_call.data.get(ATTR_VARIABLES), True))
|
service_call.data.get(ATTR_VARIABLES), True))
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def turn_onoff_service_handler(service_call):
|
def turn_onoff_service_handler(service_call):
|
||||||
@ -174,7 +175,7 @@ def async_setup(hass, config):
|
|||||||
method = 'async_{}'.format(service_call.service)
|
method = 'async_{}'.format(service_call.service)
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
tasks.append(getattr(entity, method)())
|
tasks.append(getattr(entity, method)())
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def toggle_service_handler(service_call):
|
def toggle_service_handler(service_call):
|
||||||
@ -185,7 +186,7 @@ def async_setup(hass, config):
|
|||||||
tasks.append(entity.async_turn_off())
|
tasks.append(entity.async_turn_off())
|
||||||
else:
|
else:
|
||||||
tasks.append(entity.async_turn_on())
|
tasks.append(entity.async_turn_on())
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def reload_service_handler(service_call):
|
def reload_service_handler(service_call):
|
||||||
@ -348,7 +349,9 @@ def _async_process_config(hass, config, component):
|
|||||||
tasks.append(entity.async_enable())
|
tasks.append(entity.async_enable())
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
if entities:
|
||||||
yield from component.async_add_entities(entities)
|
yield from component.async_add_entities(entities)
|
||||||
|
|
||||||
return len(entities) > 0
|
return len(entities) > 0
|
||||||
|
@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
|||||||
def _callback(self, state):
|
def _callback(self, state):
|
||||||
"""HA-FFmpeg callback for noise detection."""
|
"""HA-FFmpeg callback for noise detection."""
|
||||||
self._state = state
|
self._state = state
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _start_ffmpeg(self, config):
|
def _start_ffmpeg(self, config):
|
||||||
"""Start a FFmpeg instance."""
|
"""Start a FFmpeg instance."""
|
||||||
|
@ -8,6 +8,7 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, SENSOR_CLASSES)
|
BinarySensorDevice, SENSOR_CLASSES)
|
||||||
@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
|
|||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
self._qos = qos
|
self._qos = qos
|
||||||
|
|
||||||
|
@callback
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
"""A new MQTT message has been received."""
|
"""A new MQTT message has been received."""
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
payload = value_template.render_with_possible_json_value(
|
payload = value_template.async_render_with_possible_json_value(
|
||||||
payload)
|
payload)
|
||||||
if payload == self._payload_on:
|
if payload == self._payload_on:
|
||||||
self._state = True
|
self._state = True
|
||||||
self.update_ha_state()
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
elif payload == self._payload_off:
|
elif payload == self._payload_off:
|
||||||
self._state = False
|
self._state = False
|
||||||
self.update_ha_state()
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||||
|
|
||||||
|
@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||||
|
if not gateways:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in gateways:
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# states. Map them in a dict of lists.
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
|||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.components.netatmo import WelcomeData
|
from homeassistant.components.netatmo import WelcomeData
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ["netatmo"]
|
DEPENDENCIES = ["netatmo"]
|
||||||
@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras'
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_HOME): cv.string,
|
vol.Optional(CONF_HOME): cv.string,
|
||||||
|
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||||
vol.Optional(CONF_CAMERAS, default=[]):
|
vol.Optional(CONF_CAMERAS, default=[]):
|
||||||
vol.All(cv.ensure_list, [cv.string]),
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||||
@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup access to Netatmo binary sensor."""
|
"""Setup access to Netatmo binary sensor."""
|
||||||
netatmo = get_component('netatmo')
|
netatmo = get_component('netatmo')
|
||||||
home = config.get(CONF_HOME, None)
|
home = config.get(CONF_HOME, None)
|
||||||
|
timeout = config.get(CONF_TIMEOUT, 15)
|
||||||
|
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
try:
|
try:
|
||||||
@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
camera_name not in config[CONF_CAMERAS]:
|
camera_name not in config[CONF_CAMERAS]:
|
||||||
continue
|
continue
|
||||||
for variable in sensors:
|
for variable in sensors:
|
||||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
||||||
variable)])
|
variable)])
|
||||||
|
|
||||||
|
|
||||||
class WelcomeBinarySensor(BinarySensorDevice):
|
class WelcomeBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||||
|
|
||||||
def __init__(self, data, camera_name, home, sensor):
|
def __init__(self, data, camera_name, home, timeout, sensor):
|
||||||
"""Setup for access to the Netatmo camera events."""
|
"""Setup for access to the Netatmo camera events."""
|
||||||
self._data = data
|
self._data = data
|
||||||
self._camera_name = camera_name
|
self._camera_name = camera_name
|
||||||
self._home = home
|
self._home = home
|
||||||
|
self._timeout = timeout
|
||||||
if home:
|
if home:
|
||||||
self._name = home + ' / ' + camera_name
|
self._name = home + ' / ' + camera_name
|
||||||
else:
|
else:
|
||||||
@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
|||||||
if self._sensor_name == "Someone known":
|
if self._sensor_name == "Someone known":
|
||||||
self._state =\
|
self._state =\
|
||||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||||
self._camera_name)
|
self._camera_name,
|
||||||
|
self._timeout*60)
|
||||||
elif self._sensor_name == "Someone unknown":
|
elif self._sensor_name == "Someone unknown":
|
||||||
self._state =\
|
self._state =\
|
||||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||||
self._camera_name)
|
self._camera_name,
|
||||||
|
self._timeout*60)
|
||||||
elif self._sensor_name == "Motion":
|
elif self._sensor_name == "Motion":
|
||||||
self._state =\
|
self._state =\
|
||||||
self._data.welcomedata.motionDetected(self._home,
|
self._data.welcomedata.motionDetected(self._home,
|
||||||
self._camera_name)
|
self._camera_name,
|
||||||
|
self._timeout*60)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
|
|||||||
if not zone_sensor:
|
if not zone_sensor:
|
||||||
return
|
return
|
||||||
zone_sensor._zone['state'] = event['zone_state']
|
zone_sensor._zone['state'] = event['zone_state']
|
||||||
zone_sensor.update_ha_state()
|
zone_sensor.schedule_update_ha_state()
|
||||||
|
|
||||||
def _process_events(self, events):
|
def _process_events(self, events):
|
||||||
for event in events:
|
for event in events:
|
||||||
|
@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
|||||||
def read_gpio(port):
|
def read_gpio(port):
|
||||||
"""Read state from GPIO."""
|
"""Read state from GPIO."""
|
||||||
self._state = rpi_gpio.read_input(self._port)
|
self._state = rpi_gpio.read_input(self._port)
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
_LOGGER.error('No sensors added')
|
_LOGGER.error('No sensors added')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hass.loop.create_task(async_add_devices(sensors, True))
|
yield from async_add_devices(sensors, True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
@callback
|
@callback
|
||||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||||
"""Called when the target device changes state."""
|
"""Called when the target device changes state."""
|
||||||
hass.loop.create_task(self.async_update_ha_state(True))
|
hass.async_add_job(self.async_update_ha_state, True)
|
||||||
|
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
hass, entity_ids, template_bsensor_state_listener)
|
hass, entity_ids, template_bsensor_state_listener)
|
||||||
|
@ -4,8 +4,11 @@ A sensor that monitors trands in other components.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.trend/
|
https://home-assistant.io/components/sensor.trend/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
|
|||||||
self.from_state = None
|
self.from_state = None
|
||||||
self.to_state = None
|
self.to_state = None
|
||||||
|
|
||||||
self.update()
|
@callback
|
||||||
|
|
||||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||||
"""Called when the target device changes state."""
|
"""Called when the target device changes state."""
|
||||||
self.from_state = old_state
|
self.from_state = old_state
|
||||||
self.to_state = new_state
|
self.to_state = new_state
|
||||||
self.update_ha_state(True)
|
hass.async_add_job(self.async_update_ha_state(True))
|
||||||
|
|
||||||
track_state_change(hass, target_entity,
|
track_state_change(hass, target_entity,
|
||||||
trend_sensor_state_listener)
|
trend_sensor_state_listener)
|
||||||
@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
|
|||||||
"""No polling needed."""
|
"""No polling needed."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update(self):
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
"""Get the latest data and update the states."""
|
"""Get the latest data and update the states."""
|
||||||
if self.from_state is None or self.to_state is None:
|
if self.from_state is None or self.to_state is None:
|
||||||
return
|
return
|
||||||
|
@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
|
|||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
'Subscription update for %s',
|
'Subscription update for %s',
|
||||||
_device)
|
_device)
|
||||||
if not hasattr(self, 'hass'):
|
|
||||||
self.update()
|
self.update()
|
||||||
|
if not hasattr(self, 'hass'):
|
||||||
return
|
return
|
||||||
self.update_ha_state(True)
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
|||||||
at https://home-assistant.io/components/binary_sensor.wink/
|
at https://home-assistant.io/components/binary_sensor.wink/
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.sensor.wink import WinkDevice
|
from homeassistant.components.sensor.wink import WinkDevice
|
||||||
@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||||||
self.capability = self.wink.capability()
|
self.capability = self.wink.capability()
|
||||||
|
|
||||||
def _pubnub_update(self, message, channel):
|
def _pubnub_update(self, message, channel):
|
||||||
|
try:
|
||||||
if 'data' in message:
|
if 'data' in message:
|
||||||
json_data = json.dumps(message.get('data'))
|
json_data = json.dumps(message.get('data'))
|
||||||
else:
|
else:
|
||||||
json_data = message
|
json_data = message
|
||||||
self.wink.pubnub_update(json.loads(json_data))
|
self.wink.pubnub_update(json.loads(json_data))
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
error = "Pubnub returned invalid json for " + self.name
|
||||||
|
logging.getLogger(__name__).error(error)
|
||||||
|
self.update_ha_state(True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
|||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id or \
|
if self._value.value_id == value.value_id or \
|
||||||
self._value.node == value.node:
|
self._value.node == value.node:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
||||||
@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
|
|||||||
# If it's active make sure that we set the timeout tracker
|
# If it's active make sure that we set the timeout tracker
|
||||||
if sensor_value.data:
|
if sensor_value.data:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self.invalidate_after)
|
self.invalidate_after)
|
||||||
|
|
||||||
def value_changed(self, value):
|
def value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id:
|
if self._value.value_id == value.value_id:
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
if value.data:
|
if value.data:
|
||||||
# only allow this value to be true for re_arm secs
|
# only allow this value to be true for re_arm secs
|
||||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||||
seconds=self.re_arm_sec)
|
seconds=self.re_arm_sec)
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.update_ha_state,
|
self._hass, self.async_update_ha_state,
|
||||||
self.invalidate_after)
|
self.invalidate_after)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
183
homeassistant/components/calendar/__init__.py
Normal file
183
homeassistant/components/calendar/__init__.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
"""
|
||||||
|
Support for Google Calendar event device sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/calendar/
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from homeassistant.components.google import (CONF_OFFSET,
|
||||||
|
CONF_DEVICE_ID,
|
||||||
|
CONF_NAME)
|
||||||
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.helpers.config_validation import time_period_str
|
||||||
|
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.template import DATE_STR_FORMAT
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'calendar'
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Track states and offer events for calendars."""
|
||||||
|
component = EntityComponent(
|
||||||
|
logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN)
|
||||||
|
|
||||||
|
component.setup(config)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONF_TRACK_NEW = True
|
||||||
|
DEFAULT_CONF_OFFSET = '!!'
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class CalendarEventDevice(Entity):
|
||||||
|
"""A calendar event device."""
|
||||||
|
|
||||||
|
# Classes overloading this must set data to an object
|
||||||
|
# with an update() method
|
||||||
|
data = None
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(self, hass, data):
|
||||||
|
"""Create the Calendar Event Device."""
|
||||||
|
self._name = data.get(CONF_NAME)
|
||||||
|
self.dev_id = data.get(CONF_DEVICE_ID)
|
||||||
|
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
|
||||||
|
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
|
||||||
|
self.dev_id,
|
||||||
|
hass=hass)
|
||||||
|
|
||||||
|
self._cal_data = {
|
||||||
|
'all_day': False,
|
||||||
|
'offset_time': dt.dt.timedelta(),
|
||||||
|
'message': '',
|
||||||
|
'start': None,
|
||||||
|
'end': None,
|
||||||
|
'location': '',
|
||||||
|
'description': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def offset_reached(self):
|
||||||
|
"""Have we reached the offset time specified in the event title."""
|
||||||
|
if self._cal_data['start'] is None or \
|
||||||
|
self._cal_data['offset_time'] == dt.dt.timedelta():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return self._cal_data['start'] + self._cal_data['offset_time'] <= \
|
||||||
|
dt.now(self._cal_data['start'].tzinfo)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the entity."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""State Attributes for HA."""
|
||||||
|
start = self._cal_data.get('start', None)
|
||||||
|
end = self._cal_data.get('end', None)
|
||||||
|
start = start.strftime(DATE_STR_FORMAT) if start is not None else None
|
||||||
|
end = end.strftime(DATE_STR_FORMAT) if end is not None else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'message': self._cal_data.get('message', ''),
|
||||||
|
'all_day': self._cal_data.get('all_day', False),
|
||||||
|
'offset_reached': self.offset_reached(),
|
||||||
|
'start_time': start,
|
||||||
|
'end_time': end,
|
||||||
|
'location': self._cal_data.get('location', None),
|
||||||
|
'description': self._cal_data.get('description', None),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the calendar event."""
|
||||||
|
start = self._cal_data.get('start', None)
|
||||||
|
end = self._cal_data.get('end', None)
|
||||||
|
if start is None or end is None:
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
now = dt.now()
|
||||||
|
|
||||||
|
if start <= now and end > now:
|
||||||
|
return STATE_ON
|
||||||
|
|
||||||
|
if now >= end:
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleanup any start/end listeners that were setup."""
|
||||||
|
self._cal_data = {
|
||||||
|
'all_day': False,
|
||||||
|
'offset_time': 0,
|
||||||
|
'message': '',
|
||||||
|
'start': None,
|
||||||
|
'end': None,
|
||||||
|
'location': None,
|
||||||
|
'description': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Search for the next event."""
|
||||||
|
if not self.data or not self.data.update():
|
||||||
|
# update cached, don't do anything
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.data.event:
|
||||||
|
# we have no event to work on, make sure we're clean
|
||||||
|
self.cleanup()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_date(date):
|
||||||
|
"""Get the dateTime from date or dateTime as a local."""
|
||||||
|
if 'date' in date:
|
||||||
|
return dt.as_utc(dt.dt.datetime.combine(
|
||||||
|
dt.parse_date(date['date']), dt.dt.time()))
|
||||||
|
else:
|
||||||
|
return dt.parse_datetime(date['dateTime'])
|
||||||
|
|
||||||
|
start = _get_date(self.data.event['start'])
|
||||||
|
end = _get_date(self.data.event['end'])
|
||||||
|
|
||||||
|
summary = self.data.event['summary']
|
||||||
|
|
||||||
|
# check if we have an offset tag in the message
|
||||||
|
# time is HH:MM or MM
|
||||||
|
reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset)
|
||||||
|
search = re.search(reg, summary)
|
||||||
|
if search and search.group(1):
|
||||||
|
time = search.group(1)
|
||||||
|
if ':' not in time:
|
||||||
|
if time[0] == '+' or time[0] == '-':
|
||||||
|
time = '{}0:{}'.format(time[0], time[1:])
|
||||||
|
else:
|
||||||
|
time = '0:{}'.format(time)
|
||||||
|
|
||||||
|
offset_time = time_period_str(time)
|
||||||
|
summary = (summary[:search.start()] + summary[search.end():]) \
|
||||||
|
.strip()
|
||||||
|
else:
|
||||||
|
offset_time = dt.dt.timedelta() # default it
|
||||||
|
|
||||||
|
# cleanup the string so we don't have a bunch of double+ spaces
|
||||||
|
self._cal_data['message'] = re.sub(' +', '', summary).strip()
|
||||||
|
|
||||||
|
self._cal_data['offset_time'] = offset_time
|
||||||
|
self._cal_data['location'] = self.data.event.get('location', '')
|
||||||
|
self._cal_data['description'] = self.data.event.get('description', '')
|
||||||
|
self._cal_data['start'] = start
|
||||||
|
self._cal_data['end'] = end
|
||||||
|
self._cal_data['all_day'] = 'date' in self.data.event['start']
|
82
homeassistant/components/calendar/demo.py
Executable file
82
homeassistant/components/calendar/demo.py
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Demo platform that has two fake binary sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/demo/
|
||||||
|
"""
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.components.calendar import CalendarEventDevice
|
||||||
|
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Demo binary sensor platform."""
|
||||||
|
calendar_data_future = DemoGoogleCalendarDataFuture()
|
||||||
|
calendar_data_current = DemoGoogleCalendarDataCurrent()
|
||||||
|
add_devices([
|
||||||
|
DemoGoogleCalendar(hass, calendar_data_future, {
|
||||||
|
CONF_NAME: 'Future Event',
|
||||||
|
CONF_DEVICE_ID: 'future_event',
|
||||||
|
}),
|
||||||
|
|
||||||
|
DemoGoogleCalendar(hass, calendar_data_current, {
|
||||||
|
CONF_NAME: 'Current Event',
|
||||||
|
CONF_DEVICE_ID: 'current_event',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class DemoGoogleCalendarData(object):
|
||||||
|
"""Setup base class for data."""
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def update(self):
|
||||||
|
"""Return true so entity knows we have new data."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
|
||||||
|
"""Setup future data event."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Set the event to a future event."""
|
||||||
|
one_hour_from_now = dt_util.now() \
|
||||||
|
+ dt_util.dt.timedelta(minutes=30)
|
||||||
|
self.event = {
|
||||||
|
'start': {
|
||||||
|
'dateTime': one_hour_from_now.isoformat()
|
||||||
|
},
|
||||||
|
'end': {
|
||||||
|
'dateTime': (one_hour_from_now + dt_util.dt.
|
||||||
|
timedelta(minutes=60)).isoformat()
|
||||||
|
},
|
||||||
|
'summary': 'Future Event',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData):
|
||||||
|
"""Create a current event we're in the middle of."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Set the event data."""
|
||||||
|
middle_of_event = dt_util.now() \
|
||||||
|
- dt_util.dt.timedelta(minutes=30)
|
||||||
|
self.event = {
|
||||||
|
'start': {
|
||||||
|
'dateTime': middle_of_event.isoformat()
|
||||||
|
},
|
||||||
|
'end': {
|
||||||
|
'dateTime': (middle_of_event + dt_util.dt.
|
||||||
|
timedelta(minutes=60)).isoformat()
|
||||||
|
},
|
||||||
|
'summary': 'Current Event',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DemoGoogleCalendar(CalendarEventDevice):
|
||||||
|
"""A Demo binary sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hass, calendar_data, data):
|
||||||
|
"""The same as a google calendar but without the api calls."""
|
||||||
|
self.data = calendar_data
|
||||||
|
super().__init__(hass, data)
|
79
homeassistant/components/calendar/google.py
Normal file
79
homeassistant/components/calendar/google.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Support for Google Calendar Search binary sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.google_calendar/
|
||||||
|
"""
|
||||||
|
# pylint: disable=import-error
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components.calendar import CalendarEventDevice
|
||||||
|
from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES,
|
||||||
|
CONF_TRACK, TOKEN_FILE,
|
||||||
|
GoogleCalendarService)
|
||||||
|
from homeassistant.util import Throttle, dt
|
||||||
|
|
||||||
|
DEFAULT_GOOGLE_SEARCH_PARAMS = {
|
||||||
|
'orderBy': 'startTime',
|
||||||
|
'maxResults': 1,
|
||||||
|
'singleEvents': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return cached results if last scan was less then this time ago
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, disc_info=None):
|
||||||
|
"""Setup the calendar platform for event devices."""
|
||||||
|
if disc_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]):
|
||||||
|
return
|
||||||
|
|
||||||
|
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||||
|
add_devices([GoogleCalendarEventDevice(hass, calendar_service,
|
||||||
|
disc_info[CONF_CAL_ID], data)
|
||||||
|
for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||||
|
"""A calendar event device."""
|
||||||
|
|
||||||
|
def __init__(self, hass, calendar_service, calendar, data):
|
||||||
|
"""Create the Calendar event device."""
|
||||||
|
self.data = GoogleCalendarData(calendar_service, calendar,
|
||||||
|
data.get('search', None))
|
||||||
|
super().__init__(hass, data)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendarData(object):
|
||||||
|
"""Class to utilize calendar service object to get next event."""
|
||||||
|
|
||||||
|
def __init__(self, calendar_service, calendar_id, search=None):
|
||||||
|
"""Setup how we are going to search the google calendar."""
|
||||||
|
self.calendar_service = calendar_service
|
||||||
|
self.calendar_id = calendar_id
|
||||||
|
self.search = search
|
||||||
|
self.event = None
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data."""
|
||||||
|
service = self.calendar_service.get()
|
||||||
|
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
|
||||||
|
params['timeMin'] = dt.utcnow().isoformat('T')
|
||||||
|
params['calendarId'] = self.calendar_id
|
||||||
|
if self.search:
|
||||||
|
params['q'] = self.search
|
||||||
|
|
||||||
|
events = service.events() # pylint: disable=no-member
|
||||||
|
result = events.list(**params).execute()
|
||||||
|
|
||||||
|
items = result.get('items', [])
|
||||||
|
self.event = items[0] if len(items) == 1 else None
|
||||||
|
return True
|
@ -35,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
"""Setup a FFmpeg Camera."""
|
"""Setup a FFmpeg Camera."""
|
||||||
if not async_run_test(hass, config.get(CONF_INPUT)):
|
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||||
return
|
return
|
||||||
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
|
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||||
|
|
||||||
|
|
||||||
class FFmpegCamera(Camera):
|
class FFmpegCamera(Camera):
|
||||||
@ -85,7 +85,7 @@ class FFmpegCamera(Camera):
|
|||||||
break
|
break
|
||||||
response.write(data)
|
response.write(data)
|
||||||
finally:
|
finally:
|
||||||
self.hass.loop.create_task(stream.close())
|
self.hass.async_add_job(stream.close())
|
||||||
yield from response.write_eof()
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a generic IP Camera."""
|
"""Setup a generic IP Camera."""
|
||||||
hass.loop.create_task(async_add_devices([GenericCamera(hass, config)]))
|
yield from async_add_devices([GenericCamera(hass, config)])
|
||||||
|
|
||||||
|
|
||||||
class GenericCamera(Camera):
|
class GenericCamera(Camera):
|
||||||
|
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Setup a MJPEG IP Camera."""
|
"""Setup a MJPEG IP Camera."""
|
||||||
hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)]))
|
yield from async_add_devices([MjpegCamera(hass, config)])
|
||||||
|
|
||||||
|
|
||||||
def extract_image_from_mjpeg(stream):
|
def extract_image_from_mjpeg(stream):
|
||||||
@ -122,7 +122,7 @@ class MjpegCamera(Camera):
|
|||||||
break
|
break
|
||||||
response.write(data)
|
response.write(data)
|
||||||
finally:
|
finally:
|
||||||
self.hass.loop.create_task(stream.release())
|
self.hass.async_add_job(stream.release())
|
||||||
yield from response.write_eof()
|
yield from response.write_eof()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -14,6 +14,7 @@ from aiohttp import web
|
|||||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
|
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP)
|
||||||
@ -60,8 +61,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
"""Setup a Synology IP Camera."""
|
"""Setup a Synology IP Camera."""
|
||||||
if not config.get(CONF_VERIFY_SSL):
|
if not config.get(CONF_VERIFY_SSL):
|
||||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
connector = aiohttp.TCPConnector(verify_ssl=False)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _async_close_connector(event):
|
||||||
|
"""Close websession on shutdown."""
|
||||||
|
yield from connector.close()
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, _async_close_connector)
|
||||||
else:
|
else:
|
||||||
connector = None
|
connector = hass.websession.connector
|
||||||
|
|
||||||
websession_init = aiohttp.ClientSession(
|
websession_init = aiohttp.ClientSession(
|
||||||
loop=hass.loop,
|
loop=hass.loop,
|
||||||
@ -115,10 +124,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
websession = aiohttp.ClientSession(
|
websession = aiohttp.ClientSession(
|
||||||
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
||||||
|
|
||||||
@asyncio.coroutine
|
@callback
|
||||||
def _async_close_websession(event):
|
def _async_close_websession(event):
|
||||||
"""Close webssesion on shutdown."""
|
"""Close websession on shutdown."""
|
||||||
yield from websession.close()
|
websession.detach()
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||||
|
@ -145,7 +145,7 @@ class GenericThermostat(ClimateDevice):
|
|||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Return the maximum temperature."""
|
"""Return the maximum temperature."""
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
if self._min_temp:
|
if self._max_temp:
|
||||||
return self._max_temp
|
return self._max_temp
|
||||||
else:
|
else:
|
||||||
# Get default temp from super class
|
# Get default temp from super class
|
||||||
@ -158,7 +158,7 @@ class GenericThermostat(ClimateDevice):
|
|||||||
|
|
||||||
self._update_temp(new_state)
|
self._update_temp(new_state)
|
||||||
self._control_heating()
|
self._control_heating()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _update_temp(self, state):
|
def _update_temp(self, state):
|
||||||
"""Update thermostat with latest state from sensor."""
|
"""Update thermostat with latest state from sensor."""
|
||||||
|
@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup the mysensors climate."""
|
"""Setup the mysensors climate."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
|
||||||
|
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||||
|
if not gateways:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in gateways:
|
||||||
if float(gateway.protocol_version) < 1.5:
|
if float(gateway.protocol_version) < 1.5:
|
||||||
continue
|
continue
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.const import (
|
|||||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['proliphix==0.4.0']
|
REQUIREMENTS = ['proliphix==0.4.1']
|
||||||
|
|
||||||
ATTR_FAN = 'fan'
|
ATTR_FAN = 'fan'
|
||||||
|
|
||||||
|
331
homeassistant/components/climate/wink.py
Normal file
331
homeassistant/components/climate/wink.py
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
"""
|
||||||
|
Support for Wink thermostats.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/climate.wink/
|
||||||
|
"""
|
||||||
|
from homeassistant.components.wink import WinkDevice
|
||||||
|
from homeassistant.components.climate import (
|
||||||
|
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||||
|
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
ATTR_CURRENT_HUMIDITY)
|
||||||
|
from homeassistant.const import (
|
||||||
|
TEMP_CELSIUS, STATE_ON,
|
||||||
|
STATE_OFF, STATE_UNKNOWN)
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
|
DEPENDENCIES = ['wink']
|
||||||
|
|
||||||
|
STATE_AUX = 'aux'
|
||||||
|
STATE_ECO = 'eco'
|
||||||
|
|
||||||
|
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||||
|
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||||
|
ATTR_ECO_TARGET = "eco_target"
|
||||||
|
ATTR_OCCUPIED = "occupied"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Wink thermostat."""
|
||||||
|
import pywink
|
||||||
|
temp_unit = hass.config.units.temperature_unit
|
||||||
|
add_devices(WinkThermostat(thermostat, temp_unit)
|
||||||
|
for thermostat in pywink.get_thermostats())
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||||
|
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||||
|
"""Representation of a Wink thermostat."""
|
||||||
|
|
||||||
|
def __init__(self, wink, temp_unit):
|
||||||
|
"""Initialize the Wink device."""
|
||||||
|
super().__init__(wink)
|
||||||
|
wink = get_component('wink')
|
||||||
|
self._config_temp_unit = temp_unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self):
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
# The Wink API always returns temp in Celsius
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the optional state attributes."""
|
||||||
|
data = {}
|
||||||
|
target_temp_high = self.target_temperature_high
|
||||||
|
target_temp_low = self.target_temperature_low
|
||||||
|
if target_temp_high is not None:
|
||||||
|
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||||
|
self.target_temperature_high)
|
||||||
|
if target_temp_low is not None:
|
||||||
|
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||||
|
self.target_temperature_low)
|
||||||
|
|
||||||
|
if self.external_temperature:
|
||||||
|
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||||
|
self.external_temperature)
|
||||||
|
|
||||||
|
if self.smart_temperature:
|
||||||
|
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||||
|
|
||||||
|
if self.occupied:
|
||||||
|
data[ATTR_OCCUPIED] = self.occupied
|
||||||
|
|
||||||
|
if self.eco_target:
|
||||||
|
data[ATTR_ECO_TARGET] = self.eco_target
|
||||||
|
|
||||||
|
current_humidity = self.current_humidity
|
||||||
|
if current_humidity is not None:
|
||||||
|
data[ATTR_CURRENT_HUMIDITY] = current_humidity
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
"""Return the current temperature."""
|
||||||
|
return self.wink.current_temperature()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_humidity(self):
|
||||||
|
"""Return the current humidity."""
|
||||||
|
if self.wink.current_humidity() is not None:
|
||||||
|
# The API states humidity will be a float 0-1
|
||||||
|
# the only example API response with humidity listed show an int
|
||||||
|
# This will address both possibilities
|
||||||
|
if self.wink.current_humidity() < 1:
|
||||||
|
return self.wink.current_humidity() * 100
|
||||||
|
else:
|
||||||
|
return self.wink.current_humidity()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def external_temperature(self):
|
||||||
|
"""Return the current external temperature."""
|
||||||
|
return self.wink.current_external_temperature()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def smart_temperature(self):
|
||||||
|
"""Return the current average temp of all remote sensor."""
|
||||||
|
return self.wink.current_smart_temperature()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eco_target(self):
|
||||||
|
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||||
|
return self.wink.eco_target()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def occupied(self):
|
||||||
|
"""Return status of if the thermostat has detected occupancy."""
|
||||||
|
return self.wink.occupied()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_operation(self):
|
||||||
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
|
if not self.wink.is_on():
|
||||||
|
current_op = STATE_OFF
|
||||||
|
elif self.wink.current_hvac_mode() == 'cool_only':
|
||||||
|
current_op = STATE_COOL
|
||||||
|
elif self.wink.current_hvac_mode() == 'heat_only':
|
||||||
|
current_op = STATE_HEAT
|
||||||
|
elif self.wink.current_hvac_mode() == 'aux':
|
||||||
|
current_op = STATE_HEAT
|
||||||
|
elif self.wink.current_hvac_mode() == 'auto':
|
||||||
|
current_op = STATE_AUTO
|
||||||
|
elif self.wink.current_hvac_mode() == 'eco':
|
||||||
|
current_op = STATE_ECO
|
||||||
|
else:
|
||||||
|
current_op = STATE_UNKNOWN
|
||||||
|
return current_op
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_humidity(self):
|
||||||
|
"""Return the humidity we try to reach."""
|
||||||
|
target_hum = None
|
||||||
|
if self.wink.current_humidifier_mode() == 'on':
|
||||||
|
if self.wink.current_humidifier_set_point() is not None:
|
||||||
|
target_hum = self.wink.current_humidifier_set_point() * 100
|
||||||
|
elif self.wink.current_dehumidifier_mode() == 'on':
|
||||||
|
if self.wink.current_dehumidifier_set_point() is not None:
|
||||||
|
target_hum = self.wink.current_dehumidifier_set_point() * 100
|
||||||
|
else:
|
||||||
|
target_hum = None
|
||||||
|
return target_hum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
"""Return the temperature we try to reach."""
|
||||||
|
if self.current_operation != STATE_AUTO and not self.is_away_mode_on:
|
||||||
|
if self.current_operation == STATE_COOL:
|
||||||
|
return self.wink.current_max_set_point()
|
||||||
|
elif self.current_operation == STATE_HEAT:
|
||||||
|
return self.wink.current_min_set_point()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_low(self):
|
||||||
|
"""Return the lower bound temperature we try to reach."""
|
||||||
|
if self.current_operation == STATE_AUTO:
|
||||||
|
return self.wink.current_min_set_point()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature_high(self):
|
||||||
|
"""Return the higher bound temperature we try to reach."""
|
||||||
|
if self.current_operation == STATE_AUTO:
|
||||||
|
return self.wink.current_max_set_point()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_away_mode_on(self):
|
||||||
|
"""Return if away mode is on."""
|
||||||
|
return self.wink.away()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_aux_heat_on(self):
|
||||||
|
"""Return true if aux heater."""
|
||||||
|
if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on():
|
||||||
|
return True
|
||||||
|
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_temperature(self, **kwargs):
|
||||||
|
"""Set new target temperature."""
|
||||||
|
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
|
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
|
if target_temp is not None:
|
||||||
|
if self.current_operation == STATE_COOL:
|
||||||
|
target_temp_high = target_temp
|
||||||
|
if self.current_operation == STATE_HEAT:
|
||||||
|
target_temp_low = target_temp
|
||||||
|
if target_temp_low is not None:
|
||||||
|
target_temp_low = target_temp_low
|
||||||
|
if target_temp_high is not None:
|
||||||
|
target_temp_high = target_temp_high
|
||||||
|
self.wink.set_temperature(target_temp_low, target_temp_high)
|
||||||
|
|
||||||
|
def set_operation_mode(self, operation_mode):
|
||||||
|
"""Set operation mode."""
|
||||||
|
if operation_mode == STATE_HEAT:
|
||||||
|
self.wink.set_operation_mode('heat_only')
|
||||||
|
elif operation_mode == STATE_COOL:
|
||||||
|
self.wink.set_operation_mode('cool_only')
|
||||||
|
elif operation_mode == STATE_AUTO:
|
||||||
|
self.wink.set_operation_mode('auto')
|
||||||
|
elif operation_mode == STATE_OFF:
|
||||||
|
self.wink.set_operation_mode('off')
|
||||||
|
elif operation_mode == STATE_AUX:
|
||||||
|
self.wink.set_operation_mode('aux')
|
||||||
|
elif operation_mode == STATE_ECO:
|
||||||
|
self.wink.set_operation_mode('eco')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def operation_list(self):
|
||||||
|
"""List of available operation modes."""
|
||||||
|
op_list = ['off']
|
||||||
|
modes = self.wink.hvac_modes()
|
||||||
|
if 'cool_only' in modes:
|
||||||
|
op_list.append(STATE_COOL)
|
||||||
|
if 'heat_only' in modes or 'aux' in modes:
|
||||||
|
op_list.append(STATE_HEAT)
|
||||||
|
if 'auto' in modes:
|
||||||
|
op_list.append(STATE_AUTO)
|
||||||
|
if 'eco' in modes:
|
||||||
|
op_list.append(STATE_ECO)
|
||||||
|
return op_list
|
||||||
|
|
||||||
|
def turn_away_mode_on(self):
|
||||||
|
"""Turn away on."""
|
||||||
|
self.wink.set_away_mode()
|
||||||
|
|
||||||
|
def turn_away_mode_off(self):
|
||||||
|
"""Turn away off."""
|
||||||
|
self.wink.set_away_mode(False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_fan_mode(self):
|
||||||
|
"""Return whether the fan is on."""
|
||||||
|
if self.wink.current_fan_mode() == 'on':
|
||||||
|
return STATE_ON
|
||||||
|
elif self.wink.current_fan_mode() == 'auto':
|
||||||
|
return STATE_AUTO
|
||||||
|
else:
|
||||||
|
# No Fan available so disable slider
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fan_list(self):
|
||||||
|
"""List of available fan modes."""
|
||||||
|
if self.wink.has_fan():
|
||||||
|
return self.wink.fan_modes()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_fan_mode(self, fan):
|
||||||
|
"""Turn fan on/off."""
|
||||||
|
self.wink.set_fan_mode(fan.lower())
|
||||||
|
|
||||||
|
def turn_aux_heat_on(self):
|
||||||
|
"""Turn auxillary heater on."""
|
||||||
|
self.set_operation_mode(STATE_AUX)
|
||||||
|
|
||||||
|
def turn_aux_heat_off(self):
|
||||||
|
"""Turn auxillary heater off."""
|
||||||
|
self.set_operation_mode(STATE_AUTO)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_temp(self):
|
||||||
|
"""Return the minimum temperature."""
|
||||||
|
minimum = 7 # Default minimum
|
||||||
|
min_min = self.wink.min_min_set_point()
|
||||||
|
min_max = self.wink.min_max_set_point()
|
||||||
|
return_value = minimum
|
||||||
|
if self.current_operation == STATE_HEAT:
|
||||||
|
if min_min:
|
||||||
|
return_value = min_min
|
||||||
|
else:
|
||||||
|
return_value = minimum
|
||||||
|
elif self.current_operation == STATE_COOL:
|
||||||
|
if min_max:
|
||||||
|
return_value = min_max
|
||||||
|
else:
|
||||||
|
return_value = minimum
|
||||||
|
elif self.current_operation == STATE_AUTO:
|
||||||
|
if min_min and min_max:
|
||||||
|
return_value = min(min_min, min_max)
|
||||||
|
else:
|
||||||
|
return_value = minimum
|
||||||
|
else:
|
||||||
|
return_value = minimum
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_temp(self):
|
||||||
|
"""Return the maximum temperature."""
|
||||||
|
maximum = 35 # Default maximum
|
||||||
|
max_min = self.wink.max_min_set_point()
|
||||||
|
max_max = self.wink.max_max_set_point()
|
||||||
|
return_value = maximum
|
||||||
|
if self.current_operation == STATE_HEAT:
|
||||||
|
if max_min:
|
||||||
|
return_value = max_min
|
||||||
|
else:
|
||||||
|
return_value = maximum
|
||||||
|
elif self.current_operation == STATE_COOL:
|
||||||
|
if max_max:
|
||||||
|
return_value = max_max
|
||||||
|
else:
|
||||||
|
return_value = maximum
|
||||||
|
elif self.current_operation == STATE_AUTO:
|
||||||
|
if max_min and max_max:
|
||||||
|
return_value = min(max_min, max_max)
|
||||||
|
else:
|
||||||
|
return_value = maximum
|
||||||
|
else:
|
||||||
|
return_value = maximum
|
||||||
|
return return_value
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Support for ZWave climate devices.
|
Support for Z-Wave climate devices.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/climate.zwave/
|
https://home-assistant.io/components/climate.zwave/
|
||||||
@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
|
|||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.components.climate import DOMAIN
|
from homeassistant.components.climate import DOMAIN
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import ClimateDevice
|
||||||
ClimateDevice, ATTR_OPERATION_MODE)
|
|
||||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -18,44 +17,23 @@ from homeassistant.const import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_NAME = 'name'
|
CONF_NAME = 'name'
|
||||||
DEFAULT_NAME = 'ZWave Climate'
|
DEFAULT_NAME = 'Z-Wave Climate'
|
||||||
|
|
||||||
REMOTEC = 0x5254
|
REMOTEC = 0x5254
|
||||||
REMOTEC_ZXT_120 = 0x8377
|
REMOTEC_ZXT_120 = 0x8377
|
||||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
||||||
|
ATTR_OPERATING_STATE = 'operating_state'
|
||||||
HORSTMANN = 0x0059
|
ATTR_FAN_STATE = 'fan_state'
|
||||||
HORSTMANN_HRT4_ZW = 0x3
|
|
||||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
|
||||||
|
|
||||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
|
||||||
|
|
||||||
DEVICE_MAPPINGS = {
|
DEVICE_MAPPINGS = {
|
||||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120,
|
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||||
HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW
|
|
||||||
}
|
|
||||||
|
|
||||||
SET_TEMP_TO_INDEX = {
|
|
||||||
'Heat': 1,
|
|
||||||
'Cool': 2,
|
|
||||||
'Auto': 3,
|
|
||||||
'Aux Heat': 4,
|
|
||||||
'Resume': 5,
|
|
||||||
'Fan Only': 6,
|
|
||||||
'Furnace': 7,
|
|
||||||
'Dry Air': 8,
|
|
||||||
'Moist Air': 9,
|
|
||||||
'Auto Changeover': 10,
|
|
||||||
'Heat Econ': 11,
|
|
||||||
'Cool Econ': 12,
|
|
||||||
'Away': 13,
|
|
||||||
'Unknown': 14
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the ZWave Climate devices."""
|
"""Set up the Z-Wave Climate devices."""
|
||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||||
discovery_info, zwave.NETWORK)
|
discovery_info, zwave.NETWORK)
|
||||||
@ -70,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||||
"""Represents a ZWave Climate device."""
|
"""Representation of a Z-Wave Climate device."""
|
||||||
|
|
||||||
def __init__(self, value, temp_unit):
|
def __init__(self, value, temp_unit):
|
||||||
"""Initialize the zwave climate device."""
|
"""Initialize the Z-Wave climate device."""
|
||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||||
|
self._index = value.index
|
||||||
self._node = value.node
|
self._node = value.node
|
||||||
self._target_temperature = None
|
self._target_temperature = None
|
||||||
self._current_temperature = None
|
self._current_temperature = None
|
||||||
@ -85,13 +64,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
self._operating_state = None
|
self._operating_state = None
|
||||||
self._current_fan_mode = None
|
self._current_fan_mode = None
|
||||||
self._fan_list = None
|
self._fan_list = None
|
||||||
|
self._fan_state = None
|
||||||
self._current_swing_mode = None
|
self._current_swing_mode = None
|
||||||
self._swing_list = None
|
self._swing_list = None
|
||||||
self._unit = temp_unit
|
self._unit = temp_unit
|
||||||
self._index_operation = None
|
|
||||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||||
self._zxt_120 = None
|
self._zxt_120 = None
|
||||||
self._hrt4_zw = None
|
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
# register listener
|
# register listener
|
||||||
dispatcher.connect(
|
dispatcher.connect(
|
||||||
@ -106,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||||
" workaround")
|
" workaround")
|
||||||
self._zxt_120 = 1
|
self._zxt_120 = 1
|
||||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW:
|
|
||||||
_LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat"
|
|
||||||
" workaround")
|
|
||||||
self._hrt4_zw = 1
|
|
||||||
|
|
||||||
def value_changed(self, value):
|
def value_changed(self, value):
|
||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id or \
|
if self._value.value_id == value.value_id or \
|
||||||
self._value.node == value.node:
|
self._value.node == value.node:
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
_LOGGER.debug("Value changed on network %s", value)
|
_LOGGER.debug("Value changed on network %s", value)
|
||||||
|
|
||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
@ -125,21 +99,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||||
self._current_operation = value.data
|
self._current_operation = value.data
|
||||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
|
||||||
self._current_operation)
|
|
||||||
self._operation_list = list(value.data_items)
|
self._operation_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||||
_LOGGER.debug("self._current_operation=%s",
|
_LOGGER.debug("self._current_operation=%s",
|
||||||
self._current_operation)
|
self._current_operation)
|
||||||
# Current Temp
|
# Current Temp
|
||||||
for value in (self._node.get_values(
|
for value in (
|
||||||
|
self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||||
.values()):
|
.values()):
|
||||||
if value.label == 'Temperature':
|
if value.label == 'Temperature':
|
||||||
self._current_temperature = int(value.data)
|
self._current_temperature = round((float(value.data)), 1)
|
||||||
self._unit = value.units
|
self._unit = value.units
|
||||||
# Fan Mode
|
# Fan Mode
|
||||||
for value in (self._node.get_values(
|
for value in (
|
||||||
|
self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||||
.values()):
|
.values()):
|
||||||
self._current_fan_mode = value.data
|
self._current_fan_mode = value.data
|
||||||
@ -149,7 +123,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
self._current_fan_mode)
|
self._current_fan_mode)
|
||||||
# Swing mode
|
# Swing mode
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
for value in (self._node.get_values(
|
for value in (
|
||||||
|
self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||||
.values()):
|
.values()):
|
||||||
if value.command_class == \
|
if value.command_class == \
|
||||||
@ -161,35 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
_LOGGER.debug("self._current_swing_mode=%s",
|
_LOGGER.debug("self._current_swing_mode=%s",
|
||||||
self._current_swing_mode)
|
self._current_swing_mode)
|
||||||
# Set point
|
# Set point
|
||||||
for value in (self._node.get_values(
|
temps = []
|
||||||
|
for value in (
|
||||||
|
self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
.values()):
|
.values()):
|
||||||
|
temps.append((round(float(value.data)), 1))
|
||||||
|
if value.index == self._index:
|
||||||
if value.data == 0:
|
if value.data == 0:
|
||||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||||
"current_temperature=%s",
|
"current_temperature=%s",
|
||||||
self._current_temperature)
|
self._current_temperature)
|
||||||
self._target_temperature = int(self._current_temperature)
|
self._target_temperature = (
|
||||||
|
round((float(self._current_temperature)), 1))
|
||||||
break
|
break
|
||||||
if self.current_operation is not None and \
|
else:
|
||||||
self.current_operation != 'Off':
|
self._target_temperature = round((float(value.data)), 1)
|
||||||
if self._index_operation != value.index:
|
|
||||||
continue
|
|
||||||
if self._zxt_120:
|
|
||||||
break
|
|
||||||
self._target_temperature = int(value.data)
|
|
||||||
break
|
|
||||||
_LOGGER.debug("Device can't set setpoint based on operation mode."
|
|
||||||
" Defaulting to index=1")
|
|
||||||
self._target_temperature = int(value.data)
|
|
||||||
# Operating state
|
# Operating state
|
||||||
for value in (self._node.get_values(
|
for value in (
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
|
self._node.get_values(
|
||||||
.values()):
|
class_id=zwave.const
|
||||||
|
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||||
self._operating_state = value.data
|
self._operating_state = value.data
|
||||||
|
|
||||||
|
# Fan operating state
|
||||||
|
for value in (
|
||||||
|
self._node.get_values(
|
||||||
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
|
||||||
|
.values()):
|
||||||
|
self._fan_state = value.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No polling on ZWave."""
|
"""No polling on Z-Wave."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -248,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
|
||||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
|
||||||
|
|
||||||
for value in (self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
.values()):
|
.values()):
|
||||||
if operation_mode is not None:
|
if value.index == self._index:
|
||||||
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
|
||||||
if value.index != setpoint_mode:
|
|
||||||
continue
|
|
||||||
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
|
||||||
value.data = temperature
|
|
||||||
break
|
|
||||||
|
|
||||||
if self.current_operation is not None:
|
|
||||||
if self._hrt4_zw and self.current_operation == 'Off':
|
|
||||||
# HRT4-ZW can change setpoint when off.
|
|
||||||
value.data = int(temperature)
|
|
||||||
if self._index_operation != value.index:
|
|
||||||
continue
|
|
||||||
_LOGGER.debug("self._index_operation=%s and"
|
|
||||||
" self._current_operation=%s",
|
|
||||||
self._index_operation,
|
|
||||||
self._current_operation)
|
|
||||||
if self._zxt_120:
|
if self._zxt_120:
|
||||||
_LOGGER.debug("zxt_120: Setting new setpoint for %s, "
|
|
||||||
" operation=%s, temp=%s",
|
|
||||||
self._index_operation,
|
|
||||||
self._current_operation, temperature)
|
|
||||||
# ZXT-120 does not support get setpoint
|
|
||||||
self._target_temperature = temperature
|
|
||||||
# ZXT-120 responds only to whole int
|
# ZXT-120 responds only to whole int
|
||||||
value.data = round(temperature, 0)
|
value.data = round(temperature, 0)
|
||||||
|
self._target_temperature = temperature
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Setting new setpoint for %s, "
|
|
||||||
"operation=%s, temp=%s",
|
|
||||||
self._index_operation,
|
|
||||||
self._current_operation, temperature)
|
|
||||||
value.data = temperature
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
_LOGGER.debug("Setting new setpoint for no known "
|
|
||||||
"operation mode. Index=1 and "
|
|
||||||
"temperature=%s", temperature)
|
|
||||||
value.data = temperature
|
value.data = temperature
|
||||||
|
self.update_ha_state()
|
||||||
break
|
break
|
||||||
|
|
||||||
def set_fan_mode(self, fan):
|
def set_fan_mode(self, fan):
|
||||||
@ -331,9 +276,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the device specific state attributes."""
|
"""Return the device specific state attributes."""
|
||||||
|
data = super().device_state_attributes
|
||||||
if self._operating_state:
|
if self._operating_state:
|
||||||
return {
|
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||||
"operating_state": self._operating_state,
|
if self._fan_state:
|
||||||
}
|
data[ATTR_FAN_STATE] = self._fan_state
|
||||||
else:
|
return data
|
||||||
return {}
|
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||||
|
|
||||||
ATTR_TEXT = 'text'
|
ATTR_TEXT = 'text'
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.components.cover import CoverDevice
|
from homeassistant.components.cover import CoverDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -89,29 +90,30 @@ class MqttCover(CoverDevice):
|
|||||||
self._retain = retain
|
self._retain = retain
|
||||||
self._optimistic = optimistic or state_topic is None
|
self._optimistic = optimistic or state_topic is None
|
||||||
|
|
||||||
|
@callback
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
"""A new MQTT message has been received."""
|
"""A new MQTT message has been received."""
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
payload = value_template.render_with_possible_json_value(
|
payload = value_template.async_render_with_possible_json_value(
|
||||||
payload)
|
payload)
|
||||||
if payload == self._state_open:
|
if payload == self._state_open:
|
||||||
self._state = False
|
self._state = False
|
||||||
_LOGGER.warning("state=%s", int(self._state))
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
self.update_ha_state()
|
|
||||||
elif payload == self._state_closed:
|
elif payload == self._state_closed:
|
||||||
self._state = True
|
self._state = True
|
||||||
self.update_ha_state()
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||||
if int(payload) > 0:
|
if int(payload) > 0:
|
||||||
self._state = False
|
self._state = False
|
||||||
else:
|
else:
|
||||||
self._state = True
|
self._state = True
|
||||||
self._position = int(payload)
|
self._position = int(payload)
|
||||||
self.update_ha_state()
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Payload is not True, False, or integer (0-100): %s",
|
"Payload is not True, False, or integer (0-100): %s",
|
||||||
payload)
|
payload)
|
||||||
|
|
||||||
if self._state_topic is None:
|
if self._state_topic is None:
|
||||||
# Force into optimistic mode.
|
# Force into optimistic mode.
|
||||||
self._optimistic = True
|
self._optimistic = True
|
||||||
|
@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup the mysensors platform for covers."""
|
"""Setup the mysensors platform for covers."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
|
||||||
|
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||||
|
if not gateways:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in gateways:
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
set_req = gateway.const.SetReq
|
set_req = gateway.const.SetReq
|
||||||
map_sv_types = {
|
map_sv_types = {
|
||||||
|
@ -36,10 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
and value.index == 0):
|
and value.index == 0):
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZwaveRollershutter(value)])
|
add_devices([ZwaveRollershutter(value)])
|
||||||
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
|
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||||
value.command_class ==
|
|
||||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
|
||||||
if (value.type != zwave.const.TYPE_BOOL and
|
if (value.type != zwave.const.TYPE_BOOL and
|
||||||
value.genre != zwave.const.GENRE_USER):
|
value.genre != zwave.const.GENRE_USER):
|
||||||
return
|
return
|
||||||
|
@ -17,6 +17,7 @@ DOMAIN = 'demo'
|
|||||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||||
'alarm_control_panel',
|
'alarm_control_panel',
|
||||||
'binary_sensor',
|
'binary_sensor',
|
||||||
|
'calendar',
|
||||||
'camera',
|
'camera',
|
||||||
'climate',
|
'climate',
|
||||||
'cover',
|
'cover',
|
||||||
|
@ -9,6 +9,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||||
from homeassistant.helpers.event import track_point_in_time
|
from homeassistant.helpers.event import track_point_in_time
|
||||||
@ -79,14 +80,14 @@ def setup(hass, config):
|
|||||||
return None
|
return None
|
||||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||||
|
|
||||||
def turn_light_on_before_sunset(light_id):
|
def async_turn_on_before_sunset(light_id):
|
||||||
"""Helper function to turn on lights.
|
"""Helper function to turn on lights.
|
||||||
|
|
||||||
Speed is slow if there are devices home and the light is not on yet.
|
Speed is slow if there are devices home and the light is not on yet.
|
||||||
"""
|
"""
|
||||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||||
return
|
return
|
||||||
light.turn_on(hass, light_id,
|
light.async_turn_on(hass, light_id,
|
||||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||||
profile=light_profile)
|
profile=light_profile)
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ def setup(hass, config):
|
|||||||
# pre-sun set event
|
# pre-sun set event
|
||||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||||
sun.STATE_ABOVE_HORIZON)
|
sun.STATE_ABOVE_HORIZON)
|
||||||
|
@callback
|
||||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||||
"""The moment sun sets we want to have all the lights on.
|
"""The moment sun sets we want to have all the lights on.
|
||||||
|
|
||||||
@ -104,16 +106,21 @@ def setup(hass, config):
|
|||||||
if not start_point:
|
if not start_point:
|
||||||
return
|
return
|
||||||
|
|
||||||
def turn_on(light_id):
|
def async_turn_on_factory(light_id):
|
||||||
"""Lambda can keep track of function parameters.
|
"""Lambda can keep track of function parameters.
|
||||||
|
|
||||||
No local parameters. If we put the lambda directly in the below
|
No local parameters. If we put the lambda directly in the below
|
||||||
statement only the last light will be turned on.
|
statement only the last light will be turned on.
|
||||||
"""
|
"""
|
||||||
return lambda now: turn_light_on_before_sunset(light_id)
|
@callback
|
||||||
|
def async_turn_on_light(now):
|
||||||
|
"""Turn on specific light."""
|
||||||
|
async_turn_on_before_sunset(light_id)
|
||||||
|
|
||||||
|
return async_turn_on_light
|
||||||
|
|
||||||
for index, light_id in enumerate(light_ids):
|
for index, light_id in enumerate(light_ids):
|
||||||
track_point_in_time(hass, turn_on(light_id),
|
track_point_in_time(hass, async_turn_on_factory(light_id),
|
||||||
start_point + index * LIGHT_TRANSITION_TIME)
|
start_point + index * LIGHT_TRANSITION_TIME)
|
||||||
|
|
||||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||||
@ -122,6 +129,7 @@ def setup(hass, config):
|
|||||||
schedule_lights_at_sun_set(hass, None, None, None)
|
schedule_lights_at_sun_set(hass, None, None, None)
|
||||||
|
|
||||||
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
||||||
|
@callback
|
||||||
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
||||||
"""Handle tracked device state changes."""
|
"""Handle tracked device state changes."""
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
@ -136,7 +144,7 @@ def setup(hass, config):
|
|||||||
# Do we need lights?
|
# Do we need lights?
|
||||||
if light_needed:
|
if light_needed:
|
||||||
logger.info("Home coming event for %s. Turning lights on", entity)
|
logger.info("Home coming event for %s. Turning lights on", entity)
|
||||||
light.turn_on(hass, light_ids, profile=light_profile)
|
light.async_turn_on(hass, light_ids, profile=light_profile)
|
||||||
|
|
||||||
# Are we in the time span were we would turn on the lights
|
# Are we in the time span were we would turn on the lights
|
||||||
# if someone would be home?
|
# if someone would be home?
|
||||||
@ -149,7 +157,7 @@ def setup(hass, config):
|
|||||||
# when the fading in started and turn it on if so
|
# when the fading in started and turn it on if so
|
||||||
for index, light_id in enumerate(light_ids):
|
for index, light_id in enumerate(light_ids):
|
||||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||||
light.turn_on(hass, light_id)
|
light.async_turn_on(hass, light_id)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If this light didn't happen to be turned on yet so
|
# If this light didn't happen to be turned on yet so
|
||||||
@ -158,6 +166,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
if not disable_turn_off:
|
if not disable_turn_off:
|
||||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||||
|
@callback
|
||||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||||
"""Handle device group state change."""
|
"""Handle device group state change."""
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
@ -166,6 +175,6 @@ def setup(hass, config):
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Everyone has left but there are lights on. Turning them off")
|
"Everyone has left but there are lights on. Turning them off")
|
||||||
light.turn_off(hass, light_ids)
|
light.async_turn_off(hass, light_ids)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -8,13 +8,13 @@ import asyncio
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
from typing import Any, Sequence, Callable
|
from typing import Any, Sequence, Callable
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.bootstrap import (
|
from homeassistant.bootstrap import (
|
||||||
prepare_setup_platform, log_exception)
|
async_prepare_setup_platform, async_log_exception)
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components import group, zone
|
from homeassistant.components import group, zone
|
||||||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
@ -28,7 +28,7 @@ from homeassistant.util.async import run_coroutine_threadsafe
|
|||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.yaml import dump
|
from homeassistant.util.yaml import dump
|
||||||
|
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import async_track_utc_time_change
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID)
|
||||||
@ -106,14 +106,15 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
|
|||||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass: HomeAssistantType, config: ConfigType):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||||
"""Setup device tracker."""
|
"""Setup device tracker."""
|
||||||
yaml_path = hass.config.path(YAML_DEVICES)
|
yaml_path = hass.config.path(YAML_DEVICES)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conf = config.get(DOMAIN, [])
|
conf = config.get(DOMAIN, [])
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
log_exception(ex, DOMAIN, config, hass)
|
async_log_exception(ex, DOMAIN, config, hass)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
conf = conf[0] if len(conf) > 0 else {}
|
conf = conf[0] if len(conf) > 0 else {}
|
||||||
@ -121,60 +122,77 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
|||||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
||||||
|
|
||||||
devices = load_config(yaml_path, hass, consider_home)
|
devices = yield from async_load_config(yaml_path, hass, consider_home)
|
||||||
|
|
||||||
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
tracker = DeviceTracker(hass, consider_home, track_new, devices)
|
||||||
|
|
||||||
def setup_platform(p_type, p_config, disc_info=None):
|
# update tracked devices
|
||||||
|
update_tasks = [device.async_update_ha_state() for device in devices
|
||||||
|
if device.track]
|
||||||
|
if update_tasks:
|
||||||
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(p_type, p_config, disc_info=None):
|
||||||
"""Setup a device tracker platform."""
|
"""Setup a device tracker platform."""
|
||||||
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
|
platform = yield from async_prepare_setup_platform(
|
||||||
|
hass, config, DOMAIN, p_type)
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(platform, 'get_scanner'):
|
if hasattr(platform, 'get_scanner'):
|
||||||
scanner = platform.get_scanner(hass, {DOMAIN: p_config})
|
scanner = yield from hass.loop.run_in_executor(
|
||||||
|
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||||
|
|
||||||
if scanner is None:
|
if scanner is None:
|
||||||
_LOGGER.error('Error setting up platform %s', p_type)
|
_LOGGER.error('Error setting up platform %s', p_type)
|
||||||
return
|
return
|
||||||
|
|
||||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
yield from async_setup_scanner_platform(
|
||||||
|
hass, p_config, scanner, tracker.async_see)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not platform.setup_scanner(hass, p_config, tracker.see):
|
ret = yield from hass.loop.run_in_executor(
|
||||||
|
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||||
|
if not ret:
|
||||||
_LOGGER.error('Error setting up platform %s', p_type)
|
_LOGGER.error('Error setting up platform %s', p_type)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||||
|
|
||||||
for p_type, p_config in config_per_platform(config, DOMAIN):
|
setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config
|
||||||
setup_platform(p_type, p_config)
|
in config_per_platform(config, DOMAIN)]
|
||||||
|
if setup_tasks:
|
||||||
|
yield from asyncio.wait(setup_tasks, loop=hass.loop)
|
||||||
|
|
||||||
def device_tracker_discovered(service, info):
|
yield from tracker.async_setup_group()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_device_tracker_discovered(service, info):
|
||||||
"""Called when a device tracker platform is discovered."""
|
"""Called when a device tracker platform is discovered."""
|
||||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
hass.async_add_job(
|
||||||
|
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
|
||||||
|
|
||||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
discovery.async_listen(
|
||||||
device_tracker_discovered)
|
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||||
|
|
||||||
def update_stale(now):
|
# Clean up stale devices
|
||||||
"""Clean up stale devices."""
|
async_track_utc_time_change(
|
||||||
tracker.update_stale(now)
|
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
|
||||||
|
|
||||||
tracker.setup_group()
|
@asyncio.coroutine
|
||||||
|
def async_see_service(call):
|
||||||
def see_service(call):
|
|
||||||
"""Service to see a device."""
|
"""Service to see a device."""
|
||||||
args = {key: value for key, value in call.data.items() if key in
|
args = {key: value for key, value in call.data.items() if key in
|
||||||
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
(ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME,
|
||||||
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||||
tracker.see(**args)
|
yield from tracker.async_see(**args)
|
||||||
|
|
||||||
descriptions = load_yaml_config_file(
|
descriptions = yield from hass.loop.run_in_executor(
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
None, load_yaml_config_file,
|
||||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||||
descriptions.get(SERVICE_SEE))
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -188,27 +206,35 @@ class DeviceTracker(object):
|
|||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.devices = {dev.dev_id: dev for dev in devices}
|
self.devices = {dev.dev_id: dev for dev in devices}
|
||||||
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
|
||||||
|
self.consider_home = consider_home
|
||||||
|
self.track_new = track_new
|
||||||
|
self.group = None # type: group.Group
|
||||||
|
self._is_updating = asyncio.Lock(loop=hass.loop)
|
||||||
|
|
||||||
for dev in devices:
|
for dev in devices:
|
||||||
if self.devices[dev.dev_id] is not dev:
|
if self.devices[dev.dev_id] is not dev:
|
||||||
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
||||||
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
||||||
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
||||||
dev.mac)
|
dev.mac)
|
||||||
self.consider_home = consider_home
|
|
||||||
self.track_new = track_new
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
if device.track:
|
|
||||||
device.update_ha_state()
|
|
||||||
|
|
||||||
self.group = None # type: group.Group
|
|
||||||
|
|
||||||
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||||
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
|
||||||
battery: str=None, attributes: dict=None):
|
battery: str=None, attributes: dict=None):
|
||||||
"""Notify the device tracker that you see a device."""
|
"""Notify the device tracker that you see a device."""
|
||||||
with self.lock:
|
self.hass.add_job(
|
||||||
|
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||||
|
gps_accuracy, battery, attributes)
|
||||||
|
)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
|
||||||
|
location_name: str=None, gps: GPSType=None,
|
||||||
|
gps_accuracy=None, battery: str=None, attributes: dict=None):
|
||||||
|
"""Notify the device tracker that you see a device.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
if mac is None and dev_id is None:
|
if mac is None and dev_id is None:
|
||||||
raise HomeAssistantError('Neither mac or device id passed in')
|
raise HomeAssistantError('Neither mac or device id passed in')
|
||||||
elif mac is not None:
|
elif mac is not None:
|
||||||
@ -221,10 +247,10 @@ class DeviceTracker(object):
|
|||||||
device = self.devices.get(dev_id)
|
device = self.devices.get(dev_id)
|
||||||
|
|
||||||
if device:
|
if device:
|
||||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
yield from device.async_seen(host_name, location_name, gps,
|
||||||
battery, attributes)
|
gps_accuracy, battery, attributes)
|
||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
yield from device.async_update_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
# If no device can be found, create it
|
# If no device can be found, create it
|
||||||
@ -236,46 +262,60 @@ class DeviceTracker(object):
|
|||||||
if mac is not None:
|
if mac is not None:
|
||||||
self.mac_to_dev[mac] = device
|
self.mac_to_dev[mac] = device
|
||||||
|
|
||||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
yield from device.async_seen(host_name, location_name, gps,
|
||||||
attributes)
|
gps_accuracy, battery, attributes)
|
||||||
|
|
||||||
if device.track:
|
if device.track:
|
||||||
device.update_ha_state()
|
yield from device.async_update_ha_state()
|
||||||
|
|
||||||
self.hass.bus.fire(EVENT_NEW_DEVICE, {
|
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||||
ATTR_ENTITY_ID: device.entity_id,
|
ATTR_ENTITY_ID: device.entity_id,
|
||||||
ATTR_HOST_NAME: device.host_name,
|
ATTR_HOST_NAME: device.host_name,
|
||||||
})
|
})
|
||||||
|
|
||||||
# During init, we ignore the group
|
# During init, we ignore the group
|
||||||
if self.group is not None:
|
if self.group is not None:
|
||||||
self.group.update_tracked_entity_ids(
|
yield from self.group.async_update_tracked_entity_ids(
|
||||||
list(self.group.tracking) + [device.entity_id])
|
list(self.group.tracking) + [device.entity_id])
|
||||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
|
||||||
|
|
||||||
def setup_group(self):
|
# update known_devices.yaml
|
||||||
"""Initialize group for all tracked devices."""
|
self.hass.async_add_job(
|
||||||
run_coroutine_threadsafe(
|
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||||
self.async_setup_group(), self.hass.loop).result()
|
dev_id, device)
|
||||||
|
)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_update_config(self, path, dev_id, device):
|
||||||
|
"""Add device to YAML configuration file.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
with (yield from self._is_updating):
|
||||||
|
self.hass.loop.run_in_executor(
|
||||||
|
None, update_config, self.hass.config.path(YAML_DEVICES),
|
||||||
|
dev_id, device)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_group(self):
|
def async_setup_group(self):
|
||||||
"""Initialize group for all tracked devices.
|
"""Initialize group for all tracked devices.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||||
if dev.track)
|
if dev.track)
|
||||||
self.group = yield from group.Group.async_create_group(
|
self.group = yield from group.Group.async_create_group(
|
||||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||||
|
|
||||||
def update_stale(self, now: dt_util.dt.datetime):
|
@callback
|
||||||
"""Update stale devices."""
|
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||||
with self.lock:
|
"""Update stale devices.
|
||||||
|
|
||||||
|
This method must be run in the event loop.
|
||||||
|
"""
|
||||||
for device in self.devices.values():
|
for device in self.devices.values():
|
||||||
if (device.track and device.last_update_home and
|
if (device.track and device.last_update_home) and \
|
||||||
device.stale(now)):
|
device.stale(now):
|
||||||
device.update_ha_state(True)
|
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||||
|
|
||||||
|
|
||||||
class Device(Entity):
|
class Device(Entity):
|
||||||
@ -362,7 +402,8 @@ class Device(Entity):
|
|||||||
"""If device should be hidden."""
|
"""If device should be hidden."""
|
||||||
return self.away_hide and self.state != STATE_HOME
|
return self.away_hide and self.state != STATE_HOME
|
||||||
|
|
||||||
def seen(self, host_name: str=None, location_name: str=None,
|
@asyncio.coroutine
|
||||||
|
def async_seen(self, host_name: str=None, location_name: str=None,
|
||||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||||
attributes: dict=None):
|
attributes: dict=None):
|
||||||
"""Mark the device as seen."""
|
"""Mark the device as seen."""
|
||||||
@ -373,28 +414,38 @@ class Device(Entity):
|
|||||||
self.battery = battery
|
self.battery = battery
|
||||||
self.attributes = attributes
|
self.attributes = attributes
|
||||||
self.gps = None
|
self.gps = None
|
||||||
|
|
||||||
if gps is not None:
|
if gps is not None:
|
||||||
try:
|
try:
|
||||||
self.gps = float(gps[0]), float(gps[1])
|
self.gps = float(gps[0]), float(gps[1])
|
||||||
except (ValueError, TypeError, IndexError):
|
except (ValueError, TypeError, IndexError):
|
||||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||||
self.dev_id, gps)
|
self.dev_id, gps)
|
||||||
self.update()
|
|
||||||
|
# pylint: disable=not-an-iterable
|
||||||
|
yield from self.async_update()
|
||||||
|
|
||||||
def stale(self, now: dt_util.dt.datetime=None):
|
def stale(self, now: dt_util.dt.datetime=None):
|
||||||
"""Return if device state is stale."""
|
"""Return if device state is stale.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
return self.last_seen and \
|
return self.last_seen and \
|
||||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||||
|
|
||||||
def update(self):
|
@asyncio.coroutine
|
||||||
"""Update state of entity."""
|
def async_update(self):
|
||||||
|
"""Update state of entity.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
if not self.last_seen:
|
if not self.last_seen:
|
||||||
return
|
return
|
||||||
elif self.location_name:
|
elif self.location_name:
|
||||||
self._state = self.location_name
|
self._state = self.location_name
|
||||||
elif self.gps is not None:
|
elif self.gps is not None:
|
||||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
zone_state = zone.async_active_zone(
|
||||||
self.gps_accuracy)
|
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||||
if zone_state is None:
|
if zone_state is None:
|
||||||
self._state = STATE_NOT_HOME
|
self._state = STATE_NOT_HOME
|
||||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||||
@ -412,6 +463,17 @@ class Device(Entity):
|
|||||||
|
|
||||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||||
"""Load devices from YAML configuration file."""
|
"""Load devices from YAML configuration file."""
|
||||||
|
return run_coroutine_threadsafe(
|
||||||
|
async_load_config(path, hass, consider_home), hass.loop).result()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_load_config(path: str, hass: HomeAssistantType,
|
||||||
|
consider_home: timedelta):
|
||||||
|
"""Load devices from YAML configuration file.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
dev_schema = vol.Schema({
|
dev_schema = vol.Schema({
|
||||||
vol.Required('name'): cv.string,
|
vol.Required('name'): cv.string,
|
||||||
vol.Optional('track', default=False): cv.boolean,
|
vol.Optional('track', default=False): cv.boolean,
|
||||||
@ -426,7 +488,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
|||||||
try:
|
try:
|
||||||
result = []
|
result = []
|
||||||
try:
|
try:
|
||||||
devices = load_yaml_config_file(path)
|
devices = yield from hass.loop.run_in_executor(
|
||||||
|
None, load_yaml_config_file, path)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||||
return []
|
return []
|
||||||
@ -436,7 +499,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
|||||||
device = dev_schema(device)
|
device = dev_schema(device)
|
||||||
device['dev_id'] = cv.slugify(dev_id)
|
device['dev_id'] = cv.slugify(dev_id)
|
||||||
except vol.Invalid as exp:
|
except vol.Invalid as exp:
|
||||||
log_exception(exp, dev_id, devices, hass)
|
async_log_exception(exp, dev_id, devices, hass)
|
||||||
else:
|
else:
|
||||||
result.append(Device(hass, **device))
|
result.append(Device(hass, **device))
|
||||||
return result
|
return result
|
||||||
@ -445,9 +508,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
@asyncio.coroutine
|
||||||
scanner: Any, see_device: Callable):
|
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||||
"""Helper method to connect scanner-based platform to device tracker."""
|
scanner: Any, async_see_device: Callable):
|
||||||
|
"""Helper method to connect scanner-based platform to device tracker.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||||
|
|
||||||
# Initial scan of each mac we also tell about host name for config
|
# Initial scan of each mac we also tell about host name for config
|
||||||
@ -455,18 +522,20 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
|||||||
|
|
||||||
def device_tracker_scan(now: dt_util.dt.datetime):
|
def device_tracker_scan(now: dt_util.dt.datetime):
|
||||||
"""Called when interval matches."""
|
"""Called when interval matches."""
|
||||||
for mac in scanner.scan_devices():
|
found_devices = scanner.scan_devices()
|
||||||
|
|
||||||
|
for mac in found_devices:
|
||||||
if mac in seen:
|
if mac in seen:
|
||||||
host_name = None
|
host_name = None
|
||||||
else:
|
else:
|
||||||
host_name = scanner.get_device_name(mac)
|
host_name = scanner.get_device_name(mac)
|
||||||
seen.add(mac)
|
seen.add(mac)
|
||||||
see_device(mac=mac, host_name=host_name)
|
hass.async_add_job(async_see_device(mac=mac, host_name=host_name))
|
||||||
|
|
||||||
track_utc_time_change(hass, device_tracker_scan, second=range(0, 60,
|
async_track_utc_time_change(
|
||||||
interval))
|
hass, device_tracker_scan, second=range(0, 60, interval))
|
||||||
|
|
||||||
device_tracker_scan(None)
|
hass.async_add_job(device_tracker_scan, None)
|
||||||
|
|
||||||
|
|
||||||
def update_config(path: str, dev_id: str, device: Device):
|
def update_config(path: str, dev_id: str, device: Device):
|
||||||
@ -484,7 +553,10 @@ def update_config(path: str, dev_id: str, device: Device):
|
|||||||
|
|
||||||
|
|
||||||
def get_gravatar_for_email(email: str):
|
def get_gravatar_for_email(email: str):
|
||||||
"""Return an 80px Gravatar for the given email address."""
|
"""Return an 80px Gravatar for the given email address.
|
||||||
|
|
||||||
|
Async friendly.
|
||||||
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
||||||
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
||||||
|
@ -42,6 +42,7 @@ def get_scanner(hass, config):
|
|||||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
Device = namedtuple("Device", ["mac", "ip", "last_update"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile(
|
|||||||
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
|
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
|
||||||
r'(?P<status>(\w+))')
|
r'(?P<status>(\w+))')
|
||||||
|
|
||||||
|
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||||
|
_NVRAM_REGEX = re.compile(
|
||||||
|
r'.*>.*>' +
|
||||||
|
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||||
|
r'>' +
|
||||||
|
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||||
|
r'>' +
|
||||||
|
r'.*')
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
@ -84,7 +93,8 @@ def get_scanner(hass, config):
|
|||||||
|
|
||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
|
||||||
|
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||||
|
|
||||||
|
|
||||||
class AsusWrtDeviceScanner(object):
|
class AsusWrtDeviceScanner(object):
|
||||||
@ -155,7 +165,8 @@ class AsusWrtDeviceScanner(object):
|
|||||||
active_clients = [client for client in data.values() if
|
active_clients = [client for client in data.values() if
|
||||||
client['status'] == 'REACHABLE' or
|
client['status'] == 'REACHABLE' or
|
||||||
client['status'] == 'DELAY' or
|
client['status'] == 'DELAY' or
|
||||||
client['status'] == 'STALE']
|
client['status'] == 'STALE' or
|
||||||
|
client['status'] == 'IN_NVRAM']
|
||||||
self.last_results = active_clients
|
self.last_results = active_clients
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -184,13 +195,18 @@ class AsusWrtDeviceScanner(object):
|
|||||||
ssh.sendline(_WL_CMD)
|
ssh.sendline(_WL_CMD)
|
||||||
ssh.prompt()
|
ssh.prompt()
|
||||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||||
|
ssh.sendline(_NVRAM_CMD)
|
||||||
|
ssh.prompt()
|
||||||
|
nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||||
else:
|
else:
|
||||||
arp_result = ['']
|
arp_result = ['']
|
||||||
|
nvram_result = ['']
|
||||||
ssh.sendline(_LEASES_CMD)
|
ssh.sendline(_LEASES_CMD)
|
||||||
ssh.prompt()
|
ssh.prompt()
|
||||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||||
ssh.logout()
|
ssh.logout()
|
||||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||||
|
nvram_result)
|
||||||
except pxssh.ExceptionPxssh as exc:
|
except pxssh.ExceptionPxssh as exc:
|
||||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||||
return None
|
return None
|
||||||
@ -213,13 +229,18 @@ class AsusWrtDeviceScanner(object):
|
|||||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||||
leases_result = (telnet.read_until(prompt_string).
|
leases_result = (telnet.read_until(prompt_string).
|
||||||
split(b'\n')[1:-1])
|
split(b'\n')[1:-1])
|
||||||
|
telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||||
|
nvram_result = (telnet.read_until(prompt_string).
|
||||||
|
split(b'\n')[1].split(b'<')[1:])
|
||||||
else:
|
else:
|
||||||
arp_result = ['']
|
arp_result = ['']
|
||||||
|
nvram_result = ['']
|
||||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||||
leases_result = (telnet.read_until(prompt_string).
|
leases_result = (telnet.read_until(prompt_string).
|
||||||
split(b'\n')[1:-1])
|
split(b'\n')[1:-1])
|
||||||
telnet.write('exit\n'.encode('ascii'))
|
telnet.write('exit\n'.encode('ascii'))
|
||||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||||
|
nvram_result)
|
||||||
except EOFError:
|
except EOFError:
|
||||||
_LOGGER.error('Unexpected response from router')
|
_LOGGER.error('Unexpected response from router')
|
||||||
return None
|
return None
|
||||||
@ -277,6 +298,26 @@ class AsusWrtDeviceScanner(object):
|
|||||||
'ip': arp_match.group('ip'),
|
'ip': arp_match.group('ip'),
|
||||||
'mac': match.group('mac').upper(),
|
'mac': match.group('mac').upper(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# match mac addresses to IP addresses in NVRAM table
|
||||||
|
for nvr in result.nvram:
|
||||||
|
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||||
|
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||||
|
if not nvram_match:
|
||||||
|
_LOGGER.warning('Could not parse nvr row: %s', nvr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# skip current check if already in ARP table
|
||||||
|
if nvram_match.group('ip') in devices.keys():
|
||||||
|
continue
|
||||||
|
|
||||||
|
devices[nvram_match.group('ip')] = {
|
||||||
|
'host': host,
|
||||||
|
'status': 'IN_NVRAM',
|
||||||
|
'ip': nvram_match.group('ip'),
|
||||||
|
'mac': match.group('mac').upper(),
|
||||||
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for lease in result.leases:
|
for lease in result.leases:
|
||||||
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
match = _LEASES_REGEX.search(lease.decode('utf-8'))
|
||||||
|
162
homeassistant/components/device_tracker/cisco_ios.py
Normal file
162
homeassistant/components/device_tracker/cisco_ios.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Support for Cisco IOS Routers.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.cisco_ios/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \
|
||||||
|
CONF_PORT
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
# Return cached results if last scan was less then this time ago.
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pexpect==4.0.1']
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = vol.All(
|
||||||
|
PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD, default=''): cv.string,
|
||||||
|
vol.Optional(CONF_PORT): cv.port,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_scanner(hass, config):
|
||||||
|
"""Validate the configuration and return a Cisco scanner."""
|
||||||
|
scanner = CiscoDeviceScanner(config[DOMAIN])
|
||||||
|
|
||||||
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
|
class CiscoDeviceScanner(object):
|
||||||
|
"""This class queries a wireless router running Cisco IOS firmware."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
"""Initialize the scanner."""
|
||||||
|
self.host = config[CONF_HOST]
|
||||||
|
self.username = config[CONF_USERNAME]
|
||||||
|
self.port = config.get(CONF_PORT)
|
||||||
|
self.password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
self.last_results = {}
|
||||||
|
|
||||||
|
self.success_init = self._update_info()
|
||||||
|
_LOGGER.info('cisco_ios scanner initialized')
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def get_device_name(self, device):
|
||||||
|
"""The firmware doesn't save the name of the wireless device."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def scan_devices(self):
|
||||||
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
|
self._update_info()
|
||||||
|
|
||||||
|
return self.last_results
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
|
def _update_info(self):
|
||||||
|
"""
|
||||||
|
Ensure the information from the Cisco router is up to date.
|
||||||
|
|
||||||
|
Returns boolean if scanning successful.
|
||||||
|
"""
|
||||||
|
string_result = self._get_arp_data()
|
||||||
|
|
||||||
|
if string_result:
|
||||||
|
self.last_results = []
|
||||||
|
last_results = []
|
||||||
|
|
||||||
|
lines_result = string_result.splitlines()
|
||||||
|
|
||||||
|
# Remove the first two lines, as they contains the arp command
|
||||||
|
# and the arp table titles e.g.
|
||||||
|
# show ip arp
|
||||||
|
# Protocol Address | Age (min) | Hardware Addr | Type | Interface
|
||||||
|
lines_result = lines_result[2:]
|
||||||
|
|
||||||
|
for line in lines_result:
|
||||||
|
if len(line.split()) is 6:
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) != 6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||||
|
# 'GigabitEthernet0']
|
||||||
|
age = parts[2]
|
||||||
|
hw_addr = parts[3]
|
||||||
|
|
||||||
|
if age != "-":
|
||||||
|
mac = _parse_cisco_mac_address(hw_addr)
|
||||||
|
age = int(age)
|
||||||
|
if age < 1:
|
||||||
|
last_results.append(mac)
|
||||||
|
|
||||||
|
self.last_results = last_results
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_arp_data(self):
|
||||||
|
"""Open connection to the router and get arp entries."""
|
||||||
|
from pexpect import pxssh
|
||||||
|
import re
|
||||||
|
|
||||||
|
try:
|
||||||
|
cisco_ssh = pxssh.pxssh()
|
||||||
|
cisco_ssh.login(self.host, self.username, self.password,
|
||||||
|
port=self.port, auto_prompt_reset=False)
|
||||||
|
|
||||||
|
# Find the hostname
|
||||||
|
initial_line = cisco_ssh.before.decode('utf-8').splitlines()
|
||||||
|
router_hostname = initial_line[len(initial_line) - 1]
|
||||||
|
router_hostname += "#"
|
||||||
|
# Set the discovered hostname as prompt
|
||||||
|
regex_expression = ('(?i)^%s' % router_hostname).encode()
|
||||||
|
cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE)
|
||||||
|
# Allow full arp table to print at once
|
||||||
|
cisco_ssh.sendline("terminal length 0")
|
||||||
|
cisco_ssh.prompt(1)
|
||||||
|
|
||||||
|
cisco_ssh.sendline("show ip arp")
|
||||||
|
cisco_ssh.prompt(1)
|
||||||
|
|
||||||
|
devices_result = cisco_ssh.before
|
||||||
|
|
||||||
|
return devices_result.decode("utf-8")
|
||||||
|
except pxssh.ExceptionPxssh as px_e:
|
||||||
|
_LOGGER.error("pxssh failed on login.")
|
||||||
|
_LOGGER.error(px_e)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cisco_mac_address(cisco_hardware_addr):
|
||||||
|
"""
|
||||||
|
Parse a Cisco formatted HW address to normal MAC.
|
||||||
|
|
||||||
|
e.g. convert
|
||||||
|
001d.ec02.07ab
|
||||||
|
|
||||||
|
to:
|
||||||
|
00:1D:EC:02:07:AB
|
||||||
|
|
||||||
|
Takes in cisco_hwaddr: HWAddr String from Cisco ARP table
|
||||||
|
Returns a regular standard MAC address
|
||||||
|
"""
|
||||||
|
cisco_hardware_addr = cisco_hardware_addr.replace('.', '')
|
||||||
|
blocks = [cisco_hardware_addr[x:x + 2]
|
||||||
|
for x in range(0, len(cisco_hardware_addr), 2)]
|
||||||
|
|
||||||
|
return ':'.join(blocks).upper()
|
@ -8,7 +8,9 @@ import asyncio
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||||
|
STATE_NOT_HOME,
|
||||||
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from homeassistant.components.device_tracker import ( # NOQA
|
from homeassistant.components.device_tracker import ( # NOQA
|
||||||
@ -76,11 +78,13 @@ class LocativeView(HomeAssistantView):
|
|||||||
device = data['device'].replace('-', '')
|
device = data['device'].replace('-', '')
|
||||||
location_name = data['id'].lower()
|
location_name = data['id'].lower()
|
||||||
direction = data['trigger']
|
direction = data['trigger']
|
||||||
|
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||||
|
|
||||||
if direction == 'enter':
|
if direction == 'enter':
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from self.hass.loop.run_in_executor(
|
||||||
None, partial(self.see, dev_id=device,
|
None, partial(self.see, dev_id=device,
|
||||||
location_name=location_name))
|
location_name=location_name,
|
||||||
|
gps=gps_location))
|
||||||
return 'Setting location to {}'.format(location_name)
|
return 'Setting location to {}'.format(location_name)
|
||||||
|
|
||||||
elif direction == 'exit':
|
elif direction == 'exit':
|
||||||
@ -88,9 +92,11 @@ class LocativeView(HomeAssistantView):
|
|||||||
'{}.{}'.format(DOMAIN, device))
|
'{}.{}'.format(DOMAIN, device))
|
||||||
|
|
||||||
if current_state is None or current_state.state == location_name:
|
if current_state is None or current_state.state == location_name:
|
||||||
|
location_name = STATE_NOT_HOME
|
||||||
yield from self.hass.loop.run_in_executor(
|
yield from self.hass.loop.run_in_executor(
|
||||||
None, partial(self.see, dev_id=device,
|
None, partial(self.see, dev_id=device,
|
||||||
location_name=STATE_NOT_HOME))
|
location_name=location_name,
|
||||||
|
gps=gps_location))
|
||||||
return 'Setting location to not home'
|
return 'Setting location to not home'
|
||||||
else:
|
else:
|
||||||
# Ignore the message if it is telling us to exit a zone that we
|
# Ignore the message if it is telling us to exit a zone that we
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Support for scanning a network with nmap.
|
Support for scanning a network with nmap.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/device_tracker.nmap_scanner/
|
https://home-assistant.io/components/device_tracker.nmap_tracker/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -43,6 +43,7 @@ def get_scanner(hass, config):
|
|||||||
|
|
||||||
return scanner if scanner.success_init else None
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||||
|
|
||||||
|
|
||||||
|
108
homeassistant/components/device_tracker/swisscom.py
Normal file
108
homeassistant/components/device_tracker/swisscom.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Support for Swisscom routers (Internet-Box).
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.swisscom/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
# Return cached results if last scan was less then this time ago.
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_IP = '192.168.1.1'
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_scanner(hass, config):
|
||||||
|
"""Return the Swisscom device scanner."""
|
||||||
|
scanner = SwisscomDeviceScanner(config[DOMAIN])
|
||||||
|
|
||||||
|
return scanner if scanner.success_init else None
|
||||||
|
|
||||||
|
|
||||||
|
class SwisscomDeviceScanner(object):
|
||||||
|
"""This class queries a router running Swisscom Internet-Box firmware."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
"""Initialize the scanner."""
|
||||||
|
self.host = config[CONF_HOST]
|
||||||
|
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
self.last_results = {}
|
||||||
|
|
||||||
|
# Test the router is accessible.
|
||||||
|
data = self.get_swisscom_data()
|
||||||
|
self.success_init = data is not None
|
||||||
|
|
||||||
|
def scan_devices(self):
|
||||||
|
"""Scan for new devices and return a list with found device IDs."""
|
||||||
|
self._update_info()
|
||||||
|
return [client['mac'] for client in self.last_results]
|
||||||
|
|
||||||
|
def get_device_name(self, device):
|
||||||
|
"""Return the name of the given device or None if we don't know."""
|
||||||
|
if not self.last_results:
|
||||||
|
return None
|
||||||
|
for client in self.last_results:
|
||||||
|
if client['mac'] == device:
|
||||||
|
return client['host']
|
||||||
|
return None
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
|
def _update_info(self):
|
||||||
|
"""Ensure the information from the Swisscom router is up to date.
|
||||||
|
|
||||||
|
Return boolean if scanning successful.
|
||||||
|
"""
|
||||||
|
if not self.success_init:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
_LOGGER.info("Loading data from Swisscom Internet Box")
|
||||||
|
data = self.get_swisscom_data()
|
||||||
|
if not data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
active_clients = [client for client in data.values() if
|
||||||
|
client['status']]
|
||||||
|
self.last_results = active_clients
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_swisscom_data(self):
|
||||||
|
"""Retrieve data from Swisscom and return parsed result."""
|
||||||
|
url = 'http://{}/ws'.format(self.host)
|
||||||
|
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
|
||||||
|
data = """
|
||||||
|
{"service":"Devices", "method":"get",
|
||||||
|
"parameters":{"expression":"lan and not self"}}"""
|
||||||
|
|
||||||
|
request = requests.post(url, headers=headers, data=data, timeout=10)
|
||||||
|
|
||||||
|
devices = {}
|
||||||
|
for device in request.json()['status']:
|
||||||
|
try:
|
||||||
|
devices[device['Key']] = {
|
||||||
|
'ip': device['IPAddress'],
|
||||||
|
'mac': device['PhysAddress'],
|
||||||
|
'host': device['Name'],
|
||||||
|
'status': device['Active']
|
||||||
|
}
|
||||||
|
except (KeyError, requests.exceptions.RequestException):
|
||||||
|
pass
|
||||||
|
return devices
|
@ -55,10 +55,6 @@ def setup_scanner(hass, config, see):
|
|||||||
"""True if any door/window is opened."""
|
"""True if any door/window is opened."""
|
||||||
return any([door[key] for key in door if "Open" in key])
|
return any([door[key] for key in door if "Open" in key])
|
||||||
|
|
||||||
see(dev_id=dev_id,
|
|
||||||
host_name=host_name,
|
|
||||||
gps=(position["latitude"],
|
|
||||||
position["longitude"]),
|
|
||||||
attributes = dict(
|
attributes = dict(
|
||||||
unlocked=not vehicle["carLocked"],
|
unlocked=not vehicle["carLocked"],
|
||||||
tank_volume=vehicle["fuelTankVolume"],
|
tank_volume=vehicle["fuelTankVolume"],
|
||||||
@ -70,10 +66,19 @@ def setup_scanner(hass, config, see):
|
|||||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||||
doors_open=any_opened(vehicle["doors"]),
|
doors_open=any_opened(vehicle["doors"]),
|
||||||
windows_open=any_opened(vehicle["windows"]),
|
windows_open=any_opened(vehicle["windows"]),
|
||||||
heater_on=vehicle["heater"]["status"] != "off",
|
|
||||||
fuel=vehicle["fuelAmount"],
|
fuel=vehicle["fuelAmount"],
|
||||||
odometer=round(vehicle["odometer"] / 1000), # km
|
odometer=round(vehicle["odometer"] / 1000), # km
|
||||||
range=vehicle["distanceToEmpty"]))
|
range=vehicle["distanceToEmpty"])
|
||||||
|
|
||||||
|
if "heater" in vehicle and \
|
||||||
|
"status" in vehicle["heater"]:
|
||||||
|
attributes.update(heater_on=vehicle["heater"]["status"] != "off")
|
||||||
|
|
||||||
|
see(dev_id=dev_id,
|
||||||
|
host_name=host_name,
|
||||||
|
gps=(position["latitude"],
|
||||||
|
position["longitude"]),
|
||||||
|
attributes=attributes)
|
||||||
|
|
||||||
def update(now):
|
def update(now):
|
||||||
"""Update status from the online service."""
|
"""Update status from the online service."""
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['python-digitalocean==1.10.0']
|
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.discovery import load_platform, discover
|
from homeassistant.helpers.discovery import load_platform, discover
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==0.7.5']
|
REQUIREMENTS = ['netdisco==0.7.6']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
DOMAIN = 'discovery'
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant import util, core
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||||
@ -318,7 +318,16 @@ class HueLightsView(HomeAssistantView):
|
|||||||
# Construct what we need to send to the service
|
# Construct what we need to send to the service
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
|
|
||||||
|
# If the requested entity is a script add some variables
|
||||||
|
if entity.domain.lower() == "script":
|
||||||
|
data['variables'] = {
|
||||||
|
'requested_state': STATE_ON if result else STATE_OFF
|
||||||
|
}
|
||||||
|
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
|
data['variables']['requested_level'] = brightness
|
||||||
|
|
||||||
|
elif brightness is not None:
|
||||||
data[ATTR_BRIGHTNESS] = brightness
|
data[ATTR_BRIGHTNESS] = brightness
|
||||||
|
|
||||||
if entity.domain.lower() in config.off_maps_to_on_domains:
|
if entity.domain.lower() in config.off_maps_to_on_domains:
|
||||||
@ -402,6 +411,13 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||||||
|
|
||||||
report_brightness = True
|
report_brightness = True
|
||||||
result = (brightness > 0)
|
result = (brightness > 0)
|
||||||
|
elif entity.domain.lower() == "script":
|
||||||
|
# Convert 0-255 to 0-100
|
||||||
|
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||||
|
|
||||||
|
brightness = round(level)
|
||||||
|
report_brightness = True
|
||||||
|
result = True
|
||||||
|
|
||||||
return (result, brightness) if report_brightness else (result, None)
|
return (result, brightness) if report_brightness else (result, None)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.components.discovery import load_platform
|
from homeassistant.components.discovery import load_platform
|
||||||
|
|
||||||
REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5']
|
REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DOMAIN = 'envisalink'
|
DOMAIN = 'envisalink'
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f
|
Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0
|
292
homeassistant/components/google.py
Normal file
292
homeassistant/components/google.py
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
"""
|
||||||
|
Support for Google - Calendar Event Devices.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/google/
|
||||||
|
|
||||||
|
NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST
|
||||||
|
CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR
|
||||||
|
REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS
|
||||||
|
IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.error import Error as VoluptuousError
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
from homeassistant import bootstrap
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
|
from homeassistant.helpers.event import track_time_change
|
||||||
|
from homeassistant.util import convert, dt
|
||||||
|
|
||||||
|
REQUIREMENTS = [
|
||||||
|
'google-api-python-client==1.5.5',
|
||||||
|
'oauth2client==3.0.0',
|
||||||
|
]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'google'
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
|
CONF_CLIENT_ID = 'client_id'
|
||||||
|
CONF_CLIENT_SECRET = 'client_secret'
|
||||||
|
CONF_TRACK_NEW = 'track_new_calendar'
|
||||||
|
|
||||||
|
CONF_CAL_ID = 'cal_id'
|
||||||
|
CONF_DEVICE_ID = 'device_id'
|
||||||
|
CONF_NAME = 'name'
|
||||||
|
CONF_ENTITIES = 'entities'
|
||||||
|
CONF_TRACK = 'track'
|
||||||
|
CONF_SEARCH = 'search'
|
||||||
|
CONF_OFFSET = 'offset'
|
||||||
|
|
||||||
|
DEFAULT_CONF_TRACK_NEW = True
|
||||||
|
DEFAULT_CONF_OFFSET = '!!'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'google_calendar_notification'
|
||||||
|
NOTIFICATION_TITLE = 'Google Calendar Setup'
|
||||||
|
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
|
||||||
|
|
||||||
|
SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
|
||||||
|
SERVICE_FOUND_CALENDARS = 'found_calendar'
|
||||||
|
|
||||||
|
DATA_INDEX = 'google_calendars'
|
||||||
|
|
||||||
|
YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
|
||||||
|
SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
|
||||||
|
|
||||||
|
TOKEN_FILE = '.{}.token'.format(DOMAIN)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||||
|
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||||
|
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
_SINGLE_CALSEARCH_CONFIG = vol.Schema({
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||||
|
vol.Optional(CONF_TRACK): cv.boolean,
|
||||||
|
vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
|
||||||
|
vol.Optional(CONF_OFFSET): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_CAL_ID): cv.string,
|
||||||
|
vol.Required(CONF_ENTITIES, None):
|
||||||
|
vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def do_authentication(hass, config):
|
||||||
|
"""Notify user of actions and authenticate.
|
||||||
|
|
||||||
|
Notify user of user_code and verification_url then poll
|
||||||
|
until we have an access token.
|
||||||
|
"""
|
||||||
|
from oauth2client.client import (
|
||||||
|
OAuth2WebServerFlow,
|
||||||
|
OAuth2DeviceCodeError,
|
||||||
|
FlowExchangeError
|
||||||
|
)
|
||||||
|
from oauth2client.file import Storage
|
||||||
|
|
||||||
|
oauth = OAuth2WebServerFlow(
|
||||||
|
config[CONF_CLIENT_ID],
|
||||||
|
config[CONF_CLIENT_SECRET],
|
||||||
|
'https://www.googleapis.com/auth/calendar.readonly',
|
||||||
|
'Home-Assistant.io',
|
||||||
|
)
|
||||||
|
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
|
try:
|
||||||
|
dev_flow = oauth.step1_get_device_and_user_codes()
|
||||||
|
except OAuth2DeviceCodeError as err:
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Error: {}<br />You will need to restart hass after fixing.'
|
||||||
|
''.format(err),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return False
|
||||||
|
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'In order to authorize Home-Assistant to view your calendars'
|
||||||
|
'You must visit: <a href="{}" target="_blank">{}</a> and enter'
|
||||||
|
'code: {}'.format(dev_flow.verification_url,
|
||||||
|
dev_flow.verification_url,
|
||||||
|
dev_flow.user_code),
|
||||||
|
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
def step2_exchange(now):
|
||||||
|
"""Keep trying to validate the user_code until it expires."""
|
||||||
|
if now >= dt.as_local(dev_flow.user_code_expiry):
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Authenication code expired, please restart '
|
||||||
|
'Home-Assistant and try again',
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
listener()
|
||||||
|
|
||||||
|
try:
|
||||||
|
credentials = oauth.step2_exchange(device_flow_info=dev_flow)
|
||||||
|
except FlowExchangeError:
|
||||||
|
# not ready yet, call again
|
||||||
|
return
|
||||||
|
|
||||||
|
storage = Storage(hass.config.path(TOKEN_FILE))
|
||||||
|
storage.put(credentials)
|
||||||
|
do_setup(hass, config)
|
||||||
|
listener()
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'We are all setup now. Check {} for calendars that have '
|
||||||
|
'been found'.format(YAML_DEVICES),
|
||||||
|
title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID)
|
||||||
|
|
||||||
|
listener = track_time_change(hass, step2_exchange,
|
||||||
|
second=range(0, 60, dev_flow.interval))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Setup the platform."""
|
||||||
|
if DATA_INDEX not in hass.data:
|
||||||
|
hass.data[DATA_INDEX] = {}
|
||||||
|
|
||||||
|
conf = config.get(DOMAIN, {})
|
||||||
|
|
||||||
|
token_file = hass.config.path(TOKEN_FILE)
|
||||||
|
if not os.path.isfile(token_file):
|
||||||
|
do_authentication(hass, conf)
|
||||||
|
else:
|
||||||
|
do_setup(hass, conf)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup_services(hass, track_new_found_calendars, calendar_service):
|
||||||
|
"""Setup service listeners."""
|
||||||
|
def _found_calendar(call):
|
||||||
|
"""Check if we know about a calendar and generate PLATFORM_DISCOVER."""
|
||||||
|
calendar = get_calendar_info(hass, call.data)
|
||||||
|
if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar})
|
||||||
|
|
||||||
|
update_config(
|
||||||
|
hass.config.path(YAML_DEVICES),
|
||||||
|
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]
|
||||||
|
)
|
||||||
|
|
||||||
|
discovery.load_platform(hass, 'calendar', DOMAIN,
|
||||||
|
hass.data[DATA_INDEX][calendar[CONF_CAL_ID]])
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar,
|
||||||
|
None, schema=None)
|
||||||
|
|
||||||
|
def _scan_for_calendars(service):
|
||||||
|
"""Scan for new calendars."""
|
||||||
|
service = calendar_service.get()
|
||||||
|
cal_list = service.calendarList() # pylint: disable=no-member
|
||||||
|
calendars = cal_list.list().execute()['items']
|
||||||
|
for calendar in calendars:
|
||||||
|
calendar['track'] = track_new_found_calendars
|
||||||
|
hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS,
|
||||||
|
calendar)
|
||||||
|
|
||||||
|
hass.services.register(
|
||||||
|
DOMAIN, SERVICE_SCAN_CALENDARS,
|
||||||
|
_scan_for_calendars,
|
||||||
|
None, schema=None)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def do_setup(hass, config):
|
||||||
|
"""Run the setup after we have everything configured."""
|
||||||
|
# load calendars the user has configured
|
||||||
|
hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES))
|
||||||
|
|
||||||
|
calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE))
|
||||||
|
track_new_found_calendars = convert(config.get(CONF_TRACK_NEW),
|
||||||
|
bool, DEFAULT_CONF_TRACK_NEW)
|
||||||
|
setup_services(hass, track_new_found_calendars, calendar_service)
|
||||||
|
|
||||||
|
# Ensure component is loaded
|
||||||
|
bootstrap.setup_component(hass, 'calendar', config)
|
||||||
|
|
||||||
|
for calendar in hass.data[DATA_INDEX].values():
|
||||||
|
discovery.load_platform(hass, 'calendar', DOMAIN, calendar)
|
||||||
|
|
||||||
|
# look for any new calendars
|
||||||
|
hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendarService(object):
|
||||||
|
"""Calendar service interface to google."""
|
||||||
|
|
||||||
|
def __init__(self, token_file):
|
||||||
|
"""We just need the token_file."""
|
||||||
|
self.token_file = token_file
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""Get the calendar service from the storage file token."""
|
||||||
|
import httplib2
|
||||||
|
from oauth2client.file import Storage
|
||||||
|
from googleapiclient import discovery as google_discovery
|
||||||
|
credentials = Storage(self.token_file).get()
|
||||||
|
http = credentials.authorize(httplib2.Http())
|
||||||
|
service = google_discovery.build('calendar', 'v3', http=http)
|
||||||
|
return service
|
||||||
|
|
||||||
|
|
||||||
|
def get_calendar_info(hass, calendar):
|
||||||
|
"""Convert data from Google into DEVICE_SCHEMA."""
|
||||||
|
calendar_info = DEVICE_SCHEMA({
|
||||||
|
CONF_CAL_ID: calendar['id'],
|
||||||
|
CONF_ENTITIES: [{
|
||||||
|
CONF_TRACK: calendar['track'],
|
||||||
|
CONF_NAME: calendar['summary'],
|
||||||
|
CONF_DEVICE_ID: generate_entity_id('{}', calendar['summary'],
|
||||||
|
hass=hass),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
return calendar_info
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(path):
|
||||||
|
"""Load the google_calendar_devices.yaml."""
|
||||||
|
calendars = {}
|
||||||
|
try:
|
||||||
|
with open(path) as file:
|
||||||
|
data = yaml.load(file)
|
||||||
|
for calendar in data:
|
||||||
|
try:
|
||||||
|
calendars.update({calendar[CONF_CAL_ID]:
|
||||||
|
DEVICE_SCHEMA(calendar)})
|
||||||
|
except VoluptuousError as exception:
|
||||||
|
# keep going
|
||||||
|
_LOGGER.warning('Calendar Invalid Data: %s', exception)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# When YAML file could not be loaded/did not contain a dict
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return calendars
|
||||||
|
|
||||||
|
|
||||||
|
def update_config(path, calendar):
|
||||||
|
"""Write the google_calendar_devices.yaml."""
|
||||||
|
with open(path, 'a') as out:
|
||||||
|
out.write('\n')
|
||||||
|
yaml.dump([calendar], out, default_flow_style=False)
|
@ -184,7 +184,7 @@ def async_setup(hass, config):
|
|||||||
tasks = [group.async_set_visible(visible) for group
|
tasks = [group.async_set_visible(visible) for group
|
||||||
in component.async_extract_from_service(service,
|
in component.async_extract_from_service(service,
|
||||||
expand_group=False)]
|
expand_group=False)]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||||
@ -207,12 +207,13 @@ def _async_process_config(hass, config, component):
|
|||||||
icon = conf.get(CONF_ICON)
|
icon = conf.get(CONF_ICON)
|
||||||
view = conf.get(CONF_VIEW)
|
view = conf.get(CONF_VIEW)
|
||||||
|
|
||||||
# This order is important as groups get a number based on creation
|
# Don't create tasks and await them all. The order is important as
|
||||||
# order.
|
# groups get a number based on creation order.
|
||||||
group = yield from Group.async_create_group(
|
group = yield from Group.async_create_group(
|
||||||
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
|
if groups:
|
||||||
yield from component.async_add_entities(groups)
|
yield from component.async_add_entities(groups)
|
||||||
|
|
||||||
|
|
||||||
@ -394,7 +395,7 @@ class Group(Entity):
|
|||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
self._async_update_group_state(new_state)
|
self._async_update_group_state(new_state)
|
||||||
self.hass.loop.create_task(self.async_update_ha_state())
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _tracking_states(self):
|
def _tracking_states(self):
|
||||||
|
@ -222,6 +222,7 @@ class GzipFileSender(FileSender):
|
|||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
_GZIP_FILE_SENDER = GzipFileSender()
|
_GZIP_FILE_SENDER = GzipFileSender()
|
||||||
|
|
||||||
|
|
||||||
@ -461,6 +462,9 @@ def request_handler_factory(view, handler):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def handle(request):
|
def handle(request):
|
||||||
"""Handle incoming request."""
|
"""Handle incoming request."""
|
||||||
|
if not view.hass.is_running:
|
||||||
|
return web.Response(status=503)
|
||||||
|
|
||||||
remote_addr = view.hass.http.get_real_ip(request)
|
remote_addr = view.hass.http.get_real_ip(request)
|
||||||
|
|
||||||
# Auth code verbose on purpose
|
# Auth code verbose on purpose
|
||||||
|
@ -23,17 +23,23 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_INITIAL = 'initial'
|
CONF_INITIAL = 'initial'
|
||||||
|
DEFAULT_INITIAL = False
|
||||||
|
|
||||||
SERVICE_SCHEMA = vol.Schema({
|
SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
cv.slug: vol.Any({
|
cv.slug: vol.Any({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_INITIAL, default=False): cv.boolean,
|
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
}, None)}}, extra=vol.ALLOW_EXTRA)
|
}, None)
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id):
|
def is_on(hass, entity_id):
|
||||||
@ -65,10 +71,10 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
if not cfg:
|
if not cfg:
|
||||||
cfg = {}
|
cfg = DEFAULT_CONFIG
|
||||||
|
|
||||||
name = cfg.get(CONF_NAME)
|
name = cfg.get(CONF_NAME)
|
||||||
state = cfg.get(CONF_INITIAL, False)
|
state = cfg.get(CONF_INITIAL)
|
||||||
icon = cfg.get(CONF_ICON)
|
icon = cfg.get(CONF_ICON)
|
||||||
|
|
||||||
entities.append(InputBoolean(object_id, name, state, icon))
|
entities.append(InputBoolean(object_id, name, state, icon))
|
||||||
@ -89,7 +95,7 @@ def async_setup(hass, config):
|
|||||||
attr = 'async_toggle'
|
attr = 'async_toggle'
|
||||||
|
|
||||||
tasks = [getattr(input_b, attr)() for input_b in target_inputs]
|
tasks = [getattr(input_b, attr)() for input_b in target_inputs]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)
|
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)
|
||||||
|
@ -55,14 +55,16 @@ def _cv_input_select(cfg):
|
|||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
cv.slug: vol.All({
|
cv.slug: vol.All({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1),
|
vol.Required(CONF_OPTIONS):
|
||||||
[cv.string]),
|
vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]),
|
||||||
vol.Optional(CONF_INITIAL): cv.string,
|
vol.Optional(CONF_INITIAL): cv.string,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
}, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA)
|
}, _cv_input_select)})
|
||||||
|
}, required=True, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def select_option(hass, entity_id, option):
|
def select_option(hass, entity_id, option):
|
||||||
@ -111,7 +113,7 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_select_option(call.data[ATTR_OPTION])
|
tasks = [input_select.async_select_option(call.data[ATTR_OPTION])
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
|
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
|
||||||
@ -124,7 +126,7 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_offset_index(1)
|
tasks = [input_select.async_offset_index(1)
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
|
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
|
||||||
@ -137,7 +139,7 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_offset_index(-1)
|
tasks = [input_select.async_offset_index(-1)
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
||||||
|
@ -51,7 +51,9 @@ def _cv_input_slider(cfg):
|
|||||||
cfg[CONF_INITIAL] = state
|
cfg[CONF_INITIAL] = state
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
cv.slug: vol.All({
|
cv.slug: vol.All({
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_MIN): vol.Coerce(float),
|
vol.Required(CONF_MIN): vol.Coerce(float),
|
||||||
@ -61,7 +63,9 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
|||||||
vol.Range(min=1e-3)),
|
vol.Range(min=1e-3)),
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
|
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
|
||||||
}, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA)
|
}, _cv_input_slider)
|
||||||
|
})
|
||||||
|
}, required=True, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def select_value(hass, entity_id, value):
|
def select_value(hass, entity_id, value):
|
||||||
@ -101,7 +105,7 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_slider.async_select_value(call.data[ATTR_VALUE])
|
tasks = [input_slider.async_select_value(call.data[ATTR_VALUE])
|
||||||
for input_slider in target_inputs]
|
for input_slider in target_inputs]
|
||||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
|
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Native Home Assistant iOS app component.
|
Native Home Assistant iOS app component.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/ios/
|
https://home-assistant.io/ecosystem/ios/
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
@ -10,6 +10,7 @@ import csv
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -20,6 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = "light"
|
DOMAIN = "light"
|
||||||
@ -128,6 +130,18 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
|||||||
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
|
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
|
||||||
profile=None, flash=None, effect=None, color_name=None):
|
profile=None, flash=None, effect=None, color_name=None):
|
||||||
"""Turn all or specified light on."""
|
"""Turn all or specified light on."""
|
||||||
|
run_callback_threadsafe(
|
||||||
|
hass.loop, async_turn_on, hass, entity_id, transition, brightness,
|
||||||
|
rgb_color, xy_color, color_temp, white_value,
|
||||||
|
profile, flash, effect, color_name).result()
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||||
|
rgb_color=None, xy_color=None, color_temp=None,
|
||||||
|
white_value=None, profile=None, flash=None, effect=None,
|
||||||
|
color_name=None):
|
||||||
|
"""Turn all or specified light on."""
|
||||||
data = {
|
data = {
|
||||||
key: value for key, value in [
|
key: value for key, value in [
|
||||||
(ATTR_ENTITY_ID, entity_id),
|
(ATTR_ENTITY_ID, entity_id),
|
||||||
@ -144,10 +158,17 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
|||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data)
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None, transition=None):
|
def turn_off(hass, entity_id=None, transition=None):
|
||||||
|
"""Turn all or specified light off."""
|
||||||
|
run_callback_threadsafe(
|
||||||
|
hass.loop, async_turn_off, hass, entity_id, transition).result()
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_turn_off(hass, entity_id=None, transition=None):
|
||||||
"""Turn all or specified light off."""
|
"""Turn all or specified light off."""
|
||||||
data = {
|
data = {
|
||||||
key: value for key, value in [
|
key: value for key, value in [
|
||||||
@ -156,7 +177,8 @@ def turn_off(hass, entity_id=None, transition=None):
|
|||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF,
|
||||||
|
data)
|
||||||
|
|
||||||
|
|
||||||
def toggle(hass, entity_id=None, transition=None):
|
def toggle(hass, entity_id=None, transition=None):
|
||||||
|
@ -17,8 +17,8 @@ from homeassistant.components.light import (
|
|||||||
PLATFORM_SCHEMA)
|
PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip'
|
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip'
|
||||||
'#flux_led==0.8']
|
'#flux_led==0.9']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -135,9 +135,11 @@ class FluxLight(Light):
|
|||||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
effect = kwargs.get(ATTR_EFFECT)
|
effect = kwargs.get(ATTR_EFFECT)
|
||||||
if rgb:
|
if rgb is not None and brightness is not None:
|
||||||
|
self._bulb.setRgb(*tuple(rgb), brightness=brightness)
|
||||||
|
elif rgb is not None:
|
||||||
self._bulb.setRgb(*tuple(rgb))
|
self._bulb.setRgb(*tuple(rgb))
|
||||||
elif brightness:
|
elif brightness is not None:
|
||||||
if self._mode == 'rgbw':
|
if self._mode == 'rgbw':
|
||||||
self._bulb.setWarmWhite255(brightness)
|
self._bulb.setWarmWhite255(brightness)
|
||||||
elif self._mode == 'rgb':
|
elif self._mode == 'rgb':
|
||||||
|
@ -22,11 +22,12 @@ from homeassistant.components.light import (
|
|||||||
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
|
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
|
||||||
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
|
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
|
||||||
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
|
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
|
from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['phue==0.8']
|
REQUIREMENTS = ['phue==0.9']
|
||||||
|
|
||||||
# Track previously setup bridges
|
# Track previously setup bridges
|
||||||
_CONFIGURED_BRIDGES = {}
|
_CONFIGURED_BRIDGES = {}
|
||||||
@ -37,6 +38,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
||||||
|
|
||||||
DEFAULT_ALLOW_UNREACHABLE = False
|
DEFAULT_ALLOW_UNREACHABLE = False
|
||||||
|
DOMAIN = "light"
|
||||||
|
SERVICE_HUE_SCENE = "hue_activate_scene"
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||||
@ -53,6 +56,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_FILENAME): cv.string,
|
vol.Optional(CONF_FILENAME): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ATTR_GROUP_NAME = "group_name"
|
||||||
|
ATTR_SCENE_NAME = "scene_name"
|
||||||
|
SCENE_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||||
|
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||||
"""Attempt to detect host based on existing configuration."""
|
"""Attempt to detect host based on existing configuration."""
|
||||||
@ -166,6 +176,21 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
|||||||
add_devices(new_lights)
|
add_devices(new_lights)
|
||||||
|
|
||||||
_CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
|
_CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
|
||||||
|
|
||||||
|
# create a service for calling run_scene directly on the bridge,
|
||||||
|
# used to simplify automation rules.
|
||||||
|
def hue_activate_scene(call):
|
||||||
|
"""Service to call directly directly into bridge to set scenes."""
|
||||||
|
group_name = call.data[ATTR_GROUP_NAME]
|
||||||
|
scene_name = call.data[ATTR_SCENE_NAME]
|
||||||
|
bridge.run_scene(group_name, scene_name)
|
||||||
|
|
||||||
|
descriptions = load_yaml_config_file(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
|
||||||
|
descriptions.get(SERVICE_HUE_SCENE),
|
||||||
|
schema=SCENE_SCHEMA)
|
||||||
|
|
||||||
update_lights()
|
update_lights()
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ class LiteJetLight(Light):
|
|||||||
def _on_load_changed(self):
|
def _on_load_changed(self):
|
||||||
"""Called on a LiteJet thread when a load's state changes."""
|
"""Called on a LiteJet thread when a load's state changes."""
|
||||||
_LOGGER.debug("Updating due to notification for %s", self._name)
|
_LOGGER.debug("Updating due to notification for %s", self._name)
|
||||||
self._hass.loop.create_task(self.async_update_ha_state(True))
|
self._hass.async_add_job(self.async_update_ha_state(True))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
252
homeassistant/components/light/mqtt_template.py
Executable file
252
homeassistant/components/light/mqtt_template.py
Executable file
@ -0,0 +1,252 @@
|
|||||||
|
"""
|
||||||
|
Support for MQTT Template lights.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.mqtt_template/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
|
||||||
|
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
|
||||||
|
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
||||||
|
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
|
||||||
|
from homeassistant.components.mqtt import (
|
||||||
|
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = 'mqtt_template'
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'MQTT Template Light'
|
||||||
|
DEFAULT_OPTIMISTIC = False
|
||||||
|
|
||||||
|
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
|
||||||
|
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
|
||||||
|
CONF_STATE_TEMPLATE = 'state_template'
|
||||||
|
CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
|
||||||
|
CONF_RED_TEMPLATE = 'red_template'
|
||||||
|
CONF_GREEN_TEMPLATE = 'green_template'
|
||||||
|
CONF_BLUE_TEMPLATE = 'blue_template'
|
||||||
|
|
||||||
|
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH |
|
||||||
|
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
|
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
|
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
|
||||||
|
vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_STATE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_RED_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||||
|
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
|
||||||
|
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
|
||||||
|
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup a MQTT Template light."""
|
||||||
|
add_devices([MqttTemplate(
|
||||||
|
hass,
|
||||||
|
config.get(CONF_NAME),
|
||||||
|
{
|
||||||
|
key: config.get(key) for key in (
|
||||||
|
CONF_STATE_TOPIC,
|
||||||
|
CONF_COMMAND_TOPIC
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: config.get(key) for key in (
|
||||||
|
CONF_COMMAND_ON_TEMPLATE,
|
||||||
|
CONF_COMMAND_OFF_TEMPLATE,
|
||||||
|
CONF_STATE_TEMPLATE,
|
||||||
|
CONF_BRIGHTNESS_TEMPLATE,
|
||||||
|
CONF_RED_TEMPLATE,
|
||||||
|
CONF_GREEN_TEMPLATE,
|
||||||
|
CONF_BLUE_TEMPLATE
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config.get(CONF_OPTIMISTIC),
|
||||||
|
config.get(CONF_QOS),
|
||||||
|
config.get(CONF_RETAIN)
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
class MqttTemplate(Light):
|
||||||
|
"""Representation of a MQTT Template light."""
|
||||||
|
|
||||||
|
def __init__(self, hass, name, topics, templates, optimistic, qos, retain):
|
||||||
|
"""Initialize MQTT Template light."""
|
||||||
|
self._hass = hass
|
||||||
|
self._name = name
|
||||||
|
self._topics = topics
|
||||||
|
self._templates = templates
|
||||||
|
for tpl in self._templates.values():
|
||||||
|
if tpl is not None:
|
||||||
|
tpl.hass = hass
|
||||||
|
self._optimistic = optimistic or topics[CONF_STATE_TOPIC] is None \
|
||||||
|
or templates[CONF_STATE_TEMPLATE] is None
|
||||||
|
self._qos = qos
|
||||||
|
self._retain = retain
|
||||||
|
|
||||||
|
# features
|
||||||
|
self._state = False
|
||||||
|
if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None:
|
||||||
|
self._brightness = 255
|
||||||
|
else:
|
||||||
|
self._brightness = None
|
||||||
|
|
||||||
|
if (self._templates[CONF_RED_TEMPLATE] is not None and
|
||||||
|
self._templates[CONF_GREEN_TEMPLATE] is not None and
|
||||||
|
self._templates[CONF_BLUE_TEMPLATE] is not None):
|
||||||
|
self._rgb = [0, 0, 0]
|
||||||
|
else:
|
||||||
|
self._rgb = None
|
||||||
|
|
||||||
|
def state_received(topic, payload, qos):
|
||||||
|
"""A new MQTT message has been received."""
|
||||||
|
# read state
|
||||||
|
state = self._templates[CONF_STATE_TEMPLATE].\
|
||||||
|
render_with_possible_json_value(payload)
|
||||||
|
if state == STATE_ON:
|
||||||
|
self._state = True
|
||||||
|
elif state == STATE_OFF:
|
||||||
|
self._state = False
|
||||||
|
else:
|
||||||
|
_LOGGER.warning('Invalid state value received')
|
||||||
|
|
||||||
|
# read brightness
|
||||||
|
if self._brightness is not None:
|
||||||
|
try:
|
||||||
|
self._brightness = int(
|
||||||
|
self._templates[CONF_BRIGHTNESS_TEMPLATE].
|
||||||
|
render_with_possible_json_value(payload)
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning('Invalid brightness value received')
|
||||||
|
|
||||||
|
# read color
|
||||||
|
if self._rgb is not None:
|
||||||
|
try:
|
||||||
|
self._rgb[0] = int(
|
||||||
|
self._templates[CONF_RED_TEMPLATE].
|
||||||
|
render_with_possible_json_value(payload))
|
||||||
|
self._rgb[1] = int(
|
||||||
|
self._templates[CONF_GREEN_TEMPLATE].
|
||||||
|
render_with_possible_json_value(payload))
|
||||||
|
self._rgb[2] = int(
|
||||||
|
self._templates[CONF_BLUE_TEMPLATE].
|
||||||
|
render_with_possible_json_value(payload))
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning('Invalid color value received')
|
||||||
|
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
if self._topics[CONF_STATE_TOPIC] is not None:
|
||||||
|
mqtt.subscribe(self._hass, self._topics[CONF_STATE_TOPIC],
|
||||||
|
state_received, self._qos)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light between 0..255."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self):
|
||||||
|
"""Return the RGB color value [int, int, int]."""
|
||||||
|
return self._rgb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Return True if entity has to be polled for state.
|
||||||
|
|
||||||
|
False if entity pushes its state to HA.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the entity."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return True if entity is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def assumed_state(self):
|
||||||
|
"""Return True if unable to access real state of the entity."""
|
||||||
|
return self._optimistic
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the entity on."""
|
||||||
|
# state
|
||||||
|
values = {'state': True}
|
||||||
|
if self._optimistic:
|
||||||
|
self._state = True
|
||||||
|
|
||||||
|
# brightness
|
||||||
|
if ATTR_BRIGHTNESS in kwargs:
|
||||||
|
values['brightness'] = int(kwargs[ATTR_BRIGHTNESS])
|
||||||
|
|
||||||
|
if self._optimistic:
|
||||||
|
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
|
|
||||||
|
# color
|
||||||
|
if ATTR_RGB_COLOR in kwargs:
|
||||||
|
values['red'] = kwargs[ATTR_RGB_COLOR][0]
|
||||||
|
values['green'] = kwargs[ATTR_RGB_COLOR][1]
|
||||||
|
values['blue'] = kwargs[ATTR_RGB_COLOR][2]
|
||||||
|
|
||||||
|
if self._optimistic:
|
||||||
|
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||||
|
|
||||||
|
# flash
|
||||||
|
if ATTR_FLASH in kwargs:
|
||||||
|
values['flash'] = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
# transition
|
||||||
|
if ATTR_TRANSITION in kwargs:
|
||||||
|
values['transition'] = kwargs[ATTR_TRANSITION]
|
||||||
|
|
||||||
|
mqtt.publish(
|
||||||
|
self._hass, self._topics[CONF_COMMAND_TOPIC],
|
||||||
|
self._templates[CONF_COMMAND_ON_TEMPLATE].render(**values),
|
||||||
|
self._qos, self._retain
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._optimistic:
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the entity off."""
|
||||||
|
# state
|
||||||
|
values = {'state': False}
|
||||||
|
if self._optimistic:
|
||||||
|
self._state = False
|
||||||
|
|
||||||
|
# transition
|
||||||
|
if ATTR_TRANSITION in kwargs:
|
||||||
|
values['transition'] = kwargs[ATTR_TRANSITION]
|
||||||
|
|
||||||
|
mqtt.publish(
|
||||||
|
self._hass, self._topics[CONF_COMMAND_TOPIC],
|
||||||
|
self._templates[CONF_COMMAND_OFF_TEMPLATE].render(**values),
|
||||||
|
self._qos, self._retain
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._optimistic:
|
||||||
|
self.update_ha_state()
|
@ -31,7 +31,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||||
|
if not gateways:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in gateways:
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# states. Map them in a dict of lists.
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
|
@ -81,3 +81,15 @@ toggle:
|
|||||||
transition:
|
transition:
|
||||||
description: Duration in seconds it takes to get to next state
|
description: Duration in seconds it takes to get to next state
|
||||||
example: 60
|
example: 60
|
||||||
|
|
||||||
|
hue_activate_scene:
|
||||||
|
description: Activate a hue scene stored in the hue hub
|
||||||
|
|
||||||
|
fields:
|
||||||
|
group_name:
|
||||||
|
description: Name of hue group/room from the hue app
|
||||||
|
example: "Living Room"
|
||||||
|
|
||||||
|
scene_name:
|
||||||
|
description: Name of hue scene from the hue app
|
||||||
|
example: "Energize"
|
||||||
|
@ -41,7 +41,10 @@ class WinkLight(WinkDevice, Light):
|
|||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of the light."""
|
"""Return the brightness of the light."""
|
||||||
|
if self.wink.brightness() is not None:
|
||||||
return int(self.wink.brightness() * 255)
|
return int(self.wink.brightness() * 255)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rgb_color(self):
|
def rgb_color(self):
|
||||||
@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light):
|
|||||||
hue = self.wink.color_hue()
|
hue = self.wink.color_hue()
|
||||||
saturation = self.wink.color_saturation()
|
saturation = self.wink.color_saturation()
|
||||||
value = int(self.wink.brightness() * 255)
|
value = int(self.wink.brightness() * 255)
|
||||||
|
if hue is None or saturation is None or value is None:
|
||||||
|
return None
|
||||||
rgb = colorsys.hsv_to_rgb(hue, saturation, value)
|
rgb = colorsys.hsv_to_rgb(hue, saturation, value)
|
||||||
r_value = int(round(rgb[0]))
|
r_value = int(round(rgb[0]))
|
||||||
g_value = int(round(rgb[1]))
|
g_value = int(round(rgb[1]))
|
||||||
|
@ -24,26 +24,6 @@ AEOTEC = 0x86
|
|||||||
AEOTEC_ZW098_LED_BULB = 0x62
|
AEOTEC_ZW098_LED_BULB = 0x62
|
||||||
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
|
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
|
||||||
|
|
||||||
LINEAR = 0x14f
|
|
||||||
LINEAR_WD500Z_DIMMER = 0x3034
|
|
||||||
LINEAR_WD500Z_DIMMER_LIGHT = (LINEAR, LINEAR_WD500Z_DIMMER)
|
|
||||||
|
|
||||||
GE = 0x63
|
|
||||||
GE_12724_DIMMER = 0x3031
|
|
||||||
GE_12724_DIMMER_LIGHT = (GE, GE_12724_DIMMER)
|
|
||||||
|
|
||||||
DRAGONTECH = 0x184
|
|
||||||
DRAGONTECH_PD100_DIMMER = 0x3032
|
|
||||||
DRAGONTECH_PD100_DIMMER_LIGHT = (DRAGONTECH, DRAGONTECH_PD100_DIMMER)
|
|
||||||
|
|
||||||
ACT = 0x01
|
|
||||||
ACT_ZDP100_DIMMER = 0x3030
|
|
||||||
ACT_ZDP100_DIMMER_LIGHT = (ACT, ACT_ZDP100_DIMMER)
|
|
||||||
|
|
||||||
HOMESEER = 0x0c
|
|
||||||
HOMESEER_WD100_DIMMER = 0x3034
|
|
||||||
HOMESEER_WD100_DIMMER_LIGHT = (HOMESEER, HOMESEER_WD100_DIMMER)
|
|
||||||
|
|
||||||
COLOR_CHANNEL_WARM_WHITE = 0x01
|
COLOR_CHANNEL_WARM_WHITE = 0x01
|
||||||
COLOR_CHANNEL_COLD_WHITE = 0x02
|
COLOR_CHANNEL_COLD_WHITE = 0x02
|
||||||
COLOR_CHANNEL_RED = 0x04
|
COLOR_CHANNEL_RED = 0x04
|
||||||
@ -51,15 +31,9 @@ COLOR_CHANNEL_GREEN = 0x08
|
|||||||
COLOR_CHANNEL_BLUE = 0x10
|
COLOR_CHANNEL_BLUE = 0x10
|
||||||
|
|
||||||
WORKAROUND_ZW098 = 'zw098'
|
WORKAROUND_ZW098 = 'zw098'
|
||||||
WORKAROUND_DELAY = 'alt_delay'
|
|
||||||
|
|
||||||
DEVICE_MAPPINGS = {
|
DEVICE_MAPPINGS = {
|
||||||
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098,
|
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
|
||||||
LINEAR_WD500Z_DIMMER_LIGHT: WORKAROUND_DELAY,
|
|
||||||
GE_12724_DIMMER_LIGHT: WORKAROUND_DELAY,
|
|
||||||
DRAGONTECH_PD100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
|
||||||
ACT_ZDP100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
|
||||||
HOMESEER_WD100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate midpoint color temperatures for bulbs that have limited
|
# Generate midpoint color temperatures for bulbs that have limited
|
||||||
@ -75,10 +49,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Find and add Z-Wave lights."""
|
"""Find and add Z-Wave lights."""
|
||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
customize = hass.data['zwave_customize']
|
||||||
|
name = super().entity_id
|
||||||
|
node_config = customize.get(name, {})
|
||||||
|
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
|
||||||
|
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||||
|
_LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
||||||
|
' CONF_REFRESH_DELAY=%s', customize, name, node_config,
|
||||||
|
refresh, delay)
|
||||||
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||||
return
|
return
|
||||||
if value.type != zwave.const.TYPE_BYTE:
|
if value.type != zwave.const.TYPE_BYTE:
|
||||||
@ -89,9 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
|
||||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||||
add_devices([ZwaveColorLight(value)])
|
add_devices([ZwaveColorLight(value, refresh, delay)])
|
||||||
else:
|
else:
|
||||||
add_devices([ZwaveDimmer(value)])
|
add_devices([ZwaveDimmer(value, refresh, delay)])
|
||||||
|
|
||||||
|
|
||||||
def brightness_state(value):
|
def brightness_state(value):
|
||||||
@ -105,7 +85,7 @@ def brightness_state(value):
|
|||||||
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||||
"""Representation of a Z-Wave dimmer."""
|
"""Representation of a Z-Wave dimmer."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, value, refresh, delay):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
@ -113,7 +93,8 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||||
self._brightness = None
|
self._brightness = None
|
||||||
self._state = None
|
self._state = None
|
||||||
self._alt_delay = None
|
self._delay = delay
|
||||||
|
self._refresh_value = refresh
|
||||||
self._zw098 = None
|
self._zw098 = None
|
||||||
|
|
||||||
# Enable appropriate workaround flags for our device
|
# Enable appropriate workaround flags for our device
|
||||||
@ -126,17 +107,14 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||||
self._zw098 = 1
|
self._zw098 = 1
|
||||||
elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_DELAY:
|
|
||||||
_LOGGER.debug("Dimmer delay workaround enabled for node:"
|
|
||||||
" %s", value.parent_id)
|
|
||||||
self._alt_delay = 1
|
|
||||||
|
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
|
|
||||||
# Used for value change event handling
|
# Used for value change event handling
|
||||||
self._refreshing = False
|
self._refreshing = False
|
||||||
self._timer = None
|
self._timer = None
|
||||||
|
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
||||||
|
self._refresh_value, self._delay)
|
||||||
dispatcher.connect(
|
dispatcher.connect(
|
||||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||||
|
|
||||||
@ -149,7 +127,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
"""Called when a value has changed on the network."""
|
"""Called when a value has changed on the network."""
|
||||||
if self._value.value_id == value.value_id or \
|
if self._value.value_id == value.value_id or \
|
||||||
self._value.node == value.node:
|
self._value.node == value.node:
|
||||||
|
if self._refresh_value:
|
||||||
if self._refreshing:
|
if self._refreshing:
|
||||||
self._refreshing = False
|
self._refreshing = False
|
||||||
self.update_properties()
|
self.update_properties()
|
||||||
@ -162,12 +140,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||||||
if self._timer is not None and self._timer.isAlive():
|
if self._timer is not None and self._timer.isAlive():
|
||||||
self._timer.cancel()
|
self._timer.cancel()
|
||||||
|
|
||||||
if self._alt_delay:
|
self._timer = Timer(self._delay, _refresh_value)
|
||||||
self._timer = Timer(5, _refresh_value)
|
|
||||||
else:
|
|
||||||
self._timer = Timer(2, _refresh_value)
|
|
||||||
self._timer.start()
|
self._timer.start()
|
||||||
|
self.update_ha_state()
|
||||||
|
else:
|
||||||
|
self.update_properties()
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -213,7 +190,7 @@ def ct_to_rgb(temp):
|
|||||||
class ZwaveColorLight(ZwaveDimmer):
|
class ZwaveColorLight(ZwaveDimmer):
|
||||||
"""Representation of a Z-Wave color changing light."""
|
"""Representation of a Z-Wave color changing light."""
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, value, refresh, delay):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
|
@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/aparraga/braviarc/archive/0.3.5.zip'
|
'https://github.com/aparraga/braviarc/archive/0.3.6.zip'
|
||||||
'#braviarc==0.3.5']
|
'#braviarc==0.3.6']
|
||||||
|
|
||||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||||
|
|
||||||
|
@ -280,9 +280,9 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
def new_cast_status(self, status):
|
def new_cast_status(self, status):
|
||||||
"""Called when a new cast status is received."""
|
"""Called when a new cast status is received."""
|
||||||
self.cast_status = status
|
self.cast_status = status
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def new_media_status(self, status):
|
def new_media_status(self, status):
|
||||||
"""Called when a new media status is received."""
|
"""Called when a new media status is received."""
|
||||||
self.media_status = status
|
self.media_status = status
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@ -104,7 +104,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
if len(self._players) == 0:
|
if len(self._players) == 0:
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
|
|
||||||
if self._properties['speed'] == 0:
|
if self._properties['speed'] == 0 and not self._properties['live']:
|
||||||
return STATE_PAUSED
|
return STATE_PAUSED
|
||||||
else:
|
else:
|
||||||
return STATE_PLAYING
|
return STATE_PLAYING
|
||||||
@ -120,7 +120,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
self._properties = self._server.Player.GetProperties(
|
self._properties = self._server.Player.GetProperties(
|
||||||
player_id,
|
player_id,
|
||||||
['time', 'totaltime', 'speed']
|
['time', 'totaltime', 'speed', 'live']
|
||||||
)
|
)
|
||||||
|
|
||||||
self._item = self._server.Player.GetItem(
|
self._item = self._server.Player.GetItem(
|
||||||
@ -163,7 +163,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
"""Duration of current playing media in seconds."""
|
"""Duration of current playing media in seconds."""
|
||||||
if self._properties is not None:
|
if self._properties is not None and not self._properties['live']:
|
||||||
total_time = self._properties['totaltime']
|
total_time = self._properties['totaltime']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -128,6 +128,8 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
self.send_key('KEY_POWEROFF')
|
self.send_key('KEY_POWEROFF')
|
||||||
|
# Force closing of remote session to provide instant UI feedback
|
||||||
|
self.get_remote().close()
|
||||||
|
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
"""Volume up the media player."""
|
"""Volume up the media player."""
|
||||||
|
@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
player = soco.SoCo(discovery_info)
|
player = soco.SoCo(discovery_info)
|
||||||
|
|
||||||
# if device allready exists by config
|
# if device allready exists by config
|
||||||
if player.uid in DEVICES:
|
if player.uid in [x.unique_id for x in DEVICES]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if player.is_visible:
|
if player.is_visible:
|
||||||
@ -350,9 +350,6 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
if is_available:
|
if is_available:
|
||||||
|
|
||||||
self._is_playing_tv = self._player.is_playing_tv
|
|
||||||
self._is_playing_line_in = self._player.is_playing_line_in
|
|
||||||
|
|
||||||
track_info = None
|
track_info = None
|
||||||
if self._last_avtransport_event:
|
if self._last_avtransport_event:
|
||||||
variables = self._last_avtransport_event.variables
|
variables = self._last_avtransport_event.variables
|
||||||
@ -394,6 +391,10 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._coordinator = None
|
self._coordinator = None
|
||||||
|
|
||||||
if not self._coordinator:
|
if not self._coordinator:
|
||||||
|
|
||||||
|
is_playing_tv = self._player.is_playing_tv
|
||||||
|
is_playing_line_in = self._player.is_playing_line_in
|
||||||
|
|
||||||
media_info = self._player.avTransport.GetMediaInfo(
|
media_info = self._player.avTransport.GetMediaInfo(
|
||||||
[('InstanceID', 0)]
|
[('InstanceID', 0)]
|
||||||
)
|
)
|
||||||
@ -407,7 +408,23 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
current_media_uri.startswith('x-sonosapi-stream:') or \
|
current_media_uri.startswith('x-sonosapi-stream:') or \
|
||||||
current_media_uri.startswith('x-rincon-mp3radio:')
|
current_media_uri.startswith('x-rincon-mp3radio:')
|
||||||
|
|
||||||
if is_radio_stream:
|
if is_playing_tv or is_playing_line_in:
|
||||||
|
# playing from line-in/tv.
|
||||||
|
|
||||||
|
support_previous_track = False
|
||||||
|
support_next_track = False
|
||||||
|
support_pause = False
|
||||||
|
|
||||||
|
if is_playing_tv:
|
||||||
|
media_artist = SUPPORT_SOURCE_TV
|
||||||
|
else:
|
||||||
|
media_artist = SUPPORT_SOURCE_LINEIN
|
||||||
|
|
||||||
|
media_album_name = None
|
||||||
|
media_title = None
|
||||||
|
media_image_url = None
|
||||||
|
|
||||||
|
elif is_radio_stream:
|
||||||
is_radio_stream = True
|
is_radio_stream = True
|
||||||
media_image_url = self._format_media_image_url(
|
media_image_url = self._format_media_image_url(
|
||||||
current_media_uri
|
current_media_uri
|
||||||
@ -506,6 +523,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._support_previous_track = support_previous_track
|
self._support_previous_track = support_previous_track
|
||||||
self._support_next_track = support_next_track
|
self._support_next_track = support_next_track
|
||||||
self._support_pause = support_pause
|
self._support_pause = support_pause
|
||||||
|
self._is_playing_tv = is_playing_tv
|
||||||
|
self._is_playing_line_in = is_playing_line_in
|
||||||
|
|
||||||
# update state of the whole group
|
# update state of the whole group
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
@ -513,7 +532,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
if device.entity_id is not self.entity_id:
|
if device.entity_id is not self.entity_id:
|
||||||
self.hass.add_job(device.async_update_ha_state)
|
self.hass.add_job(device.async_update_ha_state)
|
||||||
|
|
||||||
if self._queue is None:
|
if self._queue is None and self.entity_id is not None:
|
||||||
self._subscribe_to_player_events()
|
self._subscribe_to_player_events()
|
||||||
else:
|
else:
|
||||||
self._player_volume = None
|
self._player_volume = None
|
||||||
@ -714,9 +733,12 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
"""Name of the current input source."""
|
"""Name of the current input source."""
|
||||||
|
if self._coordinator:
|
||||||
|
return self._coordinator.source
|
||||||
|
else:
|
||||||
if self._is_playing_line_in:
|
if self._is_playing_line_in:
|
||||||
return SUPPORT_SOURCE_LINEIN
|
return SUPPORT_SOURCE_LINEIN
|
||||||
if self._is_playing_tv:
|
elif self._is_playing_tv:
|
||||||
return SUPPORT_SOURCE_TV
|
return SUPPORT_SOURCE_TV
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -111,9 +111,11 @@ class LogitechMediaServer(object):
|
|||||||
|
|
||||||
def query(self, *parameters):
|
def query(self, *parameters):
|
||||||
"""Send request and await response from server."""
|
"""Send request and await response from server."""
|
||||||
response = urllib.parse.unquote(self.get(' '.join(parameters)))
|
response = self.get(' '.join(parameters))
|
||||||
|
response = response.split(' ')[-1].strip()
|
||||||
|
response = urllib.parse.unquote(response)
|
||||||
|
|
||||||
return response.split(' ')[-1].strip()
|
return response
|
||||||
|
|
||||||
def get_player_status(self, player):
|
def get_player_status(self, player):
|
||||||
"""Get the status of a player."""
|
"""Get the status of a player."""
|
||||||
|
@ -18,17 +18,12 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
|
|||||||
STATE_PLAYING, STATE_IDLE)
|
STATE_PLAYING, STATE_IDLE)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['rxv==0.3.1']
|
REQUIREMENTS = ['rxv==0.4.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||||
SUPPORT_PLAY_MEDIA
|
|
||||||
|
|
||||||
# Only supported by some sources
|
|
||||||
SUPPORT_PLAYBACK = SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_STOP | \
|
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
|
||||||
|
|
||||||
CONF_SOURCE_NAMES = 'source_names'
|
CONF_SOURCE_NAMES = 'source_names'
|
||||||
CONF_SOURCE_IGNORE = 'source_ignore'
|
CONF_SOURCE_IGNORE = 'source_ignore'
|
||||||
@ -187,8 +182,16 @@ class YamahaDevice(MediaPlayerDevice):
|
|||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
supported_commands = SUPPORT_YAMAHA
|
supported_commands = SUPPORT_YAMAHA
|
||||||
if self._is_playback_supported:
|
|
||||||
supported_commands |= SUPPORT_PLAYBACK
|
supports = self._receiver.get_playback_support()
|
||||||
|
mapping = {'play': SUPPORT_PLAY_MEDIA,
|
||||||
|
'pause': SUPPORT_PAUSE,
|
||||||
|
'stop': SUPPORT_STOP,
|
||||||
|
'skip_f': SUPPORT_NEXT_TRACK,
|
||||||
|
'skip_r': SUPPORT_PREVIOUS_TRACK}
|
||||||
|
for attr, feature in mapping.items():
|
||||||
|
if getattr(supports, attr, False):
|
||||||
|
supported_commands |= feature
|
||||||
return supported_commands
|
return supported_commands
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
|
@ -18,11 +18,12 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers import template, config_validation as cv
|
from homeassistant.helpers import template, config_validation as cv
|
||||||
from homeassistant.helpers.event import threaded_listener_factory
|
from homeassistant.helpers.event import threaded_listener_factory
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE)
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE,
|
||||||
|
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "mqtt"
|
DOMAIN = 'mqtt'
|
||||||
|
|
||||||
MQTT_CLIENT = None
|
MQTT_CLIENT = None
|
||||||
|
|
||||||
@ -33,16 +34,15 @@ REQUIREMENTS = ['paho-mqtt==1.2']
|
|||||||
|
|
||||||
CONF_EMBEDDED = 'embedded'
|
CONF_EMBEDDED = 'embedded'
|
||||||
CONF_BROKER = 'broker'
|
CONF_BROKER = 'broker'
|
||||||
CONF_PORT = 'port'
|
|
||||||
CONF_CLIENT_ID = 'client_id'
|
CONF_CLIENT_ID = 'client_id'
|
||||||
CONF_KEEPALIVE = 'keepalive'
|
CONF_KEEPALIVE = 'keepalive'
|
||||||
CONF_USERNAME = 'username'
|
|
||||||
CONF_PASSWORD = 'password'
|
|
||||||
CONF_CERTIFICATE = 'certificate'
|
CONF_CERTIFICATE = 'certificate'
|
||||||
CONF_CLIENT_KEY = 'client_key'
|
CONF_CLIENT_KEY = 'client_key'
|
||||||
CONF_CLIENT_CERT = 'client_cert'
|
CONF_CLIENT_CERT = 'client_cert'
|
||||||
CONF_TLS_INSECURE = 'tls_insecure'
|
CONF_TLS_INSECURE = 'tls_insecure'
|
||||||
CONF_PROTOCOL = 'protocol'
|
|
||||||
|
CONF_BIRTH_MESSAGE = 'birth_message'
|
||||||
|
CONF_WILL_MESSAGE = 'will_message'
|
||||||
|
|
||||||
CONF_STATE_TOPIC = 'state_topic'
|
CONF_STATE_TOPIC = 'state_topic'
|
||||||
CONF_COMMAND_TOPIC = 'command_topic'
|
CONF_COMMAND_TOPIC = 'command_topic'
|
||||||
@ -78,20 +78,27 @@ def valid_publish_topic(value):
|
|||||||
"""Validate that we can publish using this MQTT topic."""
|
"""Validate that we can publish using this MQTT topic."""
|
||||||
return valid_subscribe_topic(value, invalid_chars='#+\0')
|
return valid_subscribe_topic(value, invalid_chars='#+\0')
|
||||||
|
|
||||||
|
|
||||||
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
||||||
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
|
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
|
||||||
|
|
||||||
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
|
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
|
||||||
'the mqtt broker config'
|
'the mqtt broker config'
|
||||||
|
|
||||||
|
MQTT_WILL_BIRTH_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
||||||
|
vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string,
|
||||||
|
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||||
|
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||||
|
}, required=True)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||||
vol.Optional(CONF_BROKER): cv.string,
|
vol.Optional(CONF_BROKER): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
||||||
@ -103,6 +110,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||||
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
|
vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA,
|
||||||
|
vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA,
|
||||||
|
vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA
|
||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -130,10 +139,10 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
|
|||||||
# Service call validation schema
|
# Service call validation schema
|
||||||
MQTT_PUBLISH_SCHEMA = vol.Schema({
|
MQTT_PUBLISH_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
||||||
vol.Exclusive(ATTR_PAYLOAD, 'payload'): object,
|
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object,
|
||||||
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string,
|
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string,
|
||||||
vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||||
vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||||
}, required=True)
|
}, required=True)
|
||||||
|
|
||||||
|
|
||||||
@ -196,7 +205,7 @@ def _setup_server(hass, config):
|
|||||||
server = prepare_setup_platform(hass, config, DOMAIN, 'server')
|
server = prepare_setup_platform(hass, config, DOMAIN, 'server')
|
||||||
|
|
||||||
if server is None:
|
if server is None:
|
||||||
_LOGGER.error('Unable to load embedded server.')
|
_LOGGER.error("Unable to load embedded server")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED))
|
success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED))
|
||||||
@ -221,7 +230,7 @@ def setup(hass, config):
|
|||||||
# Embedded broker doesn't have some ssl variables
|
# Embedded broker doesn't have some ssl variables
|
||||||
client_key, client_cert, tls_insecure = None, None, None
|
client_key, client_cert, tls_insecure = None, None, None
|
||||||
elif not broker_config and not broker_in_conf:
|
elif not broker_config and not broker_in_conf:
|
||||||
_LOGGER.error('Unable to start broker and auto-configure MQTT.')
|
_LOGGER.error("Unable to start broker and auto-configure MQTT")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if broker_in_conf:
|
if broker_in_conf:
|
||||||
@ -241,15 +250,18 @@ def setup(hass, config):
|
|||||||
certificate = os.path.join(os.path.dirname(__file__),
|
certificate = os.path.join(os.path.dirname(__file__),
|
||||||
'addtrustexternalcaroot.crt')
|
'addtrustexternalcaroot.crt')
|
||||||
|
|
||||||
|
will_message = conf.get(CONF_WILL_MESSAGE)
|
||||||
|
birth_message = conf.get(CONF_BIRTH_MESSAGE)
|
||||||
|
|
||||||
global MQTT_CLIENT
|
global MQTT_CLIENT
|
||||||
try:
|
try:
|
||||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
|
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
|
||||||
username, password, certificate, client_key,
|
username, password, certificate, client_key,
|
||||||
client_cert, tls_insecure, protocol)
|
client_cert, tls_insecure, protocol, will_message,
|
||||||
|
birth_message)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
_LOGGER.exception("Can't connect to the broker. "
|
_LOGGER.exception("Can't connect to the broker. "
|
||||||
"Please check your settings and the broker "
|
"Please check your settings and the broker itself")
|
||||||
"itself.")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop_mqtt(event):
|
def stop_mqtt(event):
|
||||||
@ -274,7 +286,7 @@ def setup(hass, config):
|
|||||||
except template.jinja2.TemplateError as exc:
|
except template.jinja2.TemplateError as exc:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to publish to '%s': rendering payload template of "
|
"Unable to publish to '%s': rendering payload template of "
|
||||||
"'%s' failed because %s.",
|
"'%s' failed because %s",
|
||||||
msg_topic, payload_template, exc)
|
msg_topic, payload_template, exc)
|
||||||
return
|
return
|
||||||
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
MQTT_CLIENT.publish(msg_topic, payload, qos, retain)
|
||||||
@ -296,13 +308,14 @@ class MQTT(object):
|
|||||||
|
|
||||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||||
password, certificate, client_key, client_cert,
|
password, certificate, client_key, client_cert,
|
||||||
tls_insecure, protocol):
|
tls_insecure, protocol, will_message, birth_message):
|
||||||
"""Initialize Home Assistant MQTT client."""
|
"""Initialize Home Assistant MQTT client."""
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.topics = {}
|
self.topics = {}
|
||||||
self.progress = {}
|
self.progress = {}
|
||||||
|
self.birth_message = birth_message
|
||||||
|
|
||||||
if protocol == PROTOCOL_31:
|
if protocol == PROTOCOL_31:
|
||||||
proto = mqtt.MQTTv31
|
proto = mqtt.MQTTv31
|
||||||
@ -329,7 +342,11 @@ class MQTT(object):
|
|||||||
self._mqttc.on_connect = self._mqtt_on_connect
|
self._mqttc.on_connect = self._mqtt_on_connect
|
||||||
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
||||||
self._mqttc.on_message = self._mqtt_on_message
|
self._mqttc.on_message = self._mqtt_on_message
|
||||||
|
if will_message:
|
||||||
|
self._mqttc.will_set(will_message.get(ATTR_TOPIC),
|
||||||
|
will_message.get(ATTR_PAYLOAD),
|
||||||
|
will_message.get(ATTR_QOS),
|
||||||
|
will_message.get(ATTR_RETAIN))
|
||||||
self._mqttc.connect(broker, port, keepalive)
|
self._mqttc.connect(broker, port, keepalive)
|
||||||
|
|
||||||
def publish(self, topic, payload, qos, retain):
|
def publish(self, topic, payload, qos, retain):
|
||||||
@ -365,7 +382,8 @@ class MQTT(object):
|
|||||||
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
|
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
|
||||||
"""On connect callback.
|
"""On connect callback.
|
||||||
|
|
||||||
Resubscribe to all topics we were subscribed to.
|
Resubscribe to all topics we were subscribed to and publish birth
|
||||||
|
message.
|
||||||
"""
|
"""
|
||||||
if result_code != 0:
|
if result_code != 0:
|
||||||
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
_LOGGER.error('Unable to connect to the MQTT broker: %s', {
|
||||||
@ -387,6 +405,11 @@ class MQTT(object):
|
|||||||
# qos is None if we were in process of subscribing
|
# qos is None if we were in process of subscribing
|
||||||
if qos is not None:
|
if qos is not None:
|
||||||
self.subscribe(topic, qos)
|
self.subscribe(topic, qos)
|
||||||
|
if self.birth_message:
|
||||||
|
self.publish(self.birth_message.get(ATTR_TOPIC),
|
||||||
|
self.birth_message.get(ATTR_PAYLOAD),
|
||||||
|
self.birth_message.get(ATTR_QOS),
|
||||||
|
self.birth_message.get(ATTR_RETAIN))
|
||||||
|
|
||||||
def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos):
|
def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos):
|
||||||
"""Subscribe successful callback."""
|
"""Subscribe successful callback."""
|
||||||
@ -404,7 +427,7 @@ class MQTT(object):
|
|||||||
"MQTT topic: %s, Payload: %s", msg.topic,
|
"MQTT topic: %s, Payload: %s", msg.topic,
|
||||||
msg.payload)
|
msg.payload)
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("received message on %s: %s",
|
_LOGGER.debug("Received message on %s: %s",
|
||||||
msg.topic, payload)
|
msg.topic, payload)
|
||||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||||
ATTR_TOPIC: msg.topic,
|
ATTR_TOPIC: msg.topic,
|
||||||
@ -440,14 +463,14 @@ class MQTT(object):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self._mqttc.reconnect() == 0:
|
if self._mqttc.reconnect() == 0:
|
||||||
_LOGGER.info('Successfully reconnected to the MQTT server')
|
_LOGGER.info("Successfully reconnected to the MQTT server")
|
||||||
break
|
break
|
||||||
except socket.error:
|
except socket.error:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
|
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
'Disconnected from MQTT (%s). Trying to reconnect in %ss',
|
"Disconnected from MQTT (%s). Trying to reconnect in %s s",
|
||||||
result_code, wait_time)
|
result_code, wait_time)
|
||||||
# It is ok to sleep here as we are in the MQTT thread.
|
# It is ok to sleep here as we are in the MQTT thread.
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
|
@ -4,41 +4,21 @@ Support for a local MQTT broker.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/mqtt/#use-the-embedded-broker
|
https://home-assistant.io/components/mqtt/#use-the-embedded-broker
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.mqtt import PROTOCOL_311
|
from homeassistant.components.mqtt import PROTOCOL_311
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
REQUIREMENTS = ['hbmqtt==0.7.1']
|
REQUIREMENTS = ['hbmqtt==0.7.1']
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
|
||||||
def broker_coro(loop, config):
|
|
||||||
"""Start broker coroutine."""
|
|
||||||
from hbmqtt.broker import Broker
|
|
||||||
broker = Broker(config, loop)
|
|
||||||
yield from broker.start()
|
|
||||||
return broker
|
|
||||||
|
|
||||||
|
|
||||||
def loop_run(loop, broker, shutdown_complete):
|
|
||||||
"""Run broker and clean up when done."""
|
|
||||||
loop.run_forever()
|
|
||||||
# run_forever ends when stop is called because we're shutting down
|
|
||||||
loop.run_until_complete(broker.shutdown())
|
|
||||||
loop.close()
|
|
||||||
shutdown_complete.set()
|
|
||||||
|
|
||||||
|
|
||||||
def start(hass, server_config):
|
def start(hass, server_config):
|
||||||
"""Initialize MQTT Server."""
|
"""Initialize MQTT Server."""
|
||||||
from hbmqtt.broker import BrokerException
|
from hbmqtt.broker import Broker, BrokerException
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
passwd = tempfile.NamedTemporaryFile()
|
passwd = tempfile.NamedTemporaryFile()
|
||||||
@ -48,29 +28,20 @@ def start(hass, server_config):
|
|||||||
else:
|
else:
|
||||||
client_config = None
|
client_config = None
|
||||||
|
|
||||||
start_server = asyncio.gather(broker_coro(loop, server_config),
|
broker = Broker(server_config, hass.loop)
|
||||||
loop=loop)
|
run_coroutine_threadsafe(broker.start(), hass.loop).result()
|
||||||
loop.run_until_complete(start_server)
|
|
||||||
# Result raises exception if one was raised during startup
|
|
||||||
broker = start_server.result()[0]
|
|
||||||
except BrokerException:
|
except BrokerException:
|
||||||
logging.getLogger(__name__).exception('Error initializing MQTT server')
|
logging.getLogger(__name__).exception('Error initializing MQTT server')
|
||||||
loop.close()
|
|
||||||
return False, None
|
return False, None
|
||||||
finally:
|
finally:
|
||||||
passwd.close()
|
passwd.close()
|
||||||
|
|
||||||
shutdown_complete = threading.Event()
|
@callback
|
||||||
|
def shutdown_mqtt_server(event):
|
||||||
|
"""Shut down the MQTT server."""
|
||||||
|
hass.async_add_job(broker.shutdown())
|
||||||
|
|
||||||
def shutdown(event):
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mqtt_server)
|
||||||
"""Gracefully shutdown MQTT broker."""
|
|
||||||
loop.call_soon_threadsafe(loop.stop)
|
|
||||||
shutdown_complete.wait()
|
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
|
|
||||||
|
|
||||||
threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete),
|
|
||||||
name="MQTT-server").start()
|
|
||||||
|
|
||||||
return True, client_config
|
return True, client_config
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ DEFAULT_VERSION = 1.4
|
|||||||
DEFAULT_BAUD_RATE = 115200
|
DEFAULT_BAUD_RATE = 115200
|
||||||
DEFAULT_TCP_PORT = 5003
|
DEFAULT_TCP_PORT = 5003
|
||||||
DOMAIN = 'mysensors'
|
DOMAIN = 'mysensors'
|
||||||
GATEWAYS = None
|
MYSENSORS_GATEWAYS = 'mysensors_gateways'
|
||||||
MQTT_COMPONENT = 'mqtt'
|
MQTT_COMPONENT = 'mqtt'
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/theolind/pymysensors/archive/'
|
'https://github.com/theolind/pymysensors/archive/'
|
||||||
@ -132,9 +132,15 @@ def setup(hass, config):
|
|||||||
|
|
||||||
return gateway
|
return gateway
|
||||||
|
|
||||||
|
gateways = hass.data.get(MYSENSORS_GATEWAYS)
|
||||||
|
if gateways is not None:
|
||||||
|
_LOGGER.error(
|
||||||
|
'%s already exists in %s, will not setup %s component',
|
||||||
|
MYSENSORS_GATEWAYS, hass.data, DOMAIN)
|
||||||
|
return False
|
||||||
|
|
||||||
# Setup all devices from config
|
# Setup all devices from config
|
||||||
global GATEWAYS
|
gateways = []
|
||||||
GATEWAYS = {}
|
|
||||||
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
|
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
|
||||||
|
|
||||||
for index, gway in enumerate(conf_gateways):
|
for index, gway in enumerate(conf_gateways):
|
||||||
@ -146,17 +152,19 @@ def setup(hass, config):
|
|||||||
tcp_port = gway.get(CONF_TCP_PORT)
|
tcp_port = gway.get(CONF_TCP_PORT)
|
||||||
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX)
|
in_prefix = gway.get(CONF_TOPIC_IN_PREFIX)
|
||||||
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX)
|
out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX)
|
||||||
GATEWAYS[device] = setup_gateway(
|
ready_gateway = setup_gateway(
|
||||||
device, persistence_file, baud_rate, tcp_port, in_prefix,
|
device, persistence_file, baud_rate, tcp_port, in_prefix,
|
||||||
out_prefix)
|
out_prefix)
|
||||||
if GATEWAYS[device] is None:
|
if ready_gateway is not None:
|
||||||
GATEWAYS.pop(device)
|
gateways.append(ready_gateway)
|
||||||
|
|
||||||
if not GATEWAYS:
|
if not gateways:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'No devices could be setup as gateways, check your configuration')
|
'No devices could be setup as gateways, check your configuration')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
hass.data[MYSENSORS_GATEWAYS] = gateways
|
||||||
|
|
||||||
for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
|
for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
|
||||||
'cover']:
|
'cover']:
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||||
|
81
homeassistant/components/neato.py
Normal file
81
homeassistant/components/neato.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""
|
||||||
|
Support for Neato botvac connected vacuum cleaners.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/neato/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip'
|
||||||
|
'#pybotvac==0.0.1']
|
||||||
|
|
||||||
|
DOMAIN = 'neato'
|
||||||
|
NEATO_ROBOTS = 'neato_robots'
|
||||||
|
NEATO_LOGIN = 'neato_login'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
})
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Setup the Verisure component."""
|
||||||
|
from pybotvac import Account
|
||||||
|
|
||||||
|
hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account)
|
||||||
|
hub = hass.data[NEATO_LOGIN]
|
||||||
|
if not hub.login():
|
||||||
|
_LOGGER.debug('Failed to login to Neato API')
|
||||||
|
return False
|
||||||
|
hub.update_robots()
|
||||||
|
for component in ('sensor', 'switch'):
|
||||||
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class NeatoHub(object):
|
||||||
|
"""A My Neato hub wrapper class."""
|
||||||
|
|
||||||
|
def __init__(self, hass, domain_config, neato):
|
||||||
|
"""Initialize the Neato hub."""
|
||||||
|
self.config = domain_config
|
||||||
|
self._neato = neato
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
|
self.my_neato = neato(
|
||||||
|
domain_config[CONF_USERNAME],
|
||||||
|
domain_config[CONF_PASSWORD])
|
||||||
|
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
"""Login to My Neato."""
|
||||||
|
try:
|
||||||
|
_LOGGER.debug('Trying to connect to Neato API')
|
||||||
|
self.my_neato = self._neato(self.config[CONF_USERNAME],
|
||||||
|
self.config[CONF_PASSWORD])
|
||||||
|
return True
|
||||||
|
except HTTPError:
|
||||||
|
_LOGGER.error("Unable to connect to Neato API")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@Throttle(timedelta(seconds=1))
|
||||||
|
def update_robots(self):
|
||||||
|
"""Update the robot states."""
|
||||||
|
_LOGGER.debug('Running HUB.update_robots %s',
|
||||||
|
self._hass.data[NEATO_ROBOTS])
|
||||||
|
self._hass.data[NEATO_ROBOTS] = self.my_neato.robots
|
@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
|||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/jabesq/netatmo-api-python/archive/'
|
'https://github.com/jabesq/netatmo-api-python/archive/'
|
||||||
'v0.6.0.zip#lnetatmo==0.6.0']
|
'v0.7.0.zip#lnetatmo==0.7.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -25,9 +25,7 @@ from homeassistant.components.http import HomeAssistantView
|
|||||||
from homeassistant.components.frontend import add_manifest_json_key
|
from homeassistant.components.frontend import add_manifest_json_key
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/web-push-libs/pywebpush/archive/'
|
REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2']
|
||||||
'e743dc92558fc62178d255c0018920d74fa778ed.zip#'
|
|
||||||
'pywebpush==0.5.0', 'PyJWT==1.4.2']
|
|
||||||
|
|
||||||
DEPENDENCIES = ['frontend']
|
DEPENDENCIES = ['frontend']
|
||||||
|
|
||||||
@ -141,11 +139,23 @@ def _load_config(filename):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class JSONBytesDecoder(json.JSONEncoder):
|
||||||
|
"""JSONEncoder to decode bytes objects to unicode."""
|
||||||
|
|
||||||
|
# pylint: disable=method-hidden
|
||||||
|
def default(self, obj):
|
||||||
|
"""Decode object if it's a bytes object, else defer to baseclass."""
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return obj.decode()
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
def _save_config(filename, config):
|
def _save_config(filename, config):
|
||||||
"""Save configuration."""
|
"""Save configuration."""
|
||||||
try:
|
try:
|
||||||
with open(filename, 'w') as fdesc:
|
with open(filename, 'w') as fdesc:
|
||||||
fdesc.write(json.dumps(config))
|
fdesc.write(json.dumps(
|
||||||
|
config, cls=JSONBytesDecoder, indent=4, sort_keys=True))
|
||||||
except (IOError, TypeError) as error:
|
except (IOError, TypeError) as error:
|
||||||
_LOGGER.error('Saving config file failed: %s', error)
|
_LOGGER.error('Saving config file failed: %s', error)
|
||||||
return False
|
return False
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
iOS push notification platform for notify component.
|
iOS push notification platform for notify component.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/notify.ios/
|
https://home-assistant.io/ecosystem/ios/notifications/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@ -48,8 +48,8 @@ def get_service(hass, config):
|
|||||||
if not ios.devices_with_push():
|
if not ios.devices_with_push():
|
||||||
_LOGGER.error(("The notify.ios platform was loaded but no "
|
_LOGGER.error(("The notify.ios platform was loaded but no "
|
||||||
"devices exist! Please check the documentation at "
|
"devices exist! Please check the documentation at "
|
||||||
"https://home-assistant.io/components/notify.ios/ "
|
"https://home-assistant.io/ecosystem/ios/notifications"
|
||||||
"for more information"))
|
"/ for more information"))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return iOSNotificationService()
|
return iOSNotificationService()
|
||||||
|
@ -26,8 +26,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def get_service(hass, config):
|
def get_service(hass, config):
|
||||||
"""Get the NMA notification service."""
|
"""Get the NMA notification service."""
|
||||||
response = requests.get(_RESOURCE + 'verify',
|
parameters = {
|
||||||
params={"apikey": config[CONF_API_KEY]})
|
'apikey': config[CONF_API_KEY],
|
||||||
|
}
|
||||||
|
response = requests.get(
|
||||||
|
'{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5)
|
||||||
tree = ET.fromstring(response.content)
|
tree = ET.fromstring(response.content)
|
||||||
|
|
||||||
if tree[0].tag == 'error':
|
if tree[0].tag == 'error':
|
||||||
@ -47,14 +50,15 @@ class NmaNotificationService(BaseNotificationService):
|
|||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
"""Send a message to a user."""
|
"""Send a message to a user."""
|
||||||
data = {
|
data = {
|
||||||
"apikey": self._api_key,
|
'apikey': self._api_key,
|
||||||
"application": 'home-assistant',
|
'application': 'home-assistant',
|
||||||
"event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
|
'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
|
||||||
"description": message,
|
'description': message,
|
||||||
"priority": 0,
|
'priority': 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = requests.get(_RESOURCE + 'notify', params=data)
|
response = requests.get(
|
||||||
|
'{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5)
|
||||||
tree = ET.fromstring(response.content)
|
tree = ET.fromstring(response.content)
|
||||||
|
|
||||||
if tree[0].tag == 'error':
|
if tree[0].tag == 'error':
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
|||||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['sendgrid==3.6.0']
|
REQUIREMENTS = ['sendgrid==3.6.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -55,8 +55,7 @@ def async_create(hass, message, title=None, notification_id=None):
|
|||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.loop.create_task(
|
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
|
||||||
hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
|
|||||||
|
|
||||||
DOMAIN = 'recorder'
|
DOMAIN = 'recorder'
|
||||||
|
|
||||||
REQUIREMENTS = ['sqlalchemy==1.1.2']
|
REQUIREMENTS = ['sqlalchemy==1.1.3']
|
||||||
|
|
||||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||||
|
@ -94,6 +94,7 @@ def valid_sensor(value):
|
|||||||
def _valid_light_switch(value):
|
def _valid_light_switch(value):
|
||||||
return _valid_device(value, "light_switch")
|
return _valid_device(value, "light_switch")
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_NAME): cv.string,
|
vol.Required(ATTR_NAME): cv.string,
|
||||||
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
|
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
|
||||||
|
@ -7,6 +7,7 @@ by the user or automatically based upon automation events, etc.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/script/
|
https://home-assistant.io/components/script/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -40,7 +41,7 @@ _SCRIPT_ENTRY_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA}
|
vol.Required(DOMAIN): vol.Schema({cv.slug: _SCRIPT_ENTRY_SCHEMA})
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
|
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
|
||||||
@ -72,11 +73,13 @@ def toggle(hass, entity_id):
|
|||||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id})
|
hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id})
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
"""Load the scripts from the configuration."""
|
"""Load the scripts from the configuration."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||||
group_name=GROUP_NAME_ALL_SCRIPTS)
|
group_name=GROUP_NAME_ALL_SCRIPTS)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def service_handler(service):
|
def service_handler(service):
|
||||||
"""Execute a service call to script.<script name>."""
|
"""Execute a service call to script.<script name>."""
|
||||||
entity_id = ENTITY_ID_FORMAT.format(service.service)
|
entity_id = ENTITY_ID_FORMAT.format(service.service)
|
||||||
@ -84,37 +87,47 @@ def setup(hass, config):
|
|||||||
if script.is_on:
|
if script.is_on:
|
||||||
_LOGGER.warning("Script %s already running.", entity_id)
|
_LOGGER.warning("Script %s already running.", entity_id)
|
||||||
return
|
return
|
||||||
script.turn_on(variables=service.data)
|
yield from script.async_turn_on(variables=service.data)
|
||||||
|
|
||||||
|
scripts = []
|
||||||
|
|
||||||
for object_id, cfg in config[DOMAIN].items():
|
for object_id, cfg in config[DOMAIN].items():
|
||||||
alias = cfg.get(CONF_ALIAS, object_id)
|
alias = cfg.get(CONF_ALIAS, object_id)
|
||||||
script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
|
script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
|
||||||
component.add_entities((script,))
|
scripts.append(script)
|
||||||
hass.services.register(DOMAIN, object_id, service_handler,
|
hass.services.async_register(DOMAIN, object_id, service_handler,
|
||||||
schema=SCRIPT_SERVICE_SCHEMA)
|
schema=SCRIPT_SERVICE_SCHEMA)
|
||||||
|
|
||||||
|
yield from component.async_add_entities(scripts)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def turn_on_service(service):
|
def turn_on_service(service):
|
||||||
"""Call a service to turn script on."""
|
"""Call a service to turn script on."""
|
||||||
# We could turn on script directly here, but we only want to offer
|
# We could turn on script directly here, but we only want to offer
|
||||||
# one way to do it. Otherwise no easy way to detect invocations.
|
# one way to do it. Otherwise no easy way to detect invocations.
|
||||||
for script in component.extract_from_service(service):
|
var = service.data.get(ATTR_VARIABLES)
|
||||||
turn_on(hass, script.entity_id, service.data.get(ATTR_VARIABLES))
|
for script in component.async_extract_from_service(service):
|
||||||
|
yield from hass.services.async_call(DOMAIN, script.object_id, var)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def turn_off_service(service):
|
def turn_off_service(service):
|
||||||
"""Cancel a script."""
|
"""Cancel a script."""
|
||||||
for script in component.extract_from_service(service):
|
# Stopping a script is ok to be done in parallel
|
||||||
script.turn_off()
|
yield from asyncio.wait(
|
||||||
|
[script.async_turn_off() for script
|
||||||
|
in component.async_extract_from_service(service)], loop=hass.loop)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def toggle_service(service):
|
def toggle_service(service):
|
||||||
"""Toggle a script."""
|
"""Toggle a script."""
|
||||||
for script in component.extract_from_service(service):
|
for script in component.async_extract_from_service(service):
|
||||||
script.toggle()
|
yield from script.async_toggle()
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
|
hass.services.async_register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
|
||||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
|
hass.services.async_register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
|
||||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service,
|
hass.services.async_register(DOMAIN, SERVICE_TOGGLE, toggle_service,
|
||||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -124,6 +137,7 @@ class ScriptEntity(ToggleEntity):
|
|||||||
|
|
||||||
def __init__(self, hass, object_id, name, sequence):
|
def __init__(self, hass, object_id, name, sequence):
|
||||||
"""Initialize the script."""
|
"""Initialize the script."""
|
||||||
|
self.object_id = object_id
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self.script = Script(hass, sequence, name, self.async_update_ha_state)
|
self.script = Script(hass, sequence, name, self.async_update_ha_state)
|
||||||
|
|
||||||
@ -152,10 +166,12 @@ class ScriptEntity(ToggleEntity):
|
|||||||
"""Return true if script is on."""
|
"""Return true if script is on."""
|
||||||
return self.script.is_running
|
return self.script.is_running
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
"""Turn the script on."""
|
"""Turn the script on."""
|
||||||
self.script.run(kwargs.get(ATTR_VARIABLES))
|
yield from self.script.async_run(kwargs.get(ATTR_VARIABLES))
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
"""Turn script off."""
|
"""Turn script off."""
|
||||||
self.script.stop()
|
self.script.async_stop()
|
||||||
|
61
homeassistant/components/sensor/api_streams.py
Normal file
61
homeassistant/components/sensor/api_streams.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""Entity to track connections to stream API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
|
||||||
|
class StreamHandler(logging.Handler):
|
||||||
|
"""Check log messages for stream connect/disconnect."""
|
||||||
|
|
||||||
|
def __init__(self, entity):
|
||||||
|
"""Initialize handler."""
|
||||||
|
super().__init__()
|
||||||
|
self.entity = entity
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
def handle(self, record):
|
||||||
|
"""Handle a log message."""
|
||||||
|
if not record.msg.startswith('STREAM'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if record.msg.endswith('ATTACHED'):
|
||||||
|
self.entity.count += 1
|
||||||
|
elif record.msg.endswith('RESPONSE CLOSED'):
|
||||||
|
self.entity.count -= 1
|
||||||
|
|
||||||
|
self.entity.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
"""Set up the logger for filters."""
|
||||||
|
entity = APICount()
|
||||||
|
|
||||||
|
logging.getLogger('homeassistant.components.api').addHandler(
|
||||||
|
StreamHandler(entity))
|
||||||
|
|
||||||
|
yield from async_add_devices([entity])
|
||||||
|
|
||||||
|
|
||||||
|
class APICount(Entity):
|
||||||
|
"""Entity to represent how many people are connected to stream API."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the API count."""
|
||||||
|
self.count = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return name of entity."""
|
||||||
|
return "Connected clients"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return current API count."""
|
||||||
|
return self.count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Unit of measurement."""
|
||||||
|
return "clients"
|
@ -113,6 +113,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
longitude=hass.config.longitude,
|
longitude=hass.config.longitude,
|
||||||
units=units,
|
units=units,
|
||||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||||
|
forecast_data.update()
|
||||||
forecast_data.update_currently()
|
forecast_data.update_currently()
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
_LOGGER.error(error)
|
_LOGGER.error(error)
|
||||||
@ -124,7 +125,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||||
sensors.append(DarkSkySensor(forecast_data, variable, name))
|
sensors.append(DarkSkySensor(forecast_data, variable, name))
|
||||||
|
|
||||||
add_devices(sensors)
|
add_devices(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
class DarkSkySensor(Entity):
|
class DarkSkySensor(Entity):
|
||||||
@ -139,8 +140,6 @@ class DarkSkySensor(Entity):
|
|||||||
self._state = None
|
self._state = None
|
||||||
self._unit_of_measurement = None
|
self._unit_of_measurement = None
|
||||||
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
@ -277,8 +276,6 @@ class DarkSkyData(object):
|
|||||||
self.update_hourly = Throttle(interval)(self._update_hourly)
|
self.update_hourly = Throttle(interval)(self._update_hourly)
|
||||||
self.update_daily = Throttle(interval)(self._update_daily)
|
self.update_daily = Throttle(interval)(self._update_daily)
|
||||||
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
"""Get the latest data from Dark Sky."""
|
"""Get the latest data from Dark Sky."""
|
||||||
import forecastio
|
import forecastio
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.util import Throttle
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['schiene==0.17']
|
REQUIREMENTS = ['schiene==0.18']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Support for Home Assistant iOS app sensors.
|
Support for Home Assistant iOS app sensors.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.ios/
|
https://home-assistant.io/ecosystem/ios/
|
||||||
"""
|
"""
|
||||||
from homeassistant.components import ios
|
from homeassistant.components import ios
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['batinfo==0.3']
|
REQUIREMENTS = ['batinfo==0.4.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.util import Throttle
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
|
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
|
||||||
|
|
||||||
REQUIREMENTS = ['miflora==0.1.9']
|
REQUIREMENTS = ['miflora==0.1.12']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ SENSOR_TYPES = {
|
|||||||
'light': ['Light intensity', 'lux'],
|
'light': ['Light intensity', 'lux'],
|
||||||
'moisture': ['Moisture', '%'],
|
'moisture': ['Moisture', '%'],
|
||||||
'conductivity': ['Conductivity', 'µS/cm'],
|
'conductivity': ['Conductivity', 'µS/cm'],
|
||||||
|
'battery': ['Battery', '%'],
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
@ -58,8 +58,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
sensor_type = config.get(CONF_TYPE)
|
sensor_type = config.get(CONF_TYPE)
|
||||||
|
|
||||||
hass.loop.create_task(async_add_devices(
|
yield from async_add_devices(
|
||||||
[MinMaxSensor(hass, entity_ids, name, sensor_type)], True))
|
[MinMaxSensor(hass, entity_ids, name, sensor_type)], True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class MinMaxSensor(Entity):
|
|||||||
_LOGGER.warning("Unable to store state. "
|
_LOGGER.warning("Unable to store state. "
|
||||||
"Only numerical states are supported")
|
"Only numerical states are supported")
|
||||||
|
|
||||||
hass.loop.create_task(self.async_update_ha_state(True))
|
hass.async_add_job(self.async_update_ha_state, True)
|
||||||
|
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
hass, entity_ids, async_min_max_sensor_state_listener)
|
hass, entity_ids, async_min_max_sensor_state_listener)
|
||||||
|
@ -8,6 +8,7 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
|
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT)
|
CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT)
|
||||||
@ -55,13 +56,14 @@ class MqttSensor(Entity):
|
|||||||
self._qos = qos
|
self._qos = qos
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
|
||||||
|
@callback
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
"""A new MQTT message has been received."""
|
"""A new MQTT message has been received."""
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
payload = value_template.render_with_possible_json_value(
|
payload = value_template.async_render_with_possible_json_value(
|
||||||
payload, self._state)
|
payload, self._state)
|
||||||
self._state = payload
|
self._state = payload
|
||||||
self.update_ha_state()
|
hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||||
|
|
||||||
|
@ -20,7 +20,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for gateway in mysensors.GATEWAYS.values():
|
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||||
|
if not gateways:
|
||||||
|
return
|
||||||
|
|
||||||
|
for gateway in gateways:
|
||||||
# Define the S_TYPES and V_TYPES that the platform should handle as
|
# Define the S_TYPES and V_TYPES that the platform should handle as
|
||||||
# states. Map them in a dict of lists.
|
# states. Map them in a dict of lists.
|
||||||
pres = gateway.const.Presentation
|
pres = gateway.const.Presentation
|
||||||
|
150
homeassistant/components/sensor/neato.py
Normal file
150
homeassistant/components/sensor/neato.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""
|
||||||
|
Support for Neato Connected Vaccums sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.neato/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
SENSOR_TYPE_STATUS = 'status'
|
||||||
|
SENSOR_TYPE_BATTERY = 'battery'
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
SENSOR_TYPE_STATUS: ['Status'],
|
||||||
|
SENSOR_TYPE_BATTERY: ['Battery']
|
||||||
|
}
|
||||||
|
|
||||||
|
STATES = {
|
||||||
|
1: 'Idle',
|
||||||
|
2: 'Busy',
|
||||||
|
3: 'Pause',
|
||||||
|
4: 'Error'
|
||||||
|
}
|
||||||
|
|
||||||
|
MODE = {
|
||||||
|
1: 'Eco',
|
||||||
|
2: 'Turbo'
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION = {
|
||||||
|
0: 'No action',
|
||||||
|
1: 'House cleaning',
|
||||||
|
2: 'Spot cleaning',
|
||||||
|
3: 'Manual cleaning',
|
||||||
|
4: 'Docking',
|
||||||
|
5: 'User menu active',
|
||||||
|
6: 'Cleaning cancelled',
|
||||||
|
7: 'Updating...',
|
||||||
|
8: 'Copying logs...',
|
||||||
|
9: 'Calculating position...',
|
||||||
|
10: 'IEC test'
|
||||||
|
}
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
'ui_error_brush_stuck': 'Brush stuck',
|
||||||
|
'ui_error_brush_overloaded': 'Brush overloaded',
|
||||||
|
'ui_error_bumper_stuck': 'Bumper stuck',
|
||||||
|
'ui_error_dust_bin_missing': 'Dust bin missing',
|
||||||
|
'ui_error_dust_bin_full': 'Dust bin full',
|
||||||
|
'ui_error_dust_bin_emptied': 'Dust bin emptied',
|
||||||
|
'ui_error_navigation_noprogress': 'Clear my path',
|
||||||
|
'ui_error_navigation_origin_unclean': 'Clear my path',
|
||||||
|
'ui_error_navigation_falling': 'Clear my path',
|
||||||
|
'ui_error_picked_up': 'Picked up',
|
||||||
|
'ui_error_stuck': 'Stuck!'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ALERTS = {
|
||||||
|
'ui_alert_dust_bin_full': 'Please empty dust bin',
|
||||||
|
'ui_alert_recovering_location': 'Returning to start'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Neato sensor platform."""
|
||||||
|
if not hass.data['neato_robots']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dev = []
|
||||||
|
for robot in hass.data[NEATO_ROBOTS]:
|
||||||
|
for type_name in SENSOR_TYPES:
|
||||||
|
dev.append(NeatoConnectedSensor(hass, robot, type_name))
|
||||||
|
_LOGGER.debug('Adding sensors %s', dev)
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
|
class NeatoConnectedSensor(Entity):
|
||||||
|
"""Neato Connected Sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hass, robot, sensor_type):
|
||||||
|
"""Initialize the Neato Connected sensor."""
|
||||||
|
self.type = sensor_type
|
||||||
|
self.robot = robot
|
||||||
|
self.neato = hass.data[NEATO_LOGIN]
|
||||||
|
self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0]
|
||||||
|
self._state = self.robot.state
|
||||||
|
self._battery_state = None
|
||||||
|
self._status_state = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update the properties of sensor."""
|
||||||
|
_LOGGER.debug('Update of sensor')
|
||||||
|
self.neato.update_robots()
|
||||||
|
if not self._state:
|
||||||
|
return
|
||||||
|
self._state = self.robot.state
|
||||||
|
_LOGGER.debug('self._state=%s', self._state)
|
||||||
|
if self.type == SENSOR_TYPE_STATUS:
|
||||||
|
if self._state['state'] == 1:
|
||||||
|
if self._state['details']['isCharging']:
|
||||||
|
self._status_state = 'Charging'
|
||||||
|
elif (self._state['details']['isDocked'] and
|
||||||
|
not self._state['details']['isCharging']):
|
||||||
|
self._status_state = 'Docked'
|
||||||
|
else:
|
||||||
|
self._status_state = 'Stopped'
|
||||||
|
elif self._state['state'] == 2:
|
||||||
|
if ALERTS.get(self._state['error']) is None:
|
||||||
|
self._status_state = (
|
||||||
|
MODE.get(self._state['cleaning']['mode'])
|
||||||
|
+ ' ' + ACTION.get(self._state['action']))
|
||||||
|
else:
|
||||||
|
self._status_state = ALERTS.get(self._state['error'])
|
||||||
|
elif self._state['state'] == 3:
|
||||||
|
self._status_state = 'Paused'
|
||||||
|
elif self._state['state'] == 4:
|
||||||
|
self._status_state = ERRORS.get(self._state['error'])
|
||||||
|
if self.type == SENSOR_TYPE_BATTERY:
|
||||||
|
self._battery_state = self._state['details']['charge']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return unit for the sensor."""
|
||||||
|
if self.type == SENSOR_TYPE_BATTERY:
|
||||||
|
return '%'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if sensor data is available."""
|
||||||
|
if not self._state:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the sensor state."""
|
||||||
|
if self.type == SENSOR_TYPE_STATUS:
|
||||||
|
return self._status_state
|
||||||
|
if self.type == SENSOR_TYPE_BATTERY:
|
||||||
|
return self._battery_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._robot_name
|
2
homeassistant/components/sensor/openweathermap.py
Normal file → Executable file
2
homeassistant/components/sensor/openweathermap.py
Normal file → Executable file
@ -21,7 +21,7 @@ REQUIREMENTS = ['pyowm==2.5.0']
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provied by OpenWeatherMap"
|
CONF_ATTRIBUTION = "Data provided by OpenWeatherMap"
|
||||||
CONF_FORECAST = 'forecast'
|
CONF_FORECAST = 'forecast'
|
||||||
|
|
||||||
DEFAULT_NAME = 'OWM'
|
DEFAULT_NAME = 'OWM'
|
||||||
|
117
homeassistant/components/sensor/pvoutput.py
Normal file
117
homeassistant/components/sensor/pvoutput.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Support for getting collected information from PVOutput.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.pvoutput/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.components.sensor.rest import RestData
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_TEMPERATURE, CONF_API_KEY, CONF_NAME, STATE_UNKNOWN)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_ENDPOINT = 'http://pvoutput.org/service/r2/getstatus.jsp'
|
||||||
|
|
||||||
|
ATTR_DATE = 'date'
|
||||||
|
ATTR_TIME = 'time'
|
||||||
|
ATTR_ENERGY_GENERATION = 'energy_generation'
|
||||||
|
ATTR_POWER_GENERATION = 'power_generation'
|
||||||
|
ATTR_ENERGY_CONSUMPTION = 'energy_consumption'
|
||||||
|
ATTR_POWER_CONSUMPTION = 'power_consumption'
|
||||||
|
ATTR_EFFICIENCY = 'efficiency'
|
||||||
|
ATTR_VOLTAGE = 'voltage'
|
||||||
|
|
||||||
|
CONF_SYSTEM_ID = 'system_id'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'PVOutput'
|
||||||
|
DEFAULT_VERIFY_SSL = True
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
|
vol.Required(CONF_SYSTEM_ID): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the PVOutput sensor."""
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
api_key = config.get(CONF_API_KEY)
|
||||||
|
system_id = config.get(CONF_SYSTEM_ID)
|
||||||
|
method = 'GET'
|
||||||
|
payload = auth = None
|
||||||
|
verify_ssl = DEFAULT_VERIFY_SSL
|
||||||
|
headers = {
|
||||||
|
'X-Pvoutput-Apikey': api_key,
|
||||||
|
'X-Pvoutput-SystemId': system_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
rest = RestData(method, _ENDPOINT, auth, headers, payload, verify_ssl)
|
||||||
|
rest.update()
|
||||||
|
|
||||||
|
if rest.data is None:
|
||||||
|
_LOGGER.error("Unable to fetch data from PVOutput")
|
||||||
|
return False
|
||||||
|
|
||||||
|
add_devices([PvoutputSensor(rest, name)])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
class PvoutputSensor(Entity):
|
||||||
|
"""Representation of a PVOutput sensor."""
|
||||||
|
|
||||||
|
def __init__(self, rest, name):
|
||||||
|
"""Initialize a PVOutput sensor."""
|
||||||
|
self.rest = rest
|
||||||
|
self._name = name
|
||||||
|
self.pvcoutput = False
|
||||||
|
self.status = namedtuple(
|
||||||
|
'status', [ATTR_DATE, ATTR_TIME, ATTR_ENERGY_GENERATION,
|
||||||
|
ATTR_POWER_GENERATION, ATTR_ENERGY_CONSUMPTION,
|
||||||
|
ATTR_POWER_CONSUMPTION, ATTR_EFFICIENCY,
|
||||||
|
ATTR_TEMPERATURE, ATTR_VOLTAGE])
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self.pvcoutput is not None:
|
||||||
|
return self.pvcoutput.energy_generation
|
||||||
|
else:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes of the Pi-Hole."""
|
||||||
|
if self.pvcoutput is not None:
|
||||||
|
return {
|
||||||
|
ATTR_POWER_GENERATION: self.pvcoutput.power_generation,
|
||||||
|
ATTR_ENERGY_CONSUMPTION: self.pvcoutput.energy_consumption,
|
||||||
|
ATTR_POWER_CONSUMPTION: self.pvcoutput.power_consumption,
|
||||||
|
ATTR_EFFICIENCY: self.pvcoutput.efficiency,
|
||||||
|
ATTR_TEMPERATURE: self.pvcoutput.temperature,
|
||||||
|
ATTR_VOLTAGE: self.pvcoutput.voltage,
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from the PVOutput API and updates the state."""
|
||||||
|
try:
|
||||||
|
self.rest.update()
|
||||||
|
self.pvcoutput = self.status._make(self.rest.data.split(','))
|
||||||
|
except TypeError:
|
||||||
|
self.pvcoutput = None
|
||||||
|
_LOGGER.error(
|
||||||
|
"Unable to fetch data from PVOutput. %s", self.rest.data)
|
@ -36,8 +36,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
minimum = config.get(CONF_MINIMUM)
|
minimum = config.get(CONF_MINIMUM)
|
||||||
maximum = config.get(CONF_MAXIMUM)
|
maximum = config.get(CONF_MAXIMUM)
|
||||||
|
|
||||||
hass.loop.create_task(async_add_devices(
|
yield from async_add_devices([RandomSensor(name, minimum, maximum)], True)
|
||||||
[RandomSensor(name, minimum, maximum)], True))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
sampling_size = config.get(CONF_SAMPLING_SIZE)
|
sampling_size = config.get(CONF_SAMPLING_SIZE)
|
||||||
|
|
||||||
hass.loop.create_task(async_add_devices(
|
yield from async_add_devices(
|
||||||
[StatisticsSensor(hass, entity_id, name, sampling_size)], True))
|
[StatisticsSensor(hass, entity_id, name, sampling_size)], True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ class StatisticsSensor(Entity):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
self.count = self.count + 1
|
self.count = self.count + 1
|
||||||
|
|
||||||
hass.loop.create_task(self.async_update_ha_state(True))
|
hass.async_add_job(self.async_update_ha_state, True)
|
||||||
|
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
hass, entity_id, async_stats_sensor_state_listener)
|
hass, entity_id, async_stats_sensor_state_listener)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user