mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
commit
8b6a94b0f5
12
.coveragerc
12
.coveragerc
@ -28,6 +28,9 @@ omit =
|
||||
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
|
||||
|
||||
@ -98,6 +101,9 @@ omit =
|
||||
homeassistant/components/netatmo.py
|
||||
homeassistant/components/*/netatmo.py
|
||||
|
||||
homeassistant/components/neato.py
|
||||
homeassistant/components/*/neato.py
|
||||
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
@ -132,7 +138,7 @@ omit =
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
@ -144,12 +150,14 @@ omit =
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.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/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
@ -278,6 +286,7 @@ omit =
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
@ -306,7 +315,6 @@ omit =
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/neato.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.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 os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from collections import OrderedDict
|
||||
|
||||
from types import ModuleType
|
||||
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)
|
||||
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
config = {}
|
||||
|
||||
components = loader.load_order_component(domain)
|
||||
|
||||
@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant,
|
||||
async_comp = hasattr(component, 'async_setup')
|
||||
|
||||
try:
|
||||
_LOGGER.info("Setting up %s", domain)
|
||||
if async_comp:
|
||||
result = yield from component.async_setup(hass, config)
|
||||
else:
|
||||
@ -165,15 +166,6 @@ def _async_setup_component(hass: core.HomeAssistant,
|
||||
|
||||
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(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
)
|
||||
@ -353,7 +345,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
|
||||
# run task
|
||||
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)
|
||||
|
||||
return future.result()
|
||||
@ -373,6 +365,12 @@ def async_from_config_dict(config: Dict[str, Any],
|
||||
Dynamically loads required components and its dependencies.
|
||||
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, {})
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
config = defaultdict(
|
||||
dict, {key: value or {} for key, value in config.items()})
|
||||
new_config = OrderedDict()
|
||||
for key, value in config.items():
|
||||
new_config[key] = value or {}
|
||||
config = new_config
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
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):
|
||||
yield from _async_setup_component(hass, domain, config)
|
||||
|
||||
setup_lock.release()
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
|
@ -119,7 +119,7 @@ def async_setup(hass, config):
|
||||
tasks.append(hass.services.async_call(
|
||||
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(
|
||||
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
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
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 (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
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__)
|
||||
|
||||
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
|
||||
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,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
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
|
||||
|
||||
|
||||
@ -66,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
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']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""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:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_time)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
if self._trigger_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + self._pending_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)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
|
@ -41,3 +41,14 @@ alarm_trigger:
|
||||
code:
|
||||
description: An optional code to trigger the alarm control panel with
|
||||
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)
|
||||
|
||||
|
||||
_TRIGGER_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
@ -165,7 +166,7 @@ def async_setup(hass, config):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
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
|
||||
def turn_onoff_service_handler(service_call):
|
||||
@ -174,7 +175,7 @@ def async_setup(hass, config):
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def toggle_service_handler(service_call):
|
||||
@ -185,7 +186,7 @@ def async_setup(hass, config):
|
||||
tasks.append(entity.async_turn_off())
|
||||
else:
|
||||
tasks.append(entity.async_turn_on())
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
@ -348,8 +349,10 @@ def _async_process_config(hass, config, component):
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from component.async_add_entities(entities)
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
|
@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice):
|
||||
def _callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _start_ffmpeg(self, config):
|
||||
"""Start a FFmpeg instance."""
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice):
|
||||
self._payload_off = payload_off
|
||||
self._qos = qos
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
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)
|
||||
|
||||
|
@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
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
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
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
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
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."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
timeout = config.get(CONF_TIMEOUT, 15)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name)
|
||||
self._camera_name,
|
||||
self._timeout*60)
|
||||
else:
|
||||
return None
|
||||
|
@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread):
|
||||
if not zone_sensor:
|
||||
return
|
||||
zone_sensor._zone['state'] = event['zone_state']
|
||||
zone_sensor.update_ha_state()
|
||||
zone_sensor.schedule_update_ha_state()
|
||||
|
||||
def _process_events(self, events):
|
||||
for event in events:
|
||||
|
@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice):
|
||||
def read_gpio(port):
|
||||
"""Read state from GPIO."""
|
||||
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)
|
||||
|
||||
|
@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
|
||||
hass.loop.create_task(async_add_devices(sensors, True))
|
||||
yield from async_add_devices(sensors, True)
|
||||
return True
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_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(
|
||||
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
|
||||
https://home-assistant.io/components/sensor.trend/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice):
|
||||
self.from_state = None
|
||||
self.to_state = None
|
||||
|
||||
self.update()
|
||||
|
||||
@callback
|
||||
def trend_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.from_state = old_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,
|
||||
trend_sensor_state_listener)
|
||||
@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
if self.from_state is None or self.to_state is None:
|
||||
return
|
||||
|
@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice):
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update()
|
||||
if not hasattr(self, 'hass'):
|
||||
self.update()
|
||||
return
|
||||
self.update_ha_state(True)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
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/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
self.capability = self.wink.capability()
|
||||
|
||||
def _pubnub_update(self, message, channel):
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
self.update_ha_state()
|
||||
try:
|
||||
if 'data' in message:
|
||||
json_data = json.dumps(message.get('data'))
|
||||
else:
|
||||
json_data = message
|
||||
self.wink.pubnub_update(json.loads(json_data))
|
||||
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
|
||||
def is_on(self):
|
||||
|
@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
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 sensor_value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.update_ha_state,
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@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."""
|
||||
if not async_run_test(hass, config.get(CONF_INPUT)):
|
||||
return
|
||||
hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
|
||||
yield from async_add_devices([FFmpegCamera(hass, config)])
|
||||
|
||||
|
||||
class FFmpegCamera(Camera):
|
||||
@ -85,7 +85,7 @@ class FFmpegCamera(Camera):
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.close())
|
||||
self.hass.async_add_job(stream.close())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
|
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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):
|
||||
|
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""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):
|
||||
@ -122,7 +122,7 @@ class MjpegCamera(Camera):
|
||||
break
|
||||
response.write(data)
|
||||
finally:
|
||||
self.hass.loop.create_task(stream.release())
|
||||
self.hass.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
|
@ -14,6 +14,7 @@ from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
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."""
|
||||
if not config.get(CONF_VERIFY_SSL):
|
||||
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:
|
||||
connector = None
|
||||
connector = hass.websession.connector
|
||||
|
||||
websession_init = aiohttp.ClientSession(
|
||||
loop=hass.loop,
|
||||
@ -115,10 +124,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
websession = aiohttp.ClientSession(
|
||||
loop=hass.loop, connector=connector, cookies={'id': session_id})
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def _async_close_websession(event):
|
||||
"""Close webssesion on shutdown."""
|
||||
yield from websession.close()
|
||||
"""Close websession on shutdown."""
|
||||
websession.detach()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||
|
@ -145,7 +145,7 @@ class GenericThermostat(ClimateDevice):
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
@ -158,7 +158,7 @@ class GenericThermostat(ClimateDevice):
|
||||
|
||||
self._update_temp(new_state)
|
||||
self._control_heating()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_temp(self, state):
|
||||
"""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."""
|
||||
if discovery_info is None:
|
||||
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:
|
||||
continue
|
||||
pres = gateway.const.Presentation
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.4.0']
|
||||
REQUIREMENTS = ['proliphix==0.4.1']
|
||||
|
||||
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
|
||||
https://home-assistant.io/components/climate.zwave/
|
||||
@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, ATTR_OPERATION_MODE)
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.const import (
|
||||
@ -18,44 +17,23 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'ZWave Climate'
|
||||
DEFAULT_NAME = 'Z-Wave Climate'
|
||||
|
||||
REMOTEC = 0x5254
|
||||
REMOTEC_ZXT_120 = 0x8377
|
||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120)
|
||||
|
||||
HORSTMANN = 0x0059
|
||||
HORSTMANN_HRT4_ZW = 0x3
|
||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||
ATTR_OPERATING_STATE = 'operating_state'
|
||||
ATTR_FAN_STATE = 'fan_state'
|
||||
|
||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
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
|
||||
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
_LOGGER.debug("No discovery_info=%s or no NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
@ -70,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Represents a ZWave Climate device."""
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
"""Initialize the zwave climate device."""
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
@ -85,13 +64,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._operating_state = None
|
||||
self._current_fan_mode = None
|
||||
self._fan_list = None
|
||||
self._fan_state = None
|
||||
self._current_swing_mode = None
|
||||
self._swing_list = None
|
||||
self._unit = temp_unit
|
||||
self._index_operation = None
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self._hrt4_zw = None
|
||||
self.update_properties()
|
||||
# register listener
|
||||
dispatcher.connect(
|
||||
@ -106,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
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):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
_LOGGER.debug("Value changed on network %s", value)
|
||||
|
||||
def update_properties(self):
|
||||
@ -125,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||
self._current_operation)
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
# Current Temp
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = int(value.data)
|
||||
self._current_temperature = round((float(value.data)), 1)
|
||||
self._unit = value.units
|
||||
# Fan Mode
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
@ -149,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
@ -161,35 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = int(self._current_temperature)
|
||||
break
|
||||
if self.current_operation is not None and \
|
||||
self.current_operation != 'Off':
|
||||
if self._index_operation != value.index:
|
||||
continue
|
||||
if self._zxt_120:
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
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)
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
# Operating state
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE)
|
||||
.values()):
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const
|
||||
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||
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
|
||||
def should_poll(self):
|
||||
"""No polling on ZWave."""
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
|
||||
@property
|
||||
@ -248,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
else:
|
||||
return
|
||||
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if operation_mode is not None:
|
||||
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 value.index == self._index:
|
||||
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
|
||||
value.data = round(temperature, 0)
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
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
|
||||
self.update_ha_state()
|
||||
break
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
@ -331,9 +276,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
return {
|
||||
"operating_state": self._operating_state,
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.12.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.const import (
|
||||
@ -89,29 +90,30 @@ class MqttCover(CoverDevice):
|
||||
self._retain = retain
|
||||
self._optimistic = optimistic or state_topic is None
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.render_with_possible_json_value(
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
_LOGGER.warning("state=%s", int(self._state))
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
self.update_ha_state()
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the mysensors platform for covers."""
|
||||
if discovery_info is None:
|
||||
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
|
||||
set_req = gateway.const.SetReq
|
||||
map_sv_types = {
|
||||
|
@ -36,15 +36,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
and value.index == 0):
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveRollershutter(value)])
|
||||
elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class ==
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
if (value.type != zwave.const.TYPE_BOOL and
|
||||
value.genre != zwave.const.GENRE_USER):
|
||||
return
|
||||
value.set_change_verified(False)
|
||||
add_devices([ZwaveGarageDoor(value)])
|
||||
else:
|
||||
return
|
||||
|
||||
|
@ -17,6 +17,7 @@ DOMAIN = 'demo'
|
||||
COMPONENTS_WITH_DEMO_PLATFORM = [
|
||||
'alarm_control_panel',
|
||||
'binary_sensor',
|
||||
'calendar',
|
||||
'camera',
|
||||
'climate',
|
||||
'cover',
|
||||
|
@ -9,6 +9,7 @@ from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
@ -79,21 +80,22 @@ def setup(hass, config):
|
||||
return None
|
||||
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.
|
||||
|
||||
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):
|
||||
return
|
||||
light.turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
light.async_turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
@callback
|
||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
|
||||
@ -104,16 +106,21 @@ def setup(hass, config):
|
||||
if not start_point:
|
||||
return
|
||||
|
||||
def turn_on(light_id):
|
||||
def async_turn_on_factory(light_id):
|
||||
"""Lambda can keep track of function parameters.
|
||||
|
||||
No local parameters. If we put the lambda directly in the below
|
||||
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):
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
@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):
|
||||
"""Handle tracked device state changes."""
|
||||
# pylint: disable=unused-variable
|
||||
@ -136,7 +144,7 @@ def setup(hass, config):
|
||||
# Do we need lights?
|
||||
if light_needed:
|
||||
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
|
||||
# if someone would be home?
|
||||
@ -149,7 +157,7 @@ def setup(hass, config):
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
if now > start_point + index * LIGHT_TRANSITION_TIME:
|
||||
light.turn_on(hass, light_id)
|
||||
light.async_turn_on(hass, light_id)
|
||||
|
||||
else:
|
||||
# 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:
|
||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
# pylint: disable=unused-variable
|
||||
@ -166,6 +175,6 @@ def setup(hass, config):
|
||||
|
||||
logger.info(
|
||||
"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
|
||||
|
@ -8,13 +8,13 @@ import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
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.discovery import SERVICE_NETGEAR
|
||||
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
|
||||
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 (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
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)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
"""Setup device tracker."""
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, DOMAIN, config, hass)
|
||||
async_log_exception(ex, DOMAIN, config, hass)
|
||||
return False
|
||||
else:
|
||||
conf = conf[0] if len(conf) > 0 else {}
|
||||
@ -121,60 +122,77 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
timedelta(seconds=DEFAULT_CONSIDER_HOME))
|
||||
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)
|
||||
|
||||
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."""
|
||||
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:
|
||||
return
|
||||
|
||||
try:
|
||||
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:
|
||||
_LOGGER.error('Error setting up platform %s', p_type)
|
||||
return
|
||||
|
||||
setup_scanner_platform(hass, p_config, scanner, tracker.see)
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
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)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception('Error setting up platform %s', p_type)
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN):
|
||||
setup_platform(p_type, p_config)
|
||||
setup_tasks = [async_setup_platform(p_type, p_config) for 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."""
|
||||
setup_platform(DISCOVERY_PLATFORMS[service], {}, info)
|
||||
hass.async_add_job(
|
||||
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(),
|
||||
device_tracker_discovered)
|
||||
discovery.async_listen(
|
||||
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||
|
||||
def update_stale(now):
|
||||
"""Clean up stale devices."""
|
||||
tracker.update_stale(now)
|
||||
track_utc_time_change(hass, update_stale, second=range(0, 60, 5))
|
||||
# Clean up stale devices
|
||||
async_track_utc_time_change(
|
||||
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||
|
||||
tracker.setup_group()
|
||||
|
||||
def see_service(call):
|
||||
@asyncio.coroutine
|
||||
def async_see_service(call):
|
||||
"""Service to see a device."""
|
||||
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_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)}
|
||||
tracker.see(**args)
|
||||
yield from tracker.async_see(**args)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SEE, see_service,
|
||||
descriptions.get(SERVICE_SEE))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE))
|
||||
|
||||
return True
|
||||
|
||||
@ -188,94 +206,116 @@ class DeviceTracker(object):
|
||||
self.hass = hass
|
||||
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.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:
|
||||
if self.devices[dev.dev_id] is not dev:
|
||||
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
|
||||
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
|
||||
_LOGGER.warning('Duplicate device MAC addresses detected %s',
|
||||
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,
|
||||
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."""
|
||||
with self.lock:
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
self.hass.add_job(
|
||||
self.async_see(mac, dev_id, host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
)
|
||||
|
||||
if device:
|
||||
device.seen(host_name, location_name, gps, gps_accuracy,
|
||||
battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
return
|
||||
@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.
|
||||
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
device.seen(host_name, location_name, gps, gps_accuracy, battery,
|
||||
attributes)
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if mac is None and dev_id is None:
|
||||
raise HomeAssistantError('Neither mac or device id passed in')
|
||||
elif mac is not None:
|
||||
mac = str(mac).upper()
|
||||
device = self.mac_to_dev.get(mac)
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
if device.track:
|
||||
device.update_ha_state()
|
||||
yield from device.async_update_ha_state()
|
||||
return
|
||||
|
||||
self.hass.bus.fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
# If no device can be found, create it
|
||||
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
|
||||
device = Device(
|
||||
self.hass, self.consider_home, self.track_new,
|
||||
dev_id, mac, (host_name or dev_id).replace('_', ' '))
|
||||
self.devices[dev_id] = device
|
||||
if mac is not None:
|
||||
self.mac_to_dev[mac] = device
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
self.group.update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
update_config(self.hass.config.path(YAML_DEVICES), dev_id, device)
|
||||
yield from device.async_seen(host_name, location_name, gps,
|
||||
gps_accuracy, battery, attributes)
|
||||
|
||||
def setup_group(self):
|
||||
"""Initialize group for all tracked devices."""
|
||||
run_coroutine_threadsafe(
|
||||
self.async_setup_group(), self.hass.loop).result()
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group is not None:
|
||||
yield from self.group.async_update_tracked_entity_ids(
|
||||
list(self.group.tracking) + [device.entity_id])
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||
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
|
||||
def async_setup_group(self):
|
||||
"""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()
|
||||
if dev.track)
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices."""
|
||||
with self.lock:
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home and
|
||||
device.stale(now)):
|
||||
device.update_ha_state(True)
|
||||
@callback
|
||||
def async_update_stale(self, now: dt_util.dt.datetime):
|
||||
"""Update stale devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
self.hass.async_add_job(device.async_update_ha_state(True))
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
@ -362,9 +402,10 @@ class Device(Entity):
|
||||
"""If device should be hidden."""
|
||||
return self.away_hide and self.state != STATE_HOME
|
||||
|
||||
def seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
@asyncio.coroutine
|
||||
def async_seen(self, host_name: str=None, location_name: str=None,
|
||||
gps: GPSType=None, gps_accuracy=0, battery: str=None,
|
||||
attributes: dict=None):
|
||||
"""Mark the device as seen."""
|
||||
self.last_seen = dt_util.utcnow()
|
||||
self.host_name = host_name
|
||||
@ -373,28 +414,38 @@ class Device(Entity):
|
||||
self.battery = battery
|
||||
self.attributes = attributes
|
||||
self.gps = None
|
||||
|
||||
if gps is not None:
|
||||
try:
|
||||
self.gps = float(gps[0]), float(gps[1])
|
||||
except (ValueError, TypeError, IndexError):
|
||||
_LOGGER.warning('Could not parse gps value for %s: %s',
|
||||
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):
|
||||
"""Return if device state is stale."""
|
||||
"""Return if device state is stale.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return self.last_seen and \
|
||||
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
|
||||
|
||||
def update(self):
|
||||
"""Update state of entity."""
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update state of entity.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self.last_seen:
|
||||
return
|
||||
elif self.location_name:
|
||||
self._state = self.location_name
|
||||
elif self.gps is not None:
|
||||
zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1],
|
||||
self.gps_accuracy)
|
||||
zone_state = zone.async_active_zone(
|
||||
self.hass, self.gps[0], self.gps[1], self.gps_accuracy)
|
||||
if zone_state is None:
|
||||
self._state = STATE_NOT_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):
|
||||
"""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({
|
||||
vol.Required('name'): cv.string,
|
||||
vol.Optional('track', default=False): cv.boolean,
|
||||
@ -426,7 +488,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
try:
|
||||
result = []
|
||||
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:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
@ -436,7 +499,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
device = dev_schema(device)
|
||||
device['dev_id'] = cv.slugify(dev_id)
|
||||
except vol.Invalid as exp:
|
||||
log_exception(exp, dev_id, devices, hass)
|
||||
async_log_exception(exp, dev_id, devices, hass)
|
||||
else:
|
||||
result.append(Device(hass, **device))
|
||||
return result
|
||||
@ -445,9 +508,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
return []
|
||||
|
||||
|
||||
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
scanner: Any, see_device: Callable):
|
||||
"""Helper method to connect scanner-based platform to device tracker."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
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)
|
||||
|
||||
# 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):
|
||||
"""Called when interval matches."""
|
||||
for mac in scanner.scan_devices():
|
||||
found_devices = scanner.scan_devices()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
host_name = None
|
||||
else:
|
||||
host_name = scanner.get_device_name(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,
|
||||
interval))
|
||||
async_track_utc_time_change(
|
||||
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):
|
||||
@ -484,7 +553,10 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
|
||||
|
||||
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
|
||||
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
|
||||
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())
|
||||
|
@ -42,6 +42,7 @@ def get_scanner(hass, config):
|
||||
scanner = ActiontecDeviceScanner(config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
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'(?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
|
||||
def get_scanner(hass, config):
|
||||
@ -84,7 +93,8 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(object):
|
||||
@ -155,7 +165,8 @@ class AsusWrtDeviceScanner(object):
|
||||
active_clients = [client for client in data.values() if
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE']
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
@ -184,13 +195,18 @@ class AsusWrtDeviceScanner(object):
|
||||
ssh.sendline(_WL_CMD)
|
||||
ssh.prompt()
|
||||
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:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
ssh.sendline(_LEASES_CMD)
|
||||
ssh.prompt()
|
||||
leases_result = ssh.before.split(b'\n')[1:-1]
|
||||
ssh.logout()
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except pxssh.ExceptionPxssh as exc:
|
||||
_LOGGER.error('Unexpected response from router: %s', exc)
|
||||
return None
|
||||
@ -213,13 +229,18 @@ class AsusWrtDeviceScanner(object):
|
||||
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
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:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (telnet.read_until(prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
telnet.write('exit\n'.encode('ascii'))
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result)
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
except EOFError:
|
||||
_LOGGER.error('Unexpected response from router')
|
||||
return None
|
||||
@ -277,6 +298,26 @@ class AsusWrtDeviceScanner(object):
|
||||
'ip': arp_match.group('ip'),
|
||||
'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:
|
||||
for lease in result.leases:
|
||||
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
|
||||
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
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.components.device_tracker import ( # NOQA
|
||||
@ -76,11 +78,13 @@ class LocativeView(HomeAssistantView):
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
if direction == 'enter':
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
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)
|
||||
|
||||
elif direction == 'exit':
|
||||
@ -88,9 +92,11 @@ class LocativeView(HomeAssistantView):
|
||||
'{}.{}'.format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
location_name = STATE_NOT_HOME
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
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'
|
||||
else:
|
||||
# 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.
|
||||
|
||||
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 re
|
||||
@ -43,6 +43,7 @@ def get_scanner(hass, config):
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
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,25 +55,30 @@ def setup_scanner(hass, config, see):
|
||||
"""True if any door/window is opened."""
|
||||
return any([door[key] for key in door if "Open" in key])
|
||||
|
||||
attributes = dict(
|
||||
unlocked=not vehicle["carLocked"],
|
||||
tank_volume=vehicle["fuelTankVolume"],
|
||||
average_fuel_consumption=round(
|
||||
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||
doors_open=any_opened(vehicle["doors"]),
|
||||
windows_open=any_opened(vehicle["windows"]),
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
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=dict(
|
||||
unlocked=not vehicle["carLocked"],
|
||||
tank_volume=vehicle["fuelTankVolume"],
|
||||
average_fuel_consumption=round(
|
||||
vehicle["averageFuelConsumption"] / 10, 1), # l/100km
|
||||
washer_fluid_low=vehicle["washerFluidLevel"] != "Normal",
|
||||
brake_fluid_low=vehicle["brakeFluid"] != "Normal",
|
||||
service_warning=vehicle["serviceWarningStatus"] != "Normal",
|
||||
bulb_failures=len(vehicle["bulbFailures"]) > 0,
|
||||
doors_open=any_opened(vehicle["doors"]),
|
||||
windows_open=any_opened(vehicle["windows"]),
|
||||
heater_on=vehicle["heater"]["status"] != "off",
|
||||
fuel=vehicle["fuelAmount"],
|
||||
odometer=round(vehicle["odometer"] / 1000), # km
|
||||
range=vehicle["distanceToEmpty"]))
|
||||
attributes=attributes)
|
||||
|
||||
def update(now):
|
||||
"""Update status from the online service."""
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.0']
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.7.5']
|
||||
REQUIREMENTS = ['netdisco==0.7.6']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
@ -18,7 +18,7 @@ from homeassistant import util, core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
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 (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
@ -318,7 +318,16 @@ class HueLightsView(HomeAssistantView):
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
if brightness is not None:
|
||||
# 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:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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__)
|
||||
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
|
||||
in component.async_extract_from_service(service,
|
||||
expand_group=False)]
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler,
|
||||
@ -207,13 +207,14 @@ def _async_process_config(hass, config, component):
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
|
||||
# This order is important as groups get a number based on creation
|
||||
# order.
|
||||
# Don't create tasks and await them all. The order is important as
|
||||
# groups get a number based on creation order.
|
||||
group = yield from Group.async_create_group(
|
||||
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
||||
groups.append(group)
|
||||
|
||||
yield from component.async_add_entities(groups)
|
||||
if groups:
|
||||
yield from component.async_add_entities(groups)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
@ -394,7 +395,7 @@ class Group(Entity):
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
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
|
||||
def _tracking_states(self):
|
||||
|
@ -222,6 +222,7 @@ class GzipFileSender(FileSender):
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
_GZIP_FILE_SENDER = GzipFileSender()
|
||||
|
||||
|
||||
@ -461,6 +462,9 @@ def request_handler_factory(view, handler):
|
||||
@asyncio.coroutine
|
||||
def handle(request):
|
||||
"""Handle incoming request."""
|
||||
if not view.hass.is_running:
|
||||
return web.Response(status=503)
|
||||
|
||||
remote_addr = view.hass.http.get_real_ip(request)
|
||||
|
||||
# Auth code verbose on purpose
|
||||
|
@ -23,17 +23,23 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_INITIAL = 'initial'
|
||||
DEFAULT_INITIAL = False
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
||||
cv.slug: vol.Any({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL, default=False): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}, None)}}, extra=vol.ALLOW_EXTRA)
|
||||
DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.Any({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}, None)
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def is_on(hass, entity_id):
|
||||
@ -65,10 +71,10 @@ def async_setup(hass, config):
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
cfg = DEFAULT_CONFIG
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
state = cfg.get(CONF_INITIAL, False)
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(InputBoolean(object_id, name, state, icon))
|
||||
@ -89,7 +95,7 @@ def async_setup(hass, config):
|
||||
attr = 'async_toggle'
|
||||
|
||||
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(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)
|
||||
|
@ -55,14 +55,16 @@ def _cv_input_select(cfg):
|
||||
return cfg
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
||||
cv.slug: vol.All({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1),
|
||||
[cv.string]),
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.All({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_OPTIONS):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]),
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
}, _cv_input_select)})
|
||||
}, required=True, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
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])
|
||||
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(
|
||||
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
|
||||
@ -124,7 +126,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [input_select.async_offset_index(1)
|
||||
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(
|
||||
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
|
||||
@ -137,7 +139,7 @@ def async_setup(hass, config):
|
||||
|
||||
tasks = [input_select.async_offset_index(-1)
|
||||
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(
|
||||
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
||||
|
@ -51,17 +51,21 @@ def _cv_input_slider(cfg):
|
||||
cfg[CONF_INITIAL] = state
|
||||
return cfg
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
||||
cv.slug: vol.All({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_MIN): vol.Coerce(float),
|
||||
vol.Required(CONF_MAX): vol.Coerce(float),
|
||||
vol.Optional(CONF_INITIAL): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float),
|
||||
vol.Range(min=1e-3)),
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
|
||||
}, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.All({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_MIN): vol.Coerce(float),
|
||||
vol.Required(CONF_MAX): vol.Coerce(float),
|
||||
vol.Optional(CONF_INITIAL): vol.Coerce(float),
|
||||
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float),
|
||||
vol.Range(min=1e-3)),
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string
|
||||
}, _cv_input_slider)
|
||||
})
|
||||
}, required=True, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
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])
|
||||
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(
|
||||
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
|
||||
|
@ -2,7 +2,7 @@
|
||||
Native Home Assistant iOS app component.
|
||||
|
||||
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 os
|
||||
|
@ -10,6 +10,7 @@ import csv
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import group
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
@ -20,6 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
|
||||
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,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
"""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 = {
|
||||
key: value for key, value in [
|
||||
(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
|
||||
}
|
||||
|
||||
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):
|
||||
"""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."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
@ -156,7 +177,8 @@ def turn_off(hass, entity_id=None, transition=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):
|
||||
|
@ -17,8 +17,8 @@ from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip'
|
||||
'#flux_led==0.8']
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip'
|
||||
'#flux_led==0.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -135,9 +135,11 @@ class FluxLight(Light):
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
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))
|
||||
elif brightness:
|
||||
elif brightness is not None:
|
||||
if self._mode == 'rgbw':
|
||||
self._bulb.setWarmWhite255(brightness)
|
||||
elif self._mode == 'rgb':
|
||||
|
@ -22,11 +22,12 @@ from homeassistant.components.light import (
|
||||
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
|
||||
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.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['phue==0.8']
|
||||
REQUIREMENTS = ['phue==0.9']
|
||||
|
||||
# Track previously setup bridges
|
||||
_CONFIGURED_BRIDGES = {}
|
||||
@ -37,6 +38,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
|
||||
|
||||
DEFAULT_ALLOW_UNREACHABLE = False
|
||||
DOMAIN = "light"
|
||||
SERVICE_HUE_SCENE = "hue_activate_scene"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
@ -53,6 +56,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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):
|
||||
"""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)
|
||||
|
||||
_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()
|
||||
|
||||
|
||||
|
@ -47,7 +47,7 @@ class LiteJetLight(Light):
|
||||
def _on_load_changed(self):
|
||||
"""Called on a LiteJet thread when a load's state changes."""
|
||||
_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
|
||||
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:
|
||||
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
|
||||
# states. Map them in a dict of lists.
|
||||
pres = gateway.const.Presentation
|
||||
|
@ -81,3 +81,15 @@ toggle:
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to next state
|
||||
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
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
return int(self.wink.brightness() * 255)
|
||||
if self.wink.brightness() is not None:
|
||||
return int(self.wink.brightness() * 255)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light):
|
||||
hue = self.wink.color_hue()
|
||||
saturation = self.wink.color_saturation()
|
||||
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)
|
||||
r_value = int(round(rgb[0]))
|
||||
g_value = int(round(rgb[1]))
|
||||
|
@ -24,26 +24,6 @@ AEOTEC = 0x86
|
||||
AEOTEC_ZW098_LED_BULB = 0x62
|
||||
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_COLD_WHITE = 0x02
|
||||
COLOR_CHANNEL_RED = 0x04
|
||||
@ -51,15 +31,9 @@ COLOR_CHANNEL_GREEN = 0x08
|
||||
COLOR_CHANNEL_BLUE = 0x10
|
||||
|
||||
WORKAROUND_ZW098 = 'zw098'
|
||||
WORKAROUND_DELAY = 'alt_delay'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
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,
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
|
||||
}
|
||||
|
||||
# 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."""
|
||||
if discovery_info is None or zwave.NETWORK is None:
|
||||
return
|
||||
|
||||
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_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:
|
||||
return
|
||||
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)
|
||||
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||
add_devices([ZwaveColorLight(value)])
|
||||
add_devices([ZwaveColorLight(value, refresh, delay)])
|
||||
else:
|
||||
add_devices([ZwaveDimmer(value)])
|
||||
add_devices([ZwaveDimmer(value, refresh, delay)])
|
||||
|
||||
|
||||
def brightness_state(value):
|
||||
@ -105,7 +85,7 @@ def brightness_state(value):
|
||||
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
"""Representation of a Z-Wave dimmer."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, value, refresh, delay):
|
||||
"""Initialize the light."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
@ -113,7 +93,8 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self._alt_delay = None
|
||||
self._delay = delay
|
||||
self._refresh_value = refresh
|
||||
self._zw098 = None
|
||||
|
||||
# Enable appropriate workaround flags for our device
|
||||
@ -126,17 +107,14 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||
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()
|
||||
|
||||
# Used for value change event handling
|
||||
self._refreshing = False
|
||||
self._timer = None
|
||||
|
||||
_LOGGER.debug('self._refreshing=%s self.delay=%s',
|
||||
self._refresh_value, self._delay)
|
||||
dispatcher.connect(
|
||||
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
|
||||
|
||||
@ -149,26 +127,25 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
"""Called when a value has changed on the network."""
|
||||
if self._value.value_id == value.value_id or \
|
||||
self._value.node == value.node:
|
||||
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
self.update_properties()
|
||||
else:
|
||||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
self._refreshing = True
|
||||
self._value.refresh()
|
||||
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
self._timer.cancel()
|
||||
|
||||
if self._alt_delay:
|
||||
self._timer = Timer(5, _refresh_value)
|
||||
if self._refresh_value:
|
||||
if self._refreshing:
|
||||
self._refreshing = False
|
||||
self.update_properties()
|
||||
else:
|
||||
self._timer = Timer(2, _refresh_value)
|
||||
self._timer.start()
|
||||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
self._refreshing = True
|
||||
self._value.refresh()
|
||||
|
||||
self.update_ha_state()
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
self._timer.cancel()
|
||||
|
||||
self._timer = Timer(self._delay, _refresh_value)
|
||||
self._timer.start()
|
||||
self.update_ha_state()
|
||||
else:
|
||||
self.update_properties()
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
@ -213,7 +190,7 @@ def ct_to_rgb(temp):
|
||||
class ZwaveColorLight(ZwaveDimmer):
|
||||
"""Representation of a Z-Wave color changing light."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, value, refresh, delay):
|
||||
"""Initialize the light."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
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
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.5.zip'
|
||||
'#braviarc==0.3.5']
|
||||
'https://github.com/aparraga/braviarc/archive/0.3.6.zip'
|
||||
'#braviarc==0.3.6']
|
||||
|
||||
BRAVIA_CONFIG_FILE = 'bravia.conf'
|
||||
|
||||
|
@ -280,9 +280,9 @@ class CastDevice(MediaPlayerDevice):
|
||||
def new_cast_status(self, status):
|
||||
"""Called when a new cast status is received."""
|
||||
self.cast_status = status
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def new_media_status(self, status):
|
||||
"""Called when a new media status is received."""
|
||||
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:
|
||||
return STATE_IDLE
|
||||
|
||||
if self._properties['speed'] == 0:
|
||||
if self._properties['speed'] == 0 and not self._properties['live']:
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_PLAYING
|
||||
@ -120,7 +120,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
self._properties = self._server.Player.GetProperties(
|
||||
player_id,
|
||||
['time', 'totaltime', 'speed']
|
||||
['time', 'totaltime', 'speed', 'live']
|
||||
)
|
||||
|
||||
self._item = self._server.Player.GetItem(
|
||||
@ -163,7 +163,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""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']
|
||||
|
||||
return (
|
||||
|
@ -128,6 +128,8 @@ class SamsungTVDevice(MediaPlayerDevice):
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self.send_key('KEY_POWEROFF')
|
||||
# Force closing of remote session to provide instant UI feedback
|
||||
self.get_remote().close()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up the media player."""
|
||||
|
@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
player = soco.SoCo(discovery_info)
|
||||
|
||||
# if device allready exists by config
|
||||
if player.uid in DEVICES:
|
||||
if player.uid in [x.unique_id for x in DEVICES]:
|
||||
return True
|
||||
|
||||
if player.is_visible:
|
||||
@ -350,9 +350,6 @@ class SonosDevice(MediaPlayerDevice):
|
||||
|
||||
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
|
||||
if self._last_avtransport_event:
|
||||
variables = self._last_avtransport_event.variables
|
||||
@ -394,6 +391,10 @@ class SonosDevice(MediaPlayerDevice):
|
||||
self._coordinator = None
|
||||
|
||||
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(
|
||||
[('InstanceID', 0)]
|
||||
)
|
||||
@ -407,7 +408,23 @@ class SonosDevice(MediaPlayerDevice):
|
||||
current_media_uri.startswith('x-sonosapi-stream:') or \
|
||||
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
|
||||
media_image_url = self._format_media_image_url(
|
||||
current_media_uri
|
||||
@ -506,6 +523,8 @@ class SonosDevice(MediaPlayerDevice):
|
||||
self._support_previous_track = support_previous_track
|
||||
self._support_next_track = support_next_track
|
||||
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
|
||||
# pylint: disable=protected-access
|
||||
@ -513,7 +532,7 @@ class SonosDevice(MediaPlayerDevice):
|
||||
if device.entity_id is not self.entity_id:
|
||||
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()
|
||||
else:
|
||||
self._player_volume = None
|
||||
@ -714,10 +733,13 @@ class SonosDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def source(self):
|
||||
"""Name of the current input source."""
|
||||
if self._is_playing_line_in:
|
||||
return SUPPORT_SOURCE_LINEIN
|
||||
if self._is_playing_tv:
|
||||
return SUPPORT_SOURCE_TV
|
||||
if self._coordinator:
|
||||
return self._coordinator.source
|
||||
else:
|
||||
if self._is_playing_line_in:
|
||||
return SUPPORT_SOURCE_LINEIN
|
||||
elif self._is_playing_tv:
|
||||
return SUPPORT_SOURCE_TV
|
||||
|
||||
return None
|
||||
|
||||
|
@ -111,9 +111,11 @@ class LogitechMediaServer(object):
|
||||
|
||||
def query(self, *parameters):
|
||||
"""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):
|
||||
"""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)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['rxv==0.3.1']
|
||||
REQUIREMENTS = ['rxv==0.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
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
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
CONF_SOURCE_NAMES = 'source_names'
|
||||
CONF_SOURCE_IGNORE = 'source_ignore'
|
||||
@ -187,8 +182,16 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
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
|
||||
|
||||
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.event import threaded_listener_factory
|
||||
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__)
|
||||
|
||||
DOMAIN = "mqtt"
|
||||
DOMAIN = 'mqtt'
|
||||
|
||||
MQTT_CLIENT = None
|
||||
|
||||
@ -33,16 +34,15 @@ REQUIREMENTS = ['paho-mqtt==1.2']
|
||||
|
||||
CONF_EMBEDDED = 'embedded'
|
||||
CONF_BROKER = 'broker'
|
||||
CONF_PORT = 'port'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_KEEPALIVE = 'keepalive'
|
||||
CONF_USERNAME = 'username'
|
||||
CONF_PASSWORD = 'password'
|
||||
CONF_CERTIFICATE = 'certificate'
|
||||
CONF_CLIENT_KEY = 'client_key'
|
||||
CONF_CLIENT_CERT = 'client_cert'
|
||||
CONF_TLS_INSECURE = 'tls_insecure'
|
||||
CONF_PROTOCOL = 'protocol'
|
||||
|
||||
CONF_BIRTH_MESSAGE = 'birth_message'
|
||||
CONF_WILL_MESSAGE = 'will_message'
|
||||
|
||||
CONF_STATE_TOPIC = 'state_topic'
|
||||
CONF_COMMAND_TOPIC = 'command_topic'
|
||||
@ -78,20 +78,27 @@ def valid_publish_topic(value):
|
||||
"""Validate that we can publish using this MQTT topic."""
|
||||
return valid_subscribe_topic(value, invalid_chars='#+\0')
|
||||
|
||||
|
||||
_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2]))
|
||||
_HBMQTT_CONFIG_SCHEMA = vol.Schema(dict)
|
||||
|
||||
CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \
|
||||
'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({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_CLIENT_ID): cv.string,
|
||||
vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
vol.Optional(CONF_BROKER): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
||||
@ -103,6 +110,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||
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)
|
||||
|
||||
@ -130,10 +139,10 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
# Service call validation schema
|
||||
MQTT_PUBLISH_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
||||
vol.Exclusive(ATTR_PAYLOAD, 'payload'): object,
|
||||
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string,
|
||||
vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||
vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object,
|
||||
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 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)
|
||||
|
||||
|
||||
@ -196,7 +205,7 @@ def _setup_server(hass, config):
|
||||
server = prepare_setup_platform(hass, config, DOMAIN, 'server')
|
||||
|
||||
if server is None:
|
||||
_LOGGER.error('Unable to load embedded server.')
|
||||
_LOGGER.error("Unable to load embedded server")
|
||||
return None
|
||||
|
||||
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
|
||||
client_key, client_cert, tls_insecure = None, None, None
|
||||
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
|
||||
|
||||
if broker_in_conf:
|
||||
@ -241,15 +250,18 @@ def setup(hass, config):
|
||||
certificate = os.path.join(os.path.dirname(__file__),
|
||||
'addtrustexternalcaroot.crt')
|
||||
|
||||
will_message = conf.get(CONF_WILL_MESSAGE)
|
||||
birth_message = conf.get(CONF_BIRTH_MESSAGE)
|
||||
|
||||
global MQTT_CLIENT
|
||||
try:
|
||||
MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive,
|
||||
username, password, certificate, client_key,
|
||||
client_cert, tls_insecure, protocol)
|
||||
client_cert, tls_insecure, protocol, will_message,
|
||||
birth_message)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker "
|
||||
"itself.")
|
||||
"Please check your settings and the broker itself")
|
||||
return False
|
||||
|
||||
def stop_mqtt(event):
|
||||
@ -274,7 +286,7 @@ def setup(hass, config):
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to '%s': rendering payload template of "
|
||||
"'%s' failed because %s.",
|
||||
"'%s' failed because %s",
|
||||
msg_topic, payload_template, exc)
|
||||
return
|
||||
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,
|
||||
password, certificate, client_key, client_cert,
|
||||
tls_insecure, protocol):
|
||||
tls_insecure, protocol, will_message, birth_message):
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
self.hass = hass
|
||||
self.topics = {}
|
||||
self.progress = {}
|
||||
self.birth_message = birth_message
|
||||
|
||||
if protocol == PROTOCOL_31:
|
||||
proto = mqtt.MQTTv31
|
||||
@ -329,7 +342,11 @@ class MQTT(object):
|
||||
self._mqttc.on_connect = self._mqtt_on_connect
|
||||
self._mqttc.on_disconnect = self._mqtt_on_disconnect
|
||||
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)
|
||||
|
||||
def publish(self, topic, payload, qos, retain):
|
||||
@ -365,7 +382,8 @@ class MQTT(object):
|
||||
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
|
||||
"""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:
|
||||
_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
|
||||
if qos is not None:
|
||||
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):
|
||||
"""Subscribe successful callback."""
|
||||
@ -404,7 +427,7 @@ class MQTT(object):
|
||||
"MQTT topic: %s, Payload: %s", msg.topic,
|
||||
msg.payload)
|
||||
else:
|
||||
_LOGGER.debug("received message on %s: %s",
|
||||
_LOGGER.debug("Received message on %s: %s",
|
||||
msg.topic, payload)
|
||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
@ -440,14 +463,14 @@ class MQTT(object):
|
||||
while True:
|
||||
try:
|
||||
if self._mqttc.reconnect() == 0:
|
||||
_LOGGER.info('Successfully reconnected to the MQTT server')
|
||||
_LOGGER.info("Successfully reconnected to the MQTT server")
|
||||
break
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
wait_time = min(2**tries, MAX_RECONNECT_WAIT)
|
||||
_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)
|
||||
# It is ok to sleep here as we are in the MQTT thread.
|
||||
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
|
||||
https://home-assistant.io/components/mqtt/#use-the-embedded-broker
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.mqtt import PROTOCOL_311
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
REQUIREMENTS = ['hbmqtt==0.7.1']
|
||||
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):
|
||||
"""Initialize MQTT Server."""
|
||||
from hbmqtt.broker import BrokerException
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
from hbmqtt.broker import Broker, BrokerException
|
||||
|
||||
try:
|
||||
passwd = tempfile.NamedTemporaryFile()
|
||||
@ -48,29 +28,20 @@ def start(hass, server_config):
|
||||
else:
|
||||
client_config = None
|
||||
|
||||
start_server = asyncio.gather(broker_coro(loop, server_config),
|
||||
loop=loop)
|
||||
loop.run_until_complete(start_server)
|
||||
# Result raises exception if one was raised during startup
|
||||
broker = start_server.result()[0]
|
||||
broker = Broker(server_config, hass.loop)
|
||||
run_coroutine_threadsafe(broker.start(), hass.loop).result()
|
||||
except BrokerException:
|
||||
logging.getLogger(__name__).exception('Error initializing MQTT server')
|
||||
loop.close()
|
||||
return False, None
|
||||
finally:
|
||||
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):
|
||||
"""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()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mqtt_server)
|
||||
|
||||
return True, client_config
|
||||
|
||||
|
@ -38,7 +38,7 @@ DEFAULT_VERSION = 1.4
|
||||
DEFAULT_BAUD_RATE = 115200
|
||||
DEFAULT_TCP_PORT = 5003
|
||||
DOMAIN = 'mysensors'
|
||||
GATEWAYS = None
|
||||
MYSENSORS_GATEWAYS = 'mysensors_gateways'
|
||||
MQTT_COMPONENT = 'mqtt'
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/theolind/pymysensors/archive/'
|
||||
@ -132,9 +132,15 @@ def setup(hass, config):
|
||||
|
||||
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
|
||||
global GATEWAYS
|
||||
GATEWAYS = {}
|
||||
gateways = []
|
||||
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
|
||||
|
||||
for index, gway in enumerate(conf_gateways):
|
||||
@ -146,17 +152,19 @@ def setup(hass, config):
|
||||
tcp_port = gway.get(CONF_TCP_PORT)
|
||||
in_prefix = gway.get(CONF_TOPIC_IN_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,
|
||||
out_prefix)
|
||||
if GATEWAYS[device] is None:
|
||||
GATEWAYS.pop(device)
|
||||
if ready_gateway is not None:
|
||||
gateways.append(ready_gateway)
|
||||
|
||||
if not GATEWAYS:
|
||||
if not gateways:
|
||||
_LOGGER.error(
|
||||
'No devices could be setup as gateways, check your configuration')
|
||||
return False
|
||||
|
||||
hass.data[MYSENSORS_GATEWAYS] = gateways
|
||||
|
||||
for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
|
||||
'cover']:
|
||||
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 = [
|
||||
'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__)
|
||||
|
||||
|
@ -25,9 +25,7 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.frontend import add_manifest_json_key
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/web-push-libs/pywebpush/archive/'
|
||||
'e743dc92558fc62178d255c0018920d74fa778ed.zip#'
|
||||
'pywebpush==0.5.0', 'PyJWT==1.4.2']
|
||||
REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2']
|
||||
|
||||
DEPENDENCIES = ['frontend']
|
||||
|
||||
@ -141,11 +139,23 @@ def _load_config(filename):
|
||||
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):
|
||||
"""Save configuration."""
|
||||
try:
|
||||
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:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
return False
|
||||
|
@ -2,7 +2,7 @@
|
||||
iOS push notification platform for notify component.
|
||||
|
||||
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
|
||||
from datetime import datetime, timezone
|
||||
@ -48,8 +48,8 @@ def get_service(hass, config):
|
||||
if not ios.devices_with_push():
|
||||
_LOGGER.error(("The notify.ios platform was loaded but no "
|
||||
"devices exist! Please check the documentation at "
|
||||
"https://home-assistant.io/components/notify.ios/ "
|
||||
"for more information"))
|
||||
"https://home-assistant.io/ecosystem/ios/notifications"
|
||||
"/ for more information"))
|
||||
return None
|
||||
|
||||
return iOSNotificationService()
|
||||
|
@ -26,8 +26,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the NMA notification service."""
|
||||
response = requests.get(_RESOURCE + 'verify',
|
||||
params={"apikey": config[CONF_API_KEY]})
|
||||
parameters = {
|
||||
'apikey': config[CONF_API_KEY],
|
||||
}
|
||||
response = requests.get(
|
||||
'{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5)
|
||||
tree = ET.fromstring(response.content)
|
||||
|
||||
if tree[0].tag == 'error':
|
||||
@ -47,14 +50,15 @@ class NmaNotificationService(BaseNotificationService):
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
data = {
|
||||
"apikey": self._api_key,
|
||||
"application": 'home-assistant',
|
||||
"event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
|
||||
"description": message,
|
||||
"priority": 0,
|
||||
'apikey': self._api_key,
|
||||
'application': 'home-assistant',
|
||||
'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
|
||||
'description': message,
|
||||
'priority': 0,
|
||||
}
|
||||
|
||||
response = requests.get(_RESOURCE + 'notify', params=data)
|
||||
response = requests.get(
|
||||
'{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5)
|
||||
tree = ET.fromstring(response.content)
|
||||
|
||||
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)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==3.6.0']
|
||||
REQUIREMENTS = ['sendgrid==3.6.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -55,8 +55,7 @@ def async_create(hass, message, title=None, notification_id=None):
|
||||
] if value is not None
|
||||
}
|
||||
|
||||
hass.loop.create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
|
||||
hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
DOMAIN = 'recorder'
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.2']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.3']
|
||||
|
||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||
|
@ -94,6 +94,7 @@ def valid_sensor(value):
|
||||
def _valid_light_switch(value):
|
||||
return _valid_device(value, "light_switch")
|
||||
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
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
|
||||
https://home-assistant.io/components/script/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@ -40,7 +41,7 @@ _SCRIPT_ENTRY_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)
|
||||
|
||||
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})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Load the scripts from the configuration."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_SCRIPTS)
|
||||
|
||||
@asyncio.coroutine
|
||||
def service_handler(service):
|
||||
"""Execute a service call to script.<script name>."""
|
||||
entity_id = ENTITY_ID_FORMAT.format(service.service)
|
||||
@ -84,38 +87,48 @@ def setup(hass, config):
|
||||
if script.is_on:
|
||||
_LOGGER.warning("Script %s already running.", entity_id)
|
||||
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():
|
||||
alias = cfg.get(CONF_ALIAS, object_id)
|
||||
script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE])
|
||||
component.add_entities((script,))
|
||||
hass.services.register(DOMAIN, object_id, service_handler,
|
||||
schema=SCRIPT_SERVICE_SCHEMA)
|
||||
scripts.append(script)
|
||||
hass.services.async_register(DOMAIN, object_id, service_handler,
|
||||
schema=SCRIPT_SERVICE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(scripts)
|
||||
|
||||
@asyncio.coroutine
|
||||
def turn_on_service(service):
|
||||
"""Call a service to turn script on."""
|
||||
# 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.
|
||||
for script in component.extract_from_service(service):
|
||||
turn_on(hass, script.entity_id, service.data.get(ATTR_VARIABLES))
|
||||
var = 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):
|
||||
"""Cancel a script."""
|
||||
for script in component.extract_from_service(service):
|
||||
script.turn_off()
|
||||
# Stopping a script is ok to be done in parallel
|
||||
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):
|
||||
"""Toggle a script."""
|
||||
for script in component.extract_from_service(service):
|
||||
script.toggle()
|
||||
for script in component.async_extract_from_service(service):
|
||||
yield from script.async_toggle()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_TURN_ON, turn_on_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_TURN_OFF, turn_off_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
hass.services.async_register(DOMAIN, SERVICE_TOGGLE, toggle_service,
|
||||
schema=SCRIPT_TURN_ONOFF_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
@ -124,6 +137,7 @@ class ScriptEntity(ToggleEntity):
|
||||
|
||||
def __init__(self, hass, object_id, name, sequence):
|
||||
"""Initialize the script."""
|
||||
self.object_id = object_id
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
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 self.script.is_running
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""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."""
|
||||
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,
|
||||
units=units,
|
||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||
forecast_data.update()
|
||||
forecast_data.update_currently()
|
||||
except ValueError as 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]:
|
||||
sensors.append(DarkSkySensor(forecast_data, variable, name))
|
||||
|
||||
add_devices(sensors)
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class DarkSkySensor(Entity):
|
||||
@ -139,8 +140,6 @@ class DarkSkySensor(Entity):
|
||||
self._state = None
|
||||
self._unit_of_measurement = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
@ -277,8 +276,6 @@ class DarkSkyData(object):
|
||||
self.update_hourly = Throttle(interval)(self._update_hourly)
|
||||
self.update_daily = Throttle(interval)(self._update_daily)
|
||||
|
||||
self.update()
|
||||
|
||||
def _update(self):
|
||||
"""Get the latest data from Dark Sky."""
|
||||
import forecastio
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['schiene==0.17']
|
||||
REQUIREMENTS = ['schiene==0.18']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
Support for Home Assistant iOS app sensors.
|
||||
|
||||
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.helpers.entity import Entity
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['batinfo==0.3']
|
||||
REQUIREMENTS = ['batinfo==0.4.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.util import Throttle
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
|
||||
|
||||
REQUIREMENTS = ['miflora==0.1.9']
|
||||
REQUIREMENTS = ['miflora==0.1.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -41,6 +41,7 @@ SENSOR_TYPES = {
|
||||
'light': ['Light intensity', 'lux'],
|
||||
'moisture': ['Moisture', '%'],
|
||||
'conductivity': ['Conductivity', 'µS/cm'],
|
||||
'battery': ['Battery', '%'],
|
||||
}
|
||||
|
||||
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)
|
||||
sensor_type = config.get(CONF_TYPE)
|
||||
|
||||
hass.loop.create_task(async_add_devices(
|
||||
[MinMaxSensor(hass, entity_ids, name, sensor_type)], True))
|
||||
yield from async_add_devices(
|
||||
[MinMaxSensor(hass, entity_ids, name, sensor_type)], True)
|
||||
return True
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@ class MinMaxSensor(Entity):
|
||||
_LOGGER.warning("Unable to store state. "
|
||||
"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(
|
||||
hass, entity_ids, async_min_max_sensor_state_listener)
|
||||
|
@ -8,6 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT)
|
||||
@ -55,13 +56,14 @@ class MqttSensor(Entity):
|
||||
self._qos = qos
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
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)
|
||||
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)
|
||||
|
||||
|
@ -20,7 +20,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if discovery_info is None:
|
||||
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
|
||||
# states. Map them in a dict of lists.
|
||||
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__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provied by OpenWeatherMap"
|
||||
CONF_ATTRIBUTION = "Data provided by OpenWeatherMap"
|
||||
CONF_FORECAST = 'forecast'
|
||||
|
||||
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)
|
||||
maximum = config.get(CONF_MAXIMUM)
|
||||
|
||||
hass.loop.create_task(async_add_devices(
|
||||
[RandomSensor(name, minimum, maximum)], True))
|
||||
yield from async_add_devices([RandomSensor(name, minimum, maximum)], True)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -50,8 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
name = config.get(CONF_NAME)
|
||||
sampling_size = config.get(CONF_SAMPLING_SIZE)
|
||||
|
||||
hass.loop.create_task(async_add_devices(
|
||||
[StatisticsSensor(hass, entity_id, name, sampling_size)], True))
|
||||
yield from async_add_devices(
|
||||
[StatisticsSensor(hass, entity_id, name, sampling_size)], True)
|
||||
return True
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ class StatisticsSensor(Entity):
|
||||
except ValueError:
|
||||
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(
|
||||
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