Merge pull request #6199 from home-assistant/release-0-39

0.39
This commit is contained in:
Paulus Schoutsen 2017-02-25 14:36:17 -08:00 committed by GitHub
commit a19a285bb0
349 changed files with 10495 additions and 3239 deletions

View File

@ -41,6 +41,9 @@ omit =
homeassistant/components/insteon_local.py homeassistant/components/insteon_local.py
homeassistant/components/*/insteon_local.py homeassistant/components/*/insteon_local.py
homeassistant/components/insteon_plm.py
homeassistant/components/*/insteon_plm.py
homeassistant/components/ios.py homeassistant/components/ios.py
homeassistant/components/*/ios.py homeassistant/components/*/ios.py
@ -88,6 +91,9 @@ omit =
homeassistant/components/verisure.py homeassistant/components/verisure.py
homeassistant/components/*/verisure.py homeassistant/components/*/verisure.py
homeassistant/components/volvooncall.py
homeassistant/components/*/volvooncall.py
homeassistant/components/*/webostv.py homeassistant/components/*/webostv.py
homeassistant/components/wemo.py homeassistant/components/wemo.py
@ -149,10 +155,12 @@ omit =
homeassistant/components/climate/heatmiser.py homeassistant/components/climate/heatmiser.py
homeassistant/components/climate/homematic.py homeassistant/components/climate/homematic.py
homeassistant/components/climate/knx.py homeassistant/components/climate/knx.py
homeassistant/components/climate/oem.py
homeassistant/components/climate/proliphix.py homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/myq.py
homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
homeassistant/components/cover/wink.py homeassistant/components/cover/wink.py
@ -181,9 +189,7 @@ omit =
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/trackr.py homeassistant/components/device_tracker/trackr.py
homeassistant/components/device_tracker/ubus.py homeassistant/components/device_tracker/ubus.py
homeassistant/components/device_tracker/volvooncall.py
homeassistant/components/device_tracker/xiaomi.py homeassistant/components/device_tracker/xiaomi.py
homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/emoncms_history.py homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py homeassistant/components/emulated_hue/upnp.py
@ -207,6 +213,7 @@ omit =
homeassistant/components/light/tikteck.py homeassistant/components/light/tikteck.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/piglow.py homeassistant/components/light/piglow.py
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
@ -216,6 +223,7 @@ omit =
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/denonavr.py homeassistant/components/media_player/denonavr.py
@ -224,6 +232,7 @@ omit =
homeassistant/components/media_player/emby.py homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
homeassistant/components/media_player/hdmi_cec.py homeassistant/components/media_player/hdmi_cec.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
@ -233,6 +242,7 @@ omit =
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/nad.py homeassistant/components/media_player/nad.py
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/openhome.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py homeassistant/components/media_player/philips_js.py
@ -267,6 +277,7 @@ omit =
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushetta.py
homeassistant/components/notify/pushover.py homeassistant/components/notify/pushover.py
homeassistant/components/notify/pushsafer.py
homeassistant/components/notify/rest.py homeassistant/components/notify/rest.py
homeassistant/components/notify/sendgrid.py homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
@ -281,6 +292,7 @@ omit =
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/nuimo_controller.py homeassistant/components/nuimo_controller.py
homeassistant/components/remote/harmony.py homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/sensor/amcrest.py homeassistant/components/sensor/amcrest.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
@ -299,13 +311,17 @@ omit =
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/ebox.py
homeassistant/components/sensor/efergy.py homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/emoncms.py homeassistant/components/sensor/emoncms.py
homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py
homeassistant/components/sensor/fido.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gpsd.py
@ -334,6 +350,7 @@ omit =
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pocketcasts.py
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/qnap.py homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
@ -358,6 +375,7 @@ omit =
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/ups.py
homeassistant/components/sensor/usps.py homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
@ -386,6 +404,7 @@ omit =
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py homeassistant/components/switch/wake_on_lan.py
homeassistant/components/telegram_webhooks.py
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/amazon_polly.py
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py

View File

@ -26,5 +26,5 @@ If the code does not interact with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass** - [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] Tests have been added to verify that the new code works. - [ ] Tests have been added to verify that the new code works.
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16 [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L14
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51 [ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L54

View File

@ -75,7 +75,8 @@ Build home automation on top of your devices:
`Instapush <https://instapush.im>`__, `Notify My Android `Instapush <https://instapush.im>`__, `Notify My Android
(NMA) <http://www.notifymyandroid.com/>`__, (NMA) <http://www.notifymyandroid.com/>`__,
`PushBullet <https://www.pushbullet.com/>`__, `PushBullet <https://www.pushbullet.com/>`__,
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__, `PushOver <https://pushover.net/>`__,
`Slack <https://slack.com/>`__,
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber `Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
(XMPP) <http://xmpp.org>`__ (XMPP) <http://xmpp.org>`__

View File

@ -16,6 +16,7 @@ import homeassistant.components as core_components
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
import homeassistant.config as conf_util import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.package as pkg_util import homeassistant.util.package as pkg_util
from homeassistant.util.async import ( from homeassistant.util.async import (
@ -166,7 +167,7 @@ def _async_setup_component(hass: core.HomeAssistant,
loader.set_component(domain, None) loader.set_component(domain, None)
return False return False
hass.config.components.append(component.DOMAIN) hass.config.components.add(component.DOMAIN)
hass.bus.async_fire( hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
@ -298,6 +299,10 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
# Load dependencies # Load dependencies
for component in getattr(platform, 'DEPENDENCIES', []): for component in getattr(platform, 'DEPENDENCIES', []):
if component in loader.DEPENDENCY_BLACKLIST:
raise HomeAssistantError(
'{} is not allowed to be a dependency.'.format(component))
res = yield from async_setup_component(hass, component, config) res = yield from async_setup_component(hass, component, config)
if not res: if not res:
_LOGGER.error( _LOGGER.error(
@ -386,7 +391,7 @@ def async_from_config_dict(config: Dict[str, Any],
None, conf_util.process_ha_config_upgrade, hass) None, conf_util.process_ha_config_upgrade, hass)
if enable_log: if enable_log:
enable_logging(hass, verbose, log_rotate_days) async_enable_logging(hass, verbose, log_rotate_days)
hass.config.skip_pip = skip_pip hass.config.skip_pip = skip_pip
if skip_pip: if skip_pip:
@ -429,7 +434,13 @@ def async_from_config_dict(config: Dict[str, Any],
service.HASS = hass service.HASS = hass
# Setup the components # Setup the components
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components)
for domain in loader.load_order_components(components): for domain in loader.load_order_components(components):
if domain in dependency_blacklist:
raise HomeAssistantError(
'{} is not allowed to be a dependency'.format(domain))
yield from _async_setup_component(hass, domain, config) yield from _async_setup_component(hass, domain, config)
setup_lock.release() setup_lock.release()
@ -488,7 +499,7 @@ def async_from_config_file(config_path: str,
yield from hass.loop.run_in_executor( yield from hass.loop.run_in_executor(
None, mount_local_lib_path, config_dir) None, mount_local_lib_path, config_dir)
enable_logging(hass, verbose, log_rotate_days) async_enable_logging(hass, verbose, log_rotate_days)
try: try:
config_dict = yield from hass.loop.run_in_executor( config_dict = yield from hass.loop.run_in_executor(
@ -503,11 +514,12 @@ def async_from_config_file(config_path: str,
return hass return hass
def enable_logging(hass: core.HomeAssistant, verbose: bool=False, @core.callback
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None: log_rotate_days=None) -> None:
"""Setup the logging. """Setup the logging.
Async friendly. This method must be run in the event loop.
""" """
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
@ -537,10 +549,6 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
except ImportError: except ImportError:
pass pass
# AsyncHandler allready exists?
if hass.data.get(core.DATA_ASYNCHANDLER):
return
# Log errors to a file if we have write access to file or config dir # Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path(ERROR_LOG_FILENAME) err_log_path = hass.config.path(ERROR_LOG_FILENAME)
err_path_exists = os.path.isfile(err_log_path) err_path_exists = os.path.isfile(err_log_path)
@ -561,7 +569,15 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
async_handler = AsyncHandler(hass.loop, err_handler) async_handler = AsyncHandler(hass.loop, err_handler)
hass.data[core.DATA_ASYNCHANDLER] = async_handler
@asyncio.coroutine
def async_stop_async_handler(event):
"""Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler)
yield from async_handler.async_close(blocking=True)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('') logger = logging.getLogger('')
logger.addHandler(async_handler) logger.addHandler(async_handler)

View File

@ -156,10 +156,18 @@ def async_setup(hass, config):
return return
try: try:
yield from conf_util.async_check_ha_config_file(hass) errors = yield from conf_util.async_check_ha_config_file(hass)
except HomeAssistantError: except HomeAssistantError:
return return
if errors:
notif = get_component('persistent_notification')
_LOGGER.error(errors)
notif.async_create(
hass, "Config error. See dev-info panel for details.",
"Config validating", "{0}.check_config".format(ha.DOMAIN))
return
if call.service == SERVICE_HOMEASSISTANT_RESTART: if call.service == SERVICE_HOMEASSISTANT_RESTART:
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))

View File

@ -4,16 +4,20 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC).
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.envisalink/ https://home-assistant.io/components/alarm_control_panel.envisalink/
""" """
from os import path import asyncio
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.components.envisalink import ( from homeassistant.components.envisalink import (
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE) CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID) STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
@ -22,8 +26,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink'] DEPENDENCIES = ['envisalink']
DEVICES = []
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
ATTR_KEYPRESS = 'keypress' ATTR_KEYPRESS = 'keypress'
ALARM_KEYPRESS_SCHEMA = vol.Schema({ ALARM_KEYPRESS_SCHEMA = vol.Schema({
@ -32,68 +34,72 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
configured_partitions = discovery_info['partitions']
code = discovery_info[CONF_CODE]
panic_type = discovery_info[CONF_PANIC]
devices = []
for part_num in configured_partitions:
device_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
device = EnvisalinkAlarm(
hass,
part_num,
device_config_data[CONF_PARTITIONNAME],
code,
panic_type,
hass.data[DATA_EVL].alarm_state['partition'][part_num],
hass.data[DATA_EVL]
)
devices.append(device)
yield from async_add_devices(devices)
@callback
def alarm_keypress_handler(service): def alarm_keypress_handler(service):
"""Map services to methods on Alarm.""" """Map services to methods on Alarm."""
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
keypress = service.data.get(ATTR_KEYPRESS) keypress = service.data.get(ATTR_KEYPRESS)
_target_devices = [device for device in DEVICES target_devices = [device for device in devices
if device.entity_id in entity_ids] if device.entity_id in entity_ids]
for device in _target_devices: for device in target_devices:
EnvisalinkAlarm.alarm_keypress(device, keypress) device.async_alarm_keypress(keypress)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Envisalink alarm panels."""
_configured_partitions = discovery_info['partitions']
_code = discovery_info[CONF_CODE]
_panic_type = discovery_info[CONF_PANIC]
for part_num in _configured_partitions:
_device_config_data = PARTITION_SCHEMA(
_configured_partitions[part_num])
_device = EnvisalinkAlarm(
part_num,
_device_config_data[CONF_PARTITIONNAME],
_code,
_panic_type,
EVL_CONTROLLER.alarm_state['partition'][part_num],
EVL_CONTROLLER)
DEVICES.append(_device)
add_devices(DEVICES)
# Register Envisalink specific services # Register Envisalink specific services
descriptions = load_yaml_config_file( descriptions = yield from hass.loop.run_in_executor(
path.join(path.dirname(__file__), 'services.yaml')) None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
descriptions.get(SERVICE_ALARM_KEYPRESS), schema=ALARM_KEYPRESS_SCHEMA)
hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS,
alarm_keypress_handler,
descriptions.get(SERVICE_ALARM_KEYPRESS),
schema=ALARM_KEYPRESS_SCHEMA)
return True return True
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Representation of an Envisalink-based alarm panel.""" """Representation of an Envisalink-based alarm panel."""
def __init__(self, partition_number, alarm_name, code, panic_type, info, def __init__(self, hass, partition_number, alarm_name, code, panic_type,
controller): info, controller):
"""Initialize the alarm panel.""" """Initialize the alarm panel."""
from pydispatch import dispatcher
self._partition_number = partition_number self._partition_number = partition_number
self._code = code self._code = code
self._panic_type = panic_type self._panic_type = panic_type
_LOGGER.debug("Setting up alarm: %s", alarm_name)
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
dispatcher.connect(
self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
sender=dispatcher.Any)
dispatcher.connect(
self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
sender=dispatcher.Any)
_LOGGER.debug("Setting up alarm: %s", alarm_name)
super().__init__(alarm_name, info, controller)
async_dispatcher_connect(
hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
async_dispatcher_connect(
hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
@callback
def _update_callback(self, partition): def _update_callback(self, partition):
"""Update HA state, if needed.""" """Update HA state, if needed."""
if partition is None or int(partition) == self._partition_number: if partition is None or int(partition) == self._partition_number:
@ -126,39 +132,44 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
state = STATE_ALARM_DISARMED state = STATE_ALARM_DISARMED
return state return state
def alarm_disarm(self, code=None): @asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if code: if code:
EVL_CONTROLLER.disarm_partition(str(code), self.hass.data[DATA_EVL].disarm_partition(
self._partition_number) str(code), self._partition_number)
else: else:
EVL_CONTROLLER.disarm_partition(str(self._code), self.hass.data[DATA_EVL].disarm_partition(
self._partition_number) str(self._code), self._partition_number)
def alarm_arm_home(self, code=None): @asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
if code: if code:
EVL_CONTROLLER.arm_stay_partition(str(code), self.hass.data[DATA_EVL].arm_stay_partition(
self._partition_number) str(code), self._partition_number)
else: else:
EVL_CONTROLLER.arm_stay_partition(str(self._code), self.hass.data[DATA_EVL].arm_stay_partition(
self._partition_number) str(self._code), self._partition_number)
def alarm_arm_away(self, code=None): @asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
if code: if code:
EVL_CONTROLLER.arm_away_partition(str(code), self.hass.data[DATA_EVL].arm_away_partition(
self._partition_number) str(code), self._partition_number)
else: else:
EVL_CONTROLLER.arm_away_partition(str(self._code), self.hass.data[DATA_EVL].arm_away_partition(
self._partition_number) str(self._code), self._partition_number)
def alarm_trigger(self, code=None): @asyncio.coroutine
def async_alarm_trigger(self, code=None):
"""Alarm trigger command. Will be used to trigger a panic alarm.""" """Alarm trigger command. Will be used to trigger a panic alarm."""
EVL_CONTROLLER.panic_alarm(self._panic_type) self.hass.data[DATA_EVL].panic_alarm(self._panic_type)
def alarm_keypress(self, keypress=None): @callback
def async_alarm_keypress(self, keypress=None):
"""Send custom keypress.""" """Send custom keypress."""
if keypress: if keypress:
EVL_CONTROLLER.keypresses_to_partition(self._partition_number, self.hass.data[DATA_EVL].keypresses_to_partition(
keypress) self._partition_number, keypress)

View File

@ -4,10 +4,12 @@ This platform enables the possibility to control a MQTT alarm.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.mqtt/ https://home-assistant.io/components/alarm_control_panel.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import ( from homeassistant.const import (
@ -41,10 +43,10 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the MQTT platform.""" """Setup the MQTT platform."""
add_devices([MqttAlarm( yield from async_add_devices([MqttAlarm(
hass,
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),
@ -58,11 +60,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttAlarm(alarm.AlarmControlPanel): class MqttAlarm(alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
def __init__(self, hass, name, state_topic, command_topic, qos, def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
payload_disarm, payload_arm_home, payload_arm_away, code): payload_arm_home, payload_arm_away, code):
"""Initalize the MQTT alarm panel.""" """Initalize the MQTT alarm panel."""
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._hass = hass
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
@ -72,6 +73,12 @@ class MqttAlarm(alarm.AlarmControlPanel):
self._payload_arm_away = payload_arm_away self._payload_arm_away = payload_arm_away
self._code = code self._code = code
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method must be run in the event loop and returns a coroutine.
"""
@callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
@ -80,9 +87,10 @@ class MqttAlarm(alarm.AlarmControlPanel):
_LOGGER.warning('Received unexpected payload: %s', payload) _LOGGER.warning('Received unexpected payload: %s', payload)
return return
self._state = payload self._state = payload
self.update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
mqtt.subscribe(hass, self._state_topic, message_received, self._qos) return mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos)
@property @property
def should_poll(self): def should_poll(self):
@ -104,26 +112,38 @@ class MqttAlarm(alarm.AlarmControlPanel):
"""One or more characters if code is defined.""" """One or more characters if code is defined."""
return None if self._code is None else '.+' return None if self._code is None else '.+'
def alarm_disarm(self, code=None): @asyncio.coroutine
"""Send disarm command.""" def async_alarm_disarm(self, code=None):
"""Send disarm command.
This method is a coroutine.
"""
if not self._validate_code(code, 'disarming'): if not self._validate_code(code, 'disarming'):
return return
mqtt.publish(self.hass, self._command_topic, mqtt.async_publish(
self._payload_disarm, self._qos) self.hass, self._command_topic, self._payload_disarm, self._qos)
def alarm_arm_home(self, code=None): @asyncio.coroutine
"""Send arm home command.""" def async_alarm_arm_home(self, code=None):
"""Send arm home command.
This method is a coroutine.
"""
if not self._validate_code(code, 'arming home'): if not self._validate_code(code, 'arming home'):
return return
mqtt.publish(self.hass, self._command_topic, mqtt.async_publish(
self._payload_arm_home, self._qos) self.hass, self._command_topic, self._payload_arm_home, self._qos)
def alarm_arm_away(self, code=None): @asyncio.coroutine
"""Send arm away command.""" def async_alarm_arm_away(self, code=None):
"""Send arm away command.
This method is a coroutine.
"""
if not self._validate_code(code, 'arming away'): if not self._validate_code(code, 'arming away'):
return return
mqtt.publish(self.hass, self._command_topic, mqtt.async_publish(
self._payload_arm_away, self._qos) self.hass, self._command_topic, self._payload_arm_away, self._qos)
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """Validate given code."""

View File

@ -12,16 +12,19 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN, CONF_CODE, CONF_NAME,
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY,
EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/' REQUIREMENTS = ['simplisafe-python==1.0.2']
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
'simplisafe-python==0.0.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'SimpliSafe' DEFAULT_NAME = 'SimpliSafe'
DOMAIN = 'simplisafe'
NOTIFICATION_ID = 'simplisafe_notification'
NOTIFICATION_TITLE = 'SimpliSafe Setup'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
@ -33,33 +36,44 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the SimpliSafe platform.""" """Set up the SimpliSafe platform."""
from simplipy.api import SimpliSafeApiInterface, get_systems
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
code = config.get(CONF_CODE) code = config.get(CONF_CODE)
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
add_devices([SimpliSafeAlarm(name, username, password, code)]) persistent_notification = loader.get_component('persistent_notification')
simplisafe = SimpliSafeApiInterface()
status = simplisafe.set_credentials(username, password)
if status:
hass.data[DOMAIN] = simplisafe
locations = get_systems(simplisafe)
for location in locations:
add_devices([SimpliSafeAlarm(location, name, code)])
else:
message = 'Failed to log into SimpliSafe. Check credentials.'
_LOGGER.error(message)
persistent_notification.create(
hass, message,
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
def logout(event):
"""Logout of the SimpliSafe API."""
hass.data[DOMAIN].logout()
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout)
class SimpliSafeAlarm(alarm.AlarmControlPanel): class SimpliSafeAlarm(alarm.AlarmControlPanel):
"""Representation a SimpliSafe alarm.""" """Representation a SimpliSafe alarm."""
def __init__(self, name, username, password, code): def __init__(self, simplisafe, name, code):
"""Initialize the SimpliSafe alarm.""" """Initialize the SimpliSafe alarm."""
from simplisafe import SimpliSafe self.simplisafe = simplisafe
self.simplisafe = SimpliSafe(username, password)
self._name = name self._name = name
self._code = str(code) if code else None self._code = str(code) if code else None
self._id = self.simplisafe.get_id()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
@property @property
def name(self): def name(self):
@ -67,7 +81,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
if self._name is not None: if self._name is not None:
return self._name return self._name
else: else:
return 'Alarm {}'.format(self._id) return 'Alarm {}'.format(self.simplisafe.location_id())
@property @property
def code_format(self): def code_format(self):
@ -77,21 +91,32 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
return self._state status = self.simplisafe.state()
if status == 'Off':
state = STATE_ALARM_DISARMED
elif status == 'Home':
state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
state = STATE_ALARM_ARMED_AWAY
else:
state = STATE_UNKNOWN
return state
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'temperature': self.simplisafe.temperature(),
'co': self.simplisafe.carbon_monoxide(),
'fire': self.simplisafe.fire(),
'alarm': self.simplisafe.alarm(),
'last_event': self.simplisafe.last_event(),
'flood': self.simplisafe.flood()
}
def update(self): def update(self):
"""Update alarm status.""" """Update alarm status."""
self.simplisafe.get_location() self.simplisafe.update()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -13,10 +13,9 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, from homeassistant.const import (
CONF_STATE, STATE_ON, STATE_OFF, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event from homeassistant.helpers import service, event
from homeassistant.util.async import run_callback_threadsafe from homeassistant.util.async import run_callback_threadsafe
@ -32,13 +31,16 @@ CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat' CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first' CONF_SKIP_FIRST = 'skip_first'
DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string, vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]), vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=True): cv.boolean, vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=False): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Required(CONF_NOTIFIERS): cv.ensure_list}) vol.Required(CONF_NOTIFIERS): cv.ensure_list})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -60,7 +62,8 @@ def is_on(hass, entity_id):
def turn_on(hass, entity_id): def turn_on(hass, entity_id):
"""Reset the alert.""" """Reset the alert."""
run_callback_threadsafe(hass.loop, async_turn_on, hass, entity_id) run_callback_threadsafe(
hass.loop, async_turn_on, hass, entity_id).result()
@callback @callback
@ -73,7 +76,8 @@ def async_turn_on(hass, entity_id):
def turn_off(hass, entity_id): def turn_off(hass, entity_id):
"""Acknowledge alert.""" """Acknowledge alert."""
run_callback_threadsafe(hass.loop, async_turn_off, hass, entity_id) run_callback_threadsafe(
hass.loop, async_turn_off, hass, entity_id).result()
@callback @callback
@ -99,7 +103,7 @@ def async_toggle(hass, entity_id):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Setup alert component.""" """Set up the Alert component."""
alerts = config.get(DOMAIN) alerts = config.get(DOMAIN)
all_alerts = {} all_alerts = {}
@ -117,7 +121,7 @@ def async_setup(hass, config):
else: else:
yield from alert.async_turn_off() yield from alert.async_turn_off()
# setup alerts # Setup alerts
for entity_id, alert in alerts.items(): for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id, entity = Alert(hass, entity_id,
alert[CONF_NAME], alert[CONF_ENTITY_ID], alert[CONF_NAME], alert[CONF_ENTITY_ID],
@ -126,13 +130,13 @@ def async_setup(hass, config):
alert[CONF_CAN_ACK]) alert[CONF_CAN_ACK])
all_alerts[entity.entity_id] = entity all_alerts[entity.entity_id] = entity
# read descriptions # Read descriptions
descriptions = yield from hass.loop.run_in_executor( descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join( None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')) os.path.dirname(__file__), 'services.yaml'))
descriptions = descriptions.get(DOMAIN, {}) descriptions = descriptions.get(DOMAIN, {})
# setup service calls # Setup service calls
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service, DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA) descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
@ -171,8 +175,8 @@ class Alert(ToggleEntity):
self._cancel = None self._cancel = None
self.entity_id = ENTITY_ID_FORMAT.format(entity_id) self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change(hass, watched_entity_id, event.async_track_state_change(
self.watched_entity_change) hass, watched_entity_id, self.watched_entity_change)
@property @property
def name(self): def name(self):
@ -201,7 +205,7 @@ class Alert(ToggleEntity):
@asyncio.coroutine @asyncio.coroutine
def watched_entity_change(self, entity, from_state, to_state): def watched_entity_change(self, entity, from_state, to_state):
"""Determine if the alert should start or stop.""" """Determine if the alert should start or stop."""
_LOGGER.debug('Watched entity (%s) has changed.', entity) _LOGGER.debug("Watched entity (%s) has changed", entity)
if to_state.state == self._alert_state and not self._firing: if to_state.state == self._alert_state and not self._firing:
yield from self.begin_alerting() yield from self.begin_alerting()
if to_state.state != self._alert_state and self._firing: if to_state.state != self._alert_state and self._firing:
@ -210,7 +214,7 @@ class Alert(ToggleEntity):
@asyncio.coroutine @asyncio.coroutine
def begin_alerting(self): def begin_alerting(self):
"""Begin the alert procedures.""" """Begin the alert procedures."""
_LOGGER.debug('Beginning Alert: %s', self._name) _LOGGER.debug("Beginning Alert: %s", self._name)
self._ack = False self._ack = False
self._firing = True self._firing = True
self._next_delay = 0 self._next_delay = 0
@ -225,7 +229,7 @@ class Alert(ToggleEntity):
@asyncio.coroutine @asyncio.coroutine
def end_alerting(self): def end_alerting(self):
"""End the alert procedures.""" """End the alert procedures."""
_LOGGER.debug('Ending Alert: %s', self._name) _LOGGER.debug("Ending Alert: %s", self._name)
self._cancel() self._cancel()
self._ack = False self._ack = False
self._firing = False self._firing = False
@ -247,7 +251,7 @@ class Alert(ToggleEntity):
return return
if not self._ack: if not self._ack:
_LOGGER.info('Alerting: %s', self._name) _LOGGER.info("Alerting: %s", self._name)
for target in self._notifiers: for target in self._notifiers:
yield from self.hass.services.async_call( yield from self.hass.services.async_call(
'notify', target, {'message': self._name}) 'notify', target, {'message': self._name})
@ -256,14 +260,14 @@ class Alert(ToggleEntity):
@asyncio.coroutine @asyncio.coroutine
def async_turn_on(self): def async_turn_on(self):
"""Async Unacknowledge alert.""" """Async Unacknowledge alert."""
_LOGGER.debug('Reset Alert: %s', self._name) _LOGGER.debug("Reset Alert: %s", self._name)
self._ack = False self._ack = False
yield from self.async_update_ha_state() yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self): def async_turn_off(self):
"""Async Acknowledge alert.""" """Async Acknowledge alert."""
_LOGGER.debug('Acknowledged Alert: %s', self._name) _LOGGER.debug("Acknowledged Alert: %s", self._name)
self._ack = True self._ack = True
yield from self.async_update_ha_state() yield from self.async_update_ha_state()

View File

@ -77,14 +77,14 @@ class ApiaiIntentsView(HomeAssistantView):
"""Handle API.AI.""" """Handle API.AI."""
data = yield from request.json() data = yield from request.json()
_LOGGER.debug('Received Apiai request: %s', data) _LOGGER.debug("Received api.ai request: %s", data)
req = data.get('result') req = data.get('result')
if req is None: if req is None:
_LOGGER.error('Received invalid data from Apiai: %s', data) _LOGGER.error("Received invalid data from api.ai: %s", data)
return self.json_message('Expected result value not received', return self.json_message(
HTTP_BAD_REQUEST) "Expected result value not received", HTTP_BAD_REQUEST)
action_incomplete = req['actionIncomplete'] action_incomplete = req['actionIncomplete']
@ -106,7 +106,7 @@ class ApiaiIntentsView(HomeAssistantView):
# return self.json(response) # return self.json(response)
if intent == "": if intent == "":
_LOGGER.warning('Received intent with empty action') _LOGGER.warning("Received intent with empty action")
response.add_speech( response.add_speech(
"You have not defined an action in your api.ai intent.") "You have not defined an action in your api.ai intent.")
return self.json(response) return self.json(response)
@ -114,7 +114,7 @@ class ApiaiIntentsView(HomeAssistantView):
config = self.intents.get(intent) config = self.intents.get(intent)
if config is None: if config is None:
_LOGGER.warning('Received unknown intent %s', intent) _LOGGER.warning("Received unknown intent %s", intent)
response.add_speech( response.add_speech(
"Intent '%s' is not yet configured within Home Assistant." % "Intent '%s' is not yet configured within Home Assistant." %
intent) intent)

View File

@ -412,7 +412,7 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
if platform is None: if platform is None:
return None return None
remove = platform.async_trigger(hass, conf, action) remove = yield from platform.async_trigger(hass, conf, action)
if not remove: if not remove:
_LOGGER.error("Error setting up trigger %s", name) _LOGGER.error("Error setting up trigger %s", name)

View File

@ -4,6 +4,7 @@ Offer event listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#event-trigger at https://home-assistant.io/components/automation/#event-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE) event_type = config.get(CONF_EVENT_TYPE)

View File

@ -4,6 +4,7 @@ Trigger an automation when a LiteJet switch is released.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/automation.litejet/ https://home-assistant.io/components/automation.litejet/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -32,6 +33,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
number = config.get(CONF_NUMBER) number = config.get(CONF_NUMBER)

View File

@ -4,6 +4,7 @@ Offer MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger at https://home-assistant.io/components/automation/#mqtt-trigger
""" """
import asyncio
import json import json
import voluptuous as vol import voluptuous as vol
@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC) topic = config.get(CONF_TOPIC)
@ -49,4 +51,6 @@ def async_trigger(hass, config, action):
'trigger': data 'trigger': data
}) })
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener) remove = yield from mqtt.async_subscribe(
hass, topic, mqtt_automation_listener)
return remove

View File

@ -4,6 +4,7 @@ Offer numeric state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger at https://home-assistant.io/components/automation/#numeric-state-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)

View File

@ -4,6 +4,7 @@ Offer state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger at https://home-assistant.io/components/automation/#state-trigger
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
@ -34,6 +35,7 @@ TRIGGER_SCHEMA = vol.All(
) )
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
@ -43,6 +45,19 @@ def async_trigger(hass, config, action):
async_remove_state_for_cancel = None async_remove_state_for_cancel = None
async_remove_state_for_listener = None async_remove_state_for_listener = None
@callback
def clear_listener():
"""Clear all unsub listener."""
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
# pylint: disable=not-callable
if async_remove_state_for_listener is not None:
async_remove_state_for_listener()
async_remove_state_for_listener = None
if async_remove_state_for_cancel is not None:
async_remove_state_for_cancel()
async_remove_state_for_cancel = None
@callback @callback
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
@ -64,18 +79,11 @@ def async_trigger(hass, config, action):
call_action() call_action()
return return
@callback
def clear_listener():
"""Clear all unsub listener."""
nonlocal async_remove_state_for_cancel
nonlocal async_remove_state_for_listener
async_remove_state_for_listener = None
async_remove_state_for_cancel = None
@callback @callback
def state_for_listener(now): def state_for_listener(now):
"""Fire on state changes after a delay and calls action.""" """Fire on state changes after a delay and calls action."""
async_remove_state_for_cancel() nonlocal async_remove_state_for_listener
async_remove_state_for_listener = None
clear_listener() clear_listener()
call_action() call_action()
@ -84,8 +92,9 @@ def async_trigger(hass, config, action):
"""Fire on changes and cancel for listener if changed.""" """Fire on changes and cancel for listener if changed."""
if inner_to_s.state == to_s.state: if inner_to_s.state == to_s.state:
return return
async_remove_state_for_listener() clear_listener()
async_remove_state_for_cancel()
# cleanup previous listener
clear_listener() clear_listener()
async_remove_state_for_listener = async_track_point_in_utc_time( async_remove_state_for_listener = async_track_point_in_utc_time(
@ -97,14 +106,10 @@ def async_trigger(hass, config, action):
unsub = async_track_state_change( unsub = async_track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state) hass, entity_id, state_automation_listener, from_state, to_state)
@callback
def async_remove(): def async_remove():
"""Remove state listeners async.""" """Remove state listeners async."""
unsub() unsub()
# pylint: disable=not-callable clear_listener()
if async_remove_state_for_cancel is not None:
async_remove_state_for_cancel()
if async_remove_state_for_listener is not None:
async_remove_state_for_listener()
return async_remove return async_remove

View File

@ -4,6 +4,7 @@ Offer sun based automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#sun-trigger at https://home-assistant.io/components/automation/#sun-trigger
""" """
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event = config.get(CONF_EVENT) event = config.get(CONF_EVENT)

View File

@ -4,14 +4,14 @@ Offer template automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#template-trigger at https://home-assistant.io/components/automation/#template-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
from homeassistant.helpers import condition from homeassistant.helpers.event import async_track_template
from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -23,23 +23,15 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
value_template.hass = hass value_template.hass = hass
# Local variable to keep track of if the action has already been triggered
already_triggered = False
@callback @callback
def state_changed_listener(entity_id, from_s, to_s): def template_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal already_triggered
template_result = condition.async_template(hass, value_template)
# Check to see if template returns true
if template_result and not already_triggered:
already_triggered = True
hass.async_run_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'template', 'platform': 'template',
@ -48,8 +40,5 @@ def async_trigger(hass, config, action):
'to_state': to_s, 'to_state': to_s,
}, },
}) })
elif not template_result:
already_triggered = False
return async_track_state_change(hass, value_template.extract_entities(), return async_track_template(hass, value_template, template_listener)
state_changed_listener)

View File

@ -4,6 +4,7 @@ Offer time listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#time-trigger at https://home-assistant.io/components/automation/#time-trigger
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -29,6 +30,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
CONF_SECONDS, CONF_AFTER)) CONF_SECONDS, CONF_AFTER))
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
if CONF_AFTER in config: if CONF_AFTER in config:

View File

@ -4,6 +4,7 @@ Offer zone automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#zone-trigger at https://home-assistant.io/components/automation/#zone-trigger
""" """
import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
}) })
@asyncio.coroutine
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)

View File

@ -14,13 +14,13 @@ from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.deprecation import deprecated_substitute
DOMAIN = 'binary_sensor' DOMAIN = 'binary_sensor'
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
SENSOR_CLASSES = [ DEVICE_CLASSES = [
None, # Generic on/off
'cold', # On means cold (or too cold) 'cold', # On means cold (or too cold)
'connectivity', # On means connection present, Off = no connection 'connectivity', # On means connection present, Off = no connection
'gas', # CO, CO2, etc. 'gas', # CO, CO2, etc.
@ -38,7 +38,7 @@ SENSOR_CLASSES = [
'vibration', # On means vibration detected, Off means no vibration 'vibration', # On means vibration detected, Off means no vibration
] ]
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES)) DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@asyncio.coroutine @asyncio.coroutine
@ -66,16 +66,7 @@ class BinarySensorDevice(Entity):
return STATE_ON if self.is_on else STATE_OFF return STATE_ON if self.is_on else STATE_OFF
@property @property
def sensor_class(self): @deprecated_substitute('sensor_class')
"""Return the class of this sensor, from SENSOR_CLASSES.""" def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return None return None
@property
def state_attributes(self):
"""Return device specific state attributes."""
attr = {}
if self.sensor_class is not None:
attr['sensor_class'] = self.sensor_class
return attr

View File

@ -11,11 +11,12 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS) CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,7 +26,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url, vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_PIN): cv.string, vol.Required(CONF_PIN): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
@ -33,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the aREST binary sensor.""" """Set up the aREST binary sensor."""
resource = config.get(CONF_RESOURCE) resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN) pin = config.get(CONF_PIN)
sensor_class = config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
try: try:
response = requests.get(resource, timeout=10).json() response = requests.get(resource, timeout=10).json()
@ -49,18 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ArestBinarySensor( add_devices([ArestBinarySensor(
arest, resource, config.get(CONF_NAME, response[CONF_NAME]), arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
sensor_class, pin)]) device_class, pin)])
class ArestBinarySensor(BinarySensorDevice): class ArestBinarySensor(BinarySensorDevice):
"""Implement an aREST binary sensor for a pin.""" """Implement an aREST binary sensor for a pin."""
def __init__(self, arest, resource, name, sensor_class, pin): def __init__(self, arest, resource, name, device_class, pin):
"""Initialize the aREST device.""" """Initialize the aREST device."""
self.arest = arest self.arest = arest
self._resource = resource self._resource = resource
self._name = name self._name = name
self._sensor_class = sensor_class self._device_class = device_class
self._pin = pin self._pin = pin
self.update() self.update()
@ -81,9 +83,9 @@ class ArestBinarySensor(BinarySensorDevice):
return bool(self.arest.data.get('state')) return bool(self.arest.data.get('state'))
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._sensor_class return self._device_class
def update(self): def update(self):
"""Get the latest data from aREST API.""" """Get the latest data from aREST API."""

View File

@ -0,0 +1,148 @@
"""
Support for aurora forecast data sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.aurora/
"""
from datetime import timedelta
import logging
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor \
import (BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
CONF_THRESHOLD = "forecast_threshold"
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Aurora Visibility'
DEFAULT_DEVICE_CLASS = "visible"
DEFAULT_THRESHOLD = 75
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the aurora sensor."""
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Lat. or long. not set in Home Assistant config")
return False
name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD)
try:
aurora_data = AuroraData(
hass.config.latitude,
hass.config.longitude,
threshold
)
aurora_data.update()
except requests.exceptions.HTTPError as error:
_LOGGER.error(
"Connection to aurora forecast service failed: %s", error)
return False
add_devices([AuroraSensor(aurora_data, name)], True)
class AuroraSensor(BinarySensorDevice):
"""Implementation of an aurora sensor."""
def __init__(self, aurora_data, name):
"""Initialize the sensor."""
self.aurora_data = aurora_data
self._name = name
@property
def name(self):
"""Return the name of the sensor."""
return '{}'.format(self._name)
@property
def is_on(self):
"""Return true if aurora is visible."""
return self.aurora_data.is_visible if self.aurora_data else False
@property
def device_class(self):
"""Return the class of this device."""
return DEFAULT_DEVICE_CLASS
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
if self.aurora_data:
attrs["visibility_level"] = self.aurora_data.visibility_level
attrs["message"] = self.aurora_data.is_visible_text
return attrs
def update(self):
"""Get the latest data from Aurora API and updates the states."""
self.aurora_data.update()
class AuroraData(object):
"""Get aurora forecast."""
def __init__(self, latitude, longitude, threshold):
"""Initialize the data object."""
self.latitude = latitude
self.longitude = longitude
self.number_of_latitude_intervals = 513
self.number_of_longitude_intervals = 1024
self.api_url = \
"http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
self.headers = {"User-Agent": "Home Assistant Aurora Tracker v.0.1.0"}
self.threshold = int(threshold)
self.is_visible = None
self.is_visible_text = None
self.visibility_level = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the Aurora service."""
try:
self.visibility_level = self.get_aurora_forecast()
if int(self.visibility_level) > self.threshold:
self.is_visible = True
self.is_visible_text = "visible!"
else:
self.is_visible = False
self.is_visible_text = "nothing's out"
except requests.exceptions.HTTPError as error:
_LOGGER.error(
"Connection to aurora forecast service failed: %s", error)
return False
def get_aurora_forecast(self):
"""Get forecast data and parse for given long/lat."""
raw_data = requests.get(self.api_url, headers=self.headers).text
forecast_table = [
row.strip(" ").split(" ")
for row in raw_data.split("\n")
if not row.startswith("#")
]
# convert lat and long for data points in table
converted_latitude = round((self.latitude / 180)
* self.number_of_latitude_intervals)
converted_longitude = round((self.longitude / 360)
* self.number_of_longitude_intervals)
return forecast_table[converted_latitude][converted_longitude]

View File

@ -64,8 +64,8 @@ class BloomSkySensor(BinarySensorDevice):
return self._unique_id return self._unique_id
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return SENSOR_TYPES.get(self._sensor_name) return SENSOR_TYPES.get(self._sensor_name)
@property @property

View File

@ -10,12 +10,13 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA) BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
from homeassistant.components.sensor.command_line import CommandSensorData from homeassistant.components.sensor.command_line import CommandSensorData
from homeassistant.const import ( from homeassistant.const import (
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
CONF_SENSOR_CLASS, CONF_COMMAND) CONF_SENSOR_CLASS, CONF_COMMAND, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,7 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}) })
@ -42,27 +44,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
command = config.get(CONF_COMMAND) command = config.get(CONF_COMMAND)
payload_off = config.get(CONF_PAYLOAD_OFF) payload_off = config.get(CONF_PAYLOAD_OFF)
payload_on = config.get(CONF_PAYLOAD_ON) payload_on = config.get(CONF_PAYLOAD_ON)
sensor_class = config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
data = CommandSensorData(command) data = CommandSensorData(command)
add_devices([CommandBinarySensor( add_devices([CommandBinarySensor(
hass, data, name, sensor_class, payload_on, payload_off, hass, data, name, device_class, payload_on, payload_off,
value_template)]) value_template)])
class CommandBinarySensor(BinarySensorDevice): class CommandBinarySensor(BinarySensorDevice):
"""Represent a command line binary sensor.""" """Represent a command line binary sensor."""
def __init__(self, hass, data, name, sensor_class, payload_on, def __init__(self, hass, data, name, device_class, payload_on,
payload_off, value_template): payload_off, value_template):
"""Initialize the Command line binary sensor.""" """Initialize the Command line binary sensor."""
self._hass = hass self._hass = hass
self.data = data self.data = data
self._name = name self._name = name
self._sensor_class = sensor_class self._device_class = device_class
self._state = False self._state = False
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
@ -80,9 +82,9 @@ class CommandBinarySensor(BinarySensorDevice):
return self._state return self._state
@ property @ property
def sensor_class(self): def device_class(self):
"""Return the class of the binary sensor.""" """Return the class of the binary sensor."""
return self._sensor_class return self._device_class
def update(self): def update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""

View File

@ -11,7 +11,7 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES) BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
from homeassistant.const import (CONF_HOST, CONF_PORT) from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -30,7 +30,7 @@ DEFAULT_SSL = False
SCAN_INTERVAL = datetime.timedelta(seconds=1) SCAN_INTERVAL = datetime.timedelta(seconds=1)
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES), cv.positive_int: vol.In(DEVICE_CLASSES),
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -102,8 +102,8 @@ class Concord232ZoneSensor(BinarySensorDevice):
self.update() self.update()
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type return self._zone_type
@property @property

View File

@ -18,14 +18,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoBinarySensor(BinarySensorDevice): class DemoBinarySensor(BinarySensorDevice):
"""A Demo binary sensor.""" """A Demo binary sensor."""
def __init__(self, name, state, sensor_class): def __init__(self, name, state, device_class):
"""Initialize the demo sensor.""" """Initialize the demo sensor."""
self._name = name self._name = name
self._state = state self._state = state
self._sensor_type = sensor_class self._sensor_type = device_class
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._sensor_type return self._sensor_type

View File

@ -63,7 +63,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
return self.data.status == 'active' return self.data.status == 'active'
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return DEFAULT_SENSOR_CLASS return DEFAULT_SENSOR_CLASS

View File

@ -38,7 +38,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
self.sensor_name = sensor_name self.sensor_name = sensor_name
self.index = sensor_index self.index = sensor_index
self._state = None self._state = None
self._sensor_class = 'occupancy' self._device_class = 'occupancy'
self.update() self.update()
@property @property
@ -57,9 +57,9 @@ class EcobeeBinarySensor(BinarySensorDevice):
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index) return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._sensor_class return self._device_class
def update(self): def update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""

View File

@ -9,10 +9,12 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.components import enocean from homeassistant.components import enocean
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SENSOR_CLASS) from homeassistant.const import (
CONF_NAME, CONF_ID, CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,7 +24,8 @@ DEFAULT_NAME = 'EnOcean binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
@ -30,15 +33,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Binary Sensor platform fo EnOcean.""" """Setup the Binary Sensor platform fo EnOcean."""
dev_id = config.get(CONF_ID) dev_id = config.get(CONF_ID)
devname = config.get(CONF_NAME) devname = config.get(CONF_NAME)
sensor_class = config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
add_devices([EnOceanBinarySensor(dev_id, devname, sensor_class)]) add_devices([EnOceanBinarySensor(dev_id, devname, device_class)])
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
"""Representation of EnOcean binary sensors such as wall switches.""" """Representation of EnOcean binary sensors such as wall switches."""
def __init__(self, dev_id, devname, sensor_class): def __init__(self, dev_id, devname, device_class):
"""Initialize the EnOcean binary sensor.""" """Initialize the EnOcean binary sensor."""
enocean.EnOceanDevice.__init__(self) enocean.EnOceanDevice.__init__(self)
self.stype = "listener" self.stype = "listener"
@ -46,7 +49,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
self.which = -1 self.which = -1
self.onoff = -1 self.onoff = -1
self.devname = devname self.devname = devname
self._sensor_class = sensor_class self._device_class = device_class
@property @property
def name(self): def name(self):
@ -54,9 +57,9 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
return self.devname return self.devname
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._sensor_class return self._device_class
def value_changed(self, value, value2): def value_changed(self, value, value2):
"""Fire an event with the data that have changed. """Fire an event with the data that have changed.

View File

@ -4,13 +4,14 @@ Support for Envisalink zone states- represented as binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.envisalink/ https://home-assistant.io/components/binary_sensor.envisalink/
""" """
import asyncio
import logging import logging
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.envisalink import (EVL_CONTROLLER, from homeassistant.components.envisalink import (
ZONE_SCHEMA, DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
CONF_ZONENAME,
CONF_ZONETYPE,
EnvisalinkDevice,
SIGNAL_ZONE_UPDATE) SIGNAL_ZONE_UPDATE)
from homeassistant.const import ATTR_LAST_TRIP_TIME from homeassistant.const import ATTR_LAST_TRIP_TIME
@ -18,34 +19,41 @@ DEPENDENCIES = ['envisalink']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup Envisalink binary sensor devices.""" """Setup Envisalink binary sensor devices."""
_configured_zones = discovery_info['zones'] configured_zones = discovery_info['zones']
for zone_num in _configured_zones:
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num]) devices = []
_device = EnvisalinkBinarySensor( for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
device = EnvisalinkBinarySensor(
hass,
zone_num, zone_num,
_device_config_data[CONF_ZONENAME], device_config_data[CONF_ZONENAME],
_device_config_data[CONF_ZONETYPE], device_config_data[CONF_ZONETYPE],
EVL_CONTROLLER.alarm_state['zone'][zone_num], hass.data[DATA_EVL].alarm_state['zone'][zone_num],
EVL_CONTROLLER) hass.data[DATA_EVL]
add_devices_callback([_device]) )
devices.append(device)
yield from async_add_devices(devices)
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor.""" """Representation of an Envisalink binary sensor."""
def __init__(self, zone_number, zone_name, zone_type, info, controller): def __init__(self, hass, zone_number, zone_name, zone_type, info,
controller):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
from pydispatch import dispatcher
self._zone_type = zone_type self._zone_type = zone_type
self._zone_number = zone_number self._zone_number = zone_number
_LOGGER.debug('Setting up zone: ' + zone_name) _LOGGER.debug('Setting up zone: ' + zone_name)
EnvisalinkDevice.__init__(self, zone_name, info, controller) super().__init__(zone_name, info, controller)
dispatcher.connect(self._update_callback,
signal=SIGNAL_ZONE_UPDATE, async_dispatcher_connect(
sender=dispatcher.Any) hass, SIGNAL_ZONE_UPDATE, self._update_callback)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -60,11 +68,12 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
return self._info['status']['open'] return self._info['status']['open']
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type return self._zone_type
@callback
def _update_callback(self, zone): def _update_callback(self, zone):
"""Update the zone's state, if needed.""" """Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number: if zone is None or int(zone) == self._zone_number:
self.hass.async_add_job(self.update_ha_state) self.hass.async_add_job(self.async_update_ha_state())

View File

@ -122,6 +122,6 @@ class FFmpegMotion(FFmpegBinarySensor):
) )
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return "motion" return "motion"

View File

@ -91,6 +91,6 @@ class FFmpegNoise(FFmpegBinarySensor):
) )
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return "sound" return "sound"

View File

@ -179,12 +179,9 @@ class FlicButton(BinarySensorDevice):
return False return False
@property @property
def state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
attr = super(FlicButton, self).state_attributes return {"address": self.address}
attr["address"] = self.address
return attr
def _queued_event_check(self, click_type, time_diff): def _queued_event_check(self, click_type, time_diff):
"""Generate a log message and returns true if timeout exceeded.""" """Generate a log message and returns true if timeout exceeded."""

View File

@ -29,7 +29,7 @@ DEFAULT_DELAY = 0
ATTR_DELAY = 'delay' ATTR_DELAY = 'delay'
SENSOR_CLASS_MAP = { DEVICE_CLASS_MAP = {
'Motion': 'motion', 'Motion': 'motion',
'Line Crossing': 'motion', 'Line Crossing': 'motion',
'IO Trigger': None, 'IO Trigger': None,
@ -201,10 +201,10 @@ class HikvisionBinarySensor(BinarySensorDevice):
return self._sensor_state() return self._sensor_state()
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
try: try:
return SENSOR_CLASS_MAP[self._sensor] return DEVICE_CLASS_MAP[self._sensor]
except KeyError: except KeyError:
# Sensor must be unknown to us, add as generic # Sensor must be unknown to us, add as generic
return None return None

View File

@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.homematic/
import logging import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.homematic import HMDevice from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,18 +28,18 @@ SENSOR_TYPES_CLASS = {
} }
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Homematic binary sensor platform.""" """Setup the Homematic binary sensor platform."""
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic") devices = []
return homematic.setup_hmdevice_discovery_helper( for config in discovery_info[ATTR_DISCOVER_DEVICES]:
hass, new_device = HMBinarySensor(hass, config)
HMBinarySensor, new_device.link_homematic()
discovery_info, devices.append(new_device)
add_callback_devices
) add_devices(devices)
class HMBinarySensor(HMDevice, BinarySensorDevice): class HMBinarySensor(HMDevice, BinarySensorDevice):
@ -54,11 +53,8 @@ class HMBinarySensor(HMDevice, BinarySensorDevice):
return bool(self._hm_get_state()) return bool(self._hm_get_state())
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
if not self.available:
return None
# If state is MOTION (RemoteMotion works only) # If state is MOTION (RemoteMotion works only)
if self._state == "MOTION": if self._state == "MOTION":
return "motion" return "motion"

View File

@ -0,0 +1,87 @@
"""
Support for INSTEON dimmers via PowerLinc Modem.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon_plm/
"""
import logging
import asyncio
from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.loader import get_component
DEPENDENCIES = ['insteon_plm']
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the INSTEON PLM device class for the hass platform."""
plm = hass.data['insteon_plm']
device_list = []
for device in discovery_info:
name = device.get('address')
address = device.get('address_hex')
_LOGGER.info('Registered %s with binary_sensor platform.', name)
device_list.append(
InsteonPLMBinarySensorDevice(hass, plm, address, name)
)
hass.async_add_job(async_add_devices(device_list))
class InsteonPLMBinarySensorDevice(BinarySensorDevice):
"""A Class for an Insteon device."""
def __init__(self, hass, plm, address, name):
"""Initialize the binarysensor."""
self._hass = hass
self._plm = plm.protocol
self._address = address
self._name = name
self._plm.add_update_callback(
self.async_binarysensor_update, {'address': self._address})
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def address(self):
"""Return the the address of the node."""
return self._address
@property
def name(self):
"""Return the the name of the node."""
return self._name
@property
def is_on(self):
"""Return the boolean response if the node is on."""
sensorstate = self._plm.get_device_attr(self._address, 'sensorstate')
_LOGGER.info('sensor state for %s is %s', self._address, sensorstate)
return bool(sensorstate)
@property
def device_state_attributes(self):
"""Provide attributes for display on device card."""
insteon_plm = get_component('insteon_plm')
return insteon_plm.common_attributes(self)
def get_attr(self, key):
"""Return specified attribute for this device."""
return self._plm.get_device_attr(self.address, key)
@callback
def async_binarysensor_update(self, message):
"""Receive notification from transport that new data exists."""
_LOGGER.info('Received update calback from PLM for %s', self._address)
self._hass.async_add_job(self.async_update_ha_state())

View File

@ -26,7 +26,7 @@ ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
CONF_SHOW_ON_MAP = 'show_on_map' CONF_SHOW_ON_MAP = 'show_on_map'
DEFAULT_NAME = 'ISS' DEFAULT_NAME = 'ISS'
DEFAULT_SENSOR_CLASS = 'visible' DEFAULT_DEVICE_CLASS = 'visible'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
@ -77,9 +77,9 @@ class IssBinarySensor(BinarySensorDevice):
return self.iss_data.is_above if self.iss_data else False return self.iss_data.is_above if self.iss_data else False
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return DEFAULT_SENSOR_CLASS return DEFAULT_DEVICE_CLASS
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -4,6 +4,7 @@ Support for MQTT binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mqtt/ https://home-assistant.io/components/binary_sensor.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -11,12 +12,13 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES) BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
CONF_SENSOR_CLASS) CONF_SENSOR_CLASS, CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS) from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,13 +31,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)), vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
# pylint: disable=unused-argument @asyncio.coroutine
def setup_platform(hass, config, add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT binary sensor.""" """Set up the MQTT binary sensor."""
if discovery_info is not None: if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info) config = PLATFORM_SCHEMA(discovery_info)
@ -43,11 +45,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
add_devices([MqttBinarySensor(
hass, yield from async_add_devices([MqttBinarySensor(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_SENSOR_CLASS), get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS),
config.get(CONF_QOS), config.get(CONF_QOS),
config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF), config.get(CONF_PAYLOAD_OFF),
@ -58,32 +60,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttBinarySensor(BinarySensorDevice): class MqttBinarySensor(BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
def __init__(self, hass, name, state_topic, sensor_class, qos, payload_on, def __init__(self, name, state_topic, device_class, qos, payload_on,
payload_off, value_template): payload_off, value_template):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
self._hass = hass
self._name = name self._name = name
self._state = False self._state = False
self._state_topic = state_topic self._state_topic = state_topic
self._sensor_class = sensor_class self._device_class = device_class
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
self._qos = qos self._qos = qos
self._template = value_template
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method must be run in the event loop and returns a coroutine.
"""
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
if value_template is not None: if self._template is not None:
payload = value_template.async_render_with_possible_json_value( payload = self._template.async_render_with_possible_json_value(
payload) payload)
if payload == self._payload_on: if payload == self._payload_on:
self._state = True self._state = True
hass.async_add_job(self.async_update_ha_state())
elif payload == self._payload_off: elif payload == self._payload_off:
self._state = False self._state = False
hass.async_add_job(self.async_update_ha_state())
mqtt.subscribe(hass, self._state_topic, message_received, self._qos) self.hass.async_add_job(self.async_update_ha_state())
return mqtt.async_subscribe(
self.hass, self._state_topic, message_received, self._qos)
@property @property
def should_poll(self): def should_poll(self):
@ -101,6 +109,6 @@ class MqttBinarySensor(BinarySensorDevice):
return self._state return self._state
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._sensor_class return self._device_class

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.mysensors/
import logging import logging
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.binary_sensor import (SENSOR_CLASSES, from homeassistant.components.binary_sensor import (DEVICE_CLASSES,
BinarySensorDevice) BinarySensorDevice)
from homeassistant.const import STATE_ON from homeassistant.const import STATE_ON
@ -62,8 +62,8 @@ class MySensorsBinarySensor(
return False return False
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
pres = self.gateway.const.Presentation pres = self.gateway.const.Presentation
class_map = { class_map = {
pres.S_DOOR: 'opening', pres.S_DOOR: 'opening',
@ -78,5 +78,5 @@ class MySensorsBinarySensor(
pres.S_VIBRATION: 'vibration', pres.S_VIBRATION: 'vibration',
pres.S_MOISTURE: 'moisture', pres.S_MOISTURE: 'moisture',
}) })
if class_map.get(self.child_type) in SENSOR_CLASSES: if class_map.get(self.child_type) in DEVICE_CLASSES:
return class_map.get(self.child_type) return class_map.get(self.child_type)

View File

@ -154,8 +154,8 @@ class NetatmoBinarySensor(BinarySensorDevice):
return self._unique_id return self._unique_id
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
if self._cameratype == "NACamera": if self._cameratype == "NACamera":
return WELCOME_SENSOR_TYPES.get(self._sensor_name) return WELCOME_SENSOR_TYPES.get(self._sensor_name)
elif self._cameratype == "NOC": elif self._cameratype == "NOC":

View File

@ -12,7 +12,7 @@ import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA) DEVICE_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_PORT) from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -28,7 +28,7 @@ DEFAULT_PORT = '5007'
DEFAULT_SSL = False DEFAULT_SSL = False
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES), cv.positive_int: vol.In(DEVICE_CLASSES),
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -85,8 +85,8 @@ class NX584ZoneSensor(BinarySensorDevice):
self._zone_type = zone_type self._zone_type = zone_type
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type return self._zone_type
@property @property

View File

@ -99,8 +99,8 @@ class OctoPrintBinarySensor(BinarySensorDevice):
return STATE_OFF return STATE_OFF
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return None return None
def update(self): def update(self):

View File

@ -10,14 +10,15 @@ import voluptuous as vol
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA) BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
from homeassistant.components.sensor.rest import RestData from homeassistant.components.sensor.rest import RestData
from homeassistant.const import ( from homeassistant.const import (
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION) HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
@ -51,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS) headers = config.get(CONF_HEADERS)
sensor_class = config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -72,18 +74,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
add_devices([RestBinarySensor( add_devices([RestBinarySensor(
hass, rest, name, sensor_class, value_template)]) hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice): class RestBinarySensor(BinarySensorDevice):
"""Representation of a REST binary sensor.""" """Representation of a REST binary sensor."""
def __init__(self, hass, rest, name, sensor_class, value_template): def __init__(self, hass, rest, name, device_class, value_template):
"""Initialize a REST binary sensor.""" """Initialize a REST binary sensor."""
self._hass = hass self._hass = hass
self.rest = rest self.rest = rest
self._name = name self._name = name
self._sensor_class = sensor_class self._device_class = device_class
self._state = False self._state = False
self._previous_data = None self._previous_data = None
self._value_template = value_template self._value_template = value_template
@ -95,9 +97,9 @@ class RestBinarySensor(BinarySensorDevice):
return self._name return self._name
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._sensor_class return self._device_class
@property @property
def is_on(self): def is_on(self):

View File

@ -42,7 +42,7 @@ class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
return self._state is True return self._state is True
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return "occupancy" return "occupancy"

View File

@ -12,14 +12,15 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
SENSOR_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
CONF_SENSOR_CLASS, CONF_SENSORS) CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,7 +28,8 @@ SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -45,7 +47,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
entity_ids = (device_config.get(ATTR_ENTITY_ID) or entity_ids = (device_config.get(ATTR_ENTITY_ID) or
value_template.extract_entities()) value_template.extract_entities())
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
sensor_class = device_config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(
device_config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -55,7 +58,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hass, hass,
device, device,
friendly_name, friendly_name,
sensor_class, device_class,
value_template, value_template,
entity_ids) entity_ids)
) )
@ -70,14 +73,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class BinarySensorTemplate(BinarySensorDevice): class BinarySensorTemplate(BinarySensorDevice):
"""A virtual binary sensor that triggers from another sensor.""" """A virtual binary sensor that triggers from another sensor."""
def __init__(self, hass, device, friendly_name, sensor_class, def __init__(self, hass, device, friendly_name, device_class,
value_template, entity_ids): value_template, entity_ids):
"""Initialize the Template binary sensor.""" """Initialize the Template binary sensor."""
self.hass = hass self.hass = hass
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device, self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
hass=hass) hass=hass)
self._name = friendly_name self._name = friendly_name
self._sensor_class = sensor_class self._device_class = device_class
self._template = value_template self._template = value_template
self._state = None self._state = None
@ -100,9 +103,9 @@ class BinarySensorTemplate(BinarySensorDevice):
return self._state return self._state
@property @property
def sensor_class(self): def device_class(self):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._sensor_class return self._device_class
@property @property
def should_poll(self): def should_poll(self):

View File

@ -11,11 +11,12 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS, CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
ATTR_ENTITY_ID) ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_THRESHOLD): vol.Coerce(float), vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA, vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
@ -48,11 +50,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD) threshold = config.get(CONF_THRESHOLD)
limit_type = config.get(CONF_TYPE) limit_type = config.get(CONF_TYPE)
sensor_class = config.get(CONF_SENSOR_CLASS) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
yield from async_add_devices( yield from async_add_devices(
[ThresholdSensor(hass, entity_id, name, threshold, limit_type, [ThresholdSensor(hass, entity_id, name, threshold, limit_type,
sensor_class)], True) device_class)], True)
return True return True
@ -60,14 +62,14 @@ class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor.""" """Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold, limit_type, def __init__(self, hass, entity_id, name, threshold, limit_type,
sensor_class): device_class):
"""Initialize the Threshold sensor.""" """Initialize the Threshold sensor."""
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.is_upper = limit_type == 'upper' self.is_upper = limit_type == 'upper'
self._name = name self._name = name
self._threshold = threshold self._threshold = threshold
self._sensor_class = sensor_class self._device_class = device_class
self._deviation = False self._deviation = False
self.sensor_value = 0 self.sensor_value = 0
@ -105,9 +107,9 @@ class ThresholdSensor(BinarySensorDevice):
return False return False
@property @property
def sensor_class(self): def device_class(self):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._sensor_class return self._device_class
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, BinarySensorDevice,
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SENSOR_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_SENSOR_CLASS, CONF_SENSOR_CLASS,
CONF_DEVICE_CLASS,
STATE_UNKNOWN,) STATE_UNKNOWN,)
from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
@ -34,8 +36,8 @@ SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_ATTRIBUTE): cv.string,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_INVERT, default=False): cv.boolean, vol.Optional(CONF_INVERT, default=False): cv.boolean,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -52,7 +54,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
entity_id = device_config[ATTR_ENTITY_ID] entity_id = device_config[ATTR_ENTITY_ID]
attribute = device_config.get(CONF_ATTRIBUTE) attribute = device_config.get(CONF_ATTRIBUTE)
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
sensor_class = device_config[CONF_SENSOR_CLASS] device_class = get_deprecated(
device_config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
invert = device_config[CONF_INVERT] invert = device_config[CONF_INVERT]
sensors.append( sensors.append(
@ -62,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
friendly_name, friendly_name,
entity_id, entity_id,
attribute, attribute,
sensor_class, device_class,
invert) invert)
) )
if not sensors: if not sensors:
@ -76,7 +79,7 @@ class SensorTrend(BinarySensorDevice):
"""Representation of a trend Sensor.""" """Representation of a trend Sensor."""
def __init__(self, hass, device_id, friendly_name, def __init__(self, hass, device_id, friendly_name,
target_entity, attribute, sensor_class, invert): target_entity, attribute, device_class, invert):
"""Initialize the sensor.""" """Initialize the sensor."""
self._hass = hass self._hass = hass
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
@ -84,7 +87,7 @@ class SensorTrend(BinarySensorDevice):
self._name = friendly_name self._name = friendly_name
self._target_entity = target_entity self._target_entity = target_entity
self._attribute = attribute self._attribute = attribute
self._sensor_class = sensor_class self._device_class = device_class
self._invert = invert self._invert = invert
self._state = None self._state = None
self.from_state = None self.from_state = None
@ -111,9 +114,9 @@ class SensorTrend(BinarySensorDevice):
return self._state return self._state
@property @property
def sensor_class(self): def device_class(self):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._sensor_class return self._device_class
@property @property
def should_poll(self): def should_poll(self):

View File

@ -0,0 +1,40 @@
"""
Support for VOC.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.volvooncall/
"""
import logging
from homeassistant.components.volvooncall import VolvoEntity
from homeassistant.components.binary_sensor import BinarySensorDevice
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Volvo sensors."""
if discovery_info is None:
return
add_devices([VolvoSensor(hass, *discovery_info)])
class VolvoSensor(VolvoEntity, BinarySensorDevice):
"""Representation of a Volvo sensor."""
@property
def is_on(self):
"""Return True if the binary sensor is on."""
val = getattr(self.vehicle, self._attribute)
if self._attribute == 'bulb_failures':
return len(val) > 0
elif self._attribute in ['doors', 'windows']:
return any([val[key] for key in val if 'Open' in key])
else:
return val != 'Normal'
@property
def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return 'safety'

View File

@ -92,13 +92,13 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
def __init__(self, wink, hass): def __init__(self, wink, hass):
"""Initialize the Wink binary sensor.""" """Initialize the Wink binary sensor."""
super().__init__(wink, hass) super().__init__(wink, hass)
try: if hasattr(self.wink, 'unit'):
self._unit_of_measurement = self.wink.unit() self._unit_of_measurement = self.wink.unit()
except AttributeError: else:
self._unit_of_measurement = None self._unit_of_measurement = None
try: if hasattr(self.wink, 'capability'):
self.capability = self.wink.capability() self.capability = self.wink.capability()
except AttributeError: else:
self.capability = None self.capability = None
@property @property
@ -107,8 +107,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
return self.wink.state() return self.wink.state()
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return SENSOR_TYPES.get(self.capability) return SENSOR_TYPES.get(self.capability)
@ -161,8 +161,8 @@ class WinkRemote(WinkBinarySensorDevice):
} }
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return None return None

View File

@ -48,19 +48,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity): class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
"""Representation of a binary sensor within Z-Wave.""" """Representation of a binary sensor within Z-Wave."""
def __init__(self, value, sensor_class): def __init__(self, value, device_class):
"""Initialize the sensor.""" """Initialize the sensor."""
self._sensor_type = sensor_class
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._sensor_type = device_class
self._state = self._value.data
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
@property @property
def is_on(self): def is_on(self):
"""Return True if the binary sensor is on.""" """Return True if the binary sensor is on."""
return self._value.data return self._state
@property @property
def sensor_class(self): def device_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return self._sensor_type return self._sensor_type
@property @property
@ -72,24 +77,21 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
class ZWaveTriggerSensor(ZWaveBinarySensor): class ZWaveTriggerSensor(ZWaveBinarySensor):
"""Representation of a stateless sensor within Z-Wave.""" """Representation of a stateless sensor within Z-Wave."""
def __init__(self, value, sensor_class, hass, re_arm_sec=60): def __init__(self, value, device_class, hass, re_arm_sec=60):
"""Initialize the sensor.""" """Initialize the sensor."""
super(ZWaveTriggerSensor, self).__init__(value, sensor_class) super(ZWaveTriggerSensor, self).__init__(value, device_class)
self._hass = hass self._hass = hass
self.re_arm_sec = re_arm_sec self.re_arm_sec = re_arm_sec
self.invalidate_after = dt_util.utcnow() + datetime.timedelta( self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec) seconds=self.re_arm_sec)
# If it's active make sure that we set the timeout tracker # If it's active make sure that we set the timeout tracker
if value.data:
track_point_in_time( track_point_in_time(
self._hass, self.async_update_ha_state, self._hass, self.async_update_ha_state,
self.invalidate_after) self.invalidate_after)
def value_changed(self, value): def update_properties(self):
"""Called when a value for this entity's node has changed.""" """Called when a value for this entity's node has changed."""
if self._value.value_id == value.value_id: self._state = self._value.data
self.schedule_update_ha_state()
if value.data:
# only allow this value to be true for re_arm secs # only allow this value to be true for re_arm secs
self.invalidate_after = dt_util.utcnow() + datetime.timedelta( self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
seconds=self.re_arm_sec) seconds=self.re_arm_sec)
@ -100,6 +102,6 @@ class ZWaveTriggerSensor(ZWaveBinarySensor):
@property @property
def is_on(self): def is_on(self):
"""Return True if movement has happened within the rearm time.""" """Return True if movement has happened within the rearm time."""
return self._value.data and \ return self._state and \
(self.invalidate_after is None or (self.invalidate_after is None or
self.invalidate_after > dt_util.utcnow()) self.invalidate_after > dt_util.utcnow())

View File

@ -118,12 +118,13 @@ class GenericCamera(Camera):
_LOGGER.error('Timeout getting camera image') _LOGGER.error('Timeout getting camera image')
return self._last_image return self._last_image
except (aiohttp.errors.ClientError, except (aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError) as err: aiohttp.errors.DisconnectedError,
aiohttp.errors.HttpProcessingError) as err:
_LOGGER.error('Error getting new camera image: %s', err) _LOGGER.error('Error getting new camera image: %s', err)
return self._last_image return self._last_image
finally: finally:
if response is not None: if response is not None:
self.hass.async_add_job(response.release()) yield from response.release()
self._last_url = url self._last_url = url
return self._last_image return self._last_image

View File

@ -0,0 +1,78 @@
"""
Support for ZoneMinder camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.zoneminder/
"""
import asyncio
import logging
from urllib.parse import urljoin, urlencode
from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
import homeassistant.components.zoneminder as zoneminder
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder']
DOMAIN = 'zoneminder'
def _get_image_url(hass, monitor, mode):
zm_data = hass.data[DOMAIN]
query = urlencode({
'mode': mode,
'buffer': monitor['StreamReplayBuffer'],
'monitor': monitor['Id'],
})
url = '{zms_url}?{query}'.format(
zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']),
query=query,
)
_LOGGER.debug('Monitor %s %s URL (without auth): %s',
monitor['Id'], mode, url)
if not zm_data['username']:
return url
url += '&user={:s}'.format(zm_data['username'])
if not zm_data['password']:
return url
return url + '&pass={:s}'.format(zm_data['password'])
@asyncio.coroutine
# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup ZoneMinder cameras."""
cameras = []
monitors = zoneminder.get_state('api/monitors.json')
if not monitors:
_LOGGER.warning('Could not fetch monitors from ZoneMinder')
return
for i in monitors['monitors']:
monitor = i['Monitor']
if monitor['Function'] == 'None':
_LOGGER.info('Skipping camera %s', monitor['Id'])
continue
_LOGGER.info('Initializing camera %s', monitor['Id'])
device_info = {
CONF_NAME: monitor['Name'],
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
}
cameras.append(MjpegCamera(hass, device_info))
if not cameras:
_LOGGER.warning('No active cameras found')
return
yield from async_add_devices(cameras)

View File

@ -6,13 +6,14 @@ https://home-assistant.io/components/climate.homematic/
""" """
import logging import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.components.homematic import HMDevice from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.util.temperature import convert from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
from homeassistant.loader import get_component
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
_LOGGER = logging.getLogger(__name__)
STATE_MANUAL = "manual" STATE_MANUAL = "manual"
STATE_BOOST = "boost" STATE_BOOST = "boost"
@ -22,21 +23,31 @@ HM_STATE_MAP = {
"BOOST_MODE": STATE_BOOST, "BOOST_MODE": STATE_BOOST,
} }
_LOGGER = logging.getLogger(__name__) HM_TEMP_MAP = [
'ACTUAL_TEMPERATURE',
'TEMPERATURE',
]
HM_HUMI_MAP = [
'ACTUAL_HUMIDITY',
'HUMIDITY',
]
HM_CONTROL_MODE = 'CONTROL_MODE'
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Homematic thermostat platform.""" """Setup the Homematic thermostat platform."""
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic") devices = []
return homematic.setup_hmdevice_discovery_helper( for config in discovery_info[ATTR_DISCOVER_DEVICES]:
hass, new_device = HMThermostat(hass, config)
HMThermostat, new_device.link_homematic()
discovery_info, devices.append(new_device)
add_callback_devices
) add_devices(devices)
class HMThermostat(HMDevice, ClimateDevice): class HMThermostat(HMDevice, ClimateDevice):
@ -50,7 +61,7 @@ class HMThermostat(HMDevice, ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
if not self.available: if HM_CONTROL_MODE not in self._data:
return None return None
# read state and search # read state and search
@ -62,8 +73,6 @@ class HMThermostat(HMDevice, ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""List of available operation modes.""" """List of available operation modes."""
if not self.available:
return None
op_list = [] op_list = []
# generate list # generate list
@ -76,31 +85,29 @@ class HMThermostat(HMDevice, ClimateDevice):
@property @property
def current_humidity(self): def current_humidity(self):
"""Return the current humidity.""" """Return the current humidity."""
if not self.available: for node in HM_HUMI_MAP:
return None if node in self._data:
return self._data.get('ACTUAL_HUMIDITY', None) return self._data[node]
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
if not self.available: for node in HM_TEMP_MAP:
return None if node in self._data:
return self._data.get('ACTUAL_TEMPERATURE', None) return self._data[node]
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the target temperature.""" """Return the target temperature."""
if not self.available: return self._data.get(self._state)
return None
return self._data.get('SET_TEMPERATURE', None)
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if not self.available or temperature is None: if temperature is None:
return None return None
self._hmdevice.set_temperature(temperature) self._hmdevice.writeNodeData(self._state, float(temperature))
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new target operation mode.""" """Set new target operation mode."""
@ -122,10 +129,12 @@ class HMThermostat(HMDevice, ClimateDevice):
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from the Homematic metadata.""" """Generate a data dict (self._data) from the Homematic metadata."""
# Add state to data dict # Add state to data dict
self._data.update({"CONTROL_MODE": STATE_UNKNOWN, self._state = next(iter(self._hmdevice.WRITENODE.keys()))
"SET_TEMPERATURE": STATE_UNKNOWN, self._data[self._state] = STATE_UNKNOWN
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})
# support humidity # support state
if 'ACTUAL_HUMIDITY' in self._hmdevice.SENSORNODE: if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
self._data.update({'ACTUAL_HUMIDITY': STATE_UNKNOWN}) self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
for node in self._hmdevice.SENSORNODE.keys():
self._data[node] = STATE_UNKNOWN

View File

@ -0,0 +1,148 @@
"""
OpenEnergyMonitor Thermostat Support.
This provides a climate component for the ESP8266 based thermostat sold by
OpenEnergyMonitor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.oem/
"""
import logging
import requests
import voluptuous as vol
# Import the device class from the component that you want to support
from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE)
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PORT, TEMP_CELSIUS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
# Home Assistant depends on 3rd party packages for API specific code.
REQUIREMENTS = ['oemthermostat==1.1']
_LOGGER = logging.getLogger(__name__)
# Local configs
CONF_AWAY_TEMP = 'away_temp'
# Validation of the user's configuration
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default="Thermostat"): cv.string,
vol.Optional(CONF_PORT, default=80): cv.port,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float)
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup oemthermostat."""
from oemthermostat import Thermostat
# Assign configuration variables. The configuration check takes care they
# are present.
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
away_temp = config.get(CONF_AWAY_TEMP)
# If creating the class raises an exception, it failed to connect or
# something else went wrong.
try:
therm = Thermostat(host, port=port,
username=username, password=password)
except (ValueError, AssertionError, requests.RequestException):
return False
# Add devices
add_devices((ThermostatDevice(hass, therm, name, away_temp), ), True)
class ThermostatDevice(ClimateDevice):
"""Interface class for the oemthermostat module and HA."""
def __init__(self, hass, thermostat, name, away_temp):
"""Initialize the device."""
self._name = name
self.hass = hass
# Away mode stuff
self._away = False
self._away_temp = away_temp
self._prev_temp = thermostat.setpoint
self.thermostat = thermostat
# Set the thermostat mode to manual
self.thermostat.mode = 2
# set up internal state varS
self._state = None
self._temperature = None
self._setpoint = None
@property
def name(self):
"""Name of this Thermostat."""
return self._name
@property
def temperature_unit(self):
"""The unit of measurement used by the platform."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation i.e. heat, cool, idle."""
if self._state:
return STATE_HEAT
else:
return STATE_IDLE
@property
def current_temperature(self):
"""Return the current temperature."""
return self._temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._setpoint
def set_temperature(self, **kwargs):
"""Change the setpoint of the thermostat."""
# If we are setting the temp, then we don't want away mode anymore.
self.turn_away_mode_off()
temp = kwargs.get(ATTR_TEMPERATURE)
self.thermostat.setpoint = temp
@property
def is_away_mode_on(self):
"""Return true if away mode is on."""
return self._away
def turn_away_mode_on(self):
"""Turn away mode on."""
if not self._away:
self._prev_temp = self._setpoint
self.thermostat.setpoint = self._away_temp
self._away = True
def turn_away_mode_off(self):
"""Turn away mode off."""
if self._away:
self.thermostat.setpoint = self._prev_temp
self._away = False
def update(self):
"""Update local state."""
self._setpoint = self.thermostat.setpoint
self._temperature = self.thermostat.temperature
self._state = self.thermostat.state

View File

@ -68,7 +68,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._unit = temp_unit self._unit = temp_unit
_LOGGER.debug("temp_unit is %s", self._unit) _LOGGER.debug("temp_unit is %s", self._unit)
self._zxt_120 = None self._zxt_120 = None
self.update_properties()
# Make sure that we have values for the key before converting to int # Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()): value.node.product_id.strip()):
@ -79,6 +78,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat" _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
" workaround") " workaround")
self._zxt_120 = 1 self._zxt_120 = 1
self.update_properties()
def update_properties(self): def update_properties(self):
"""Callback on data changes for node values.""" """Callback on data changes for node values."""

View File

@ -0,0 +1,133 @@
"""Component to configure Home Assistant via an API."""
import asyncio
import os
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.bootstrap import (
async_prepare_setup_platform, ATTR_COMPONENT)
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView
from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'group', 'hassbian')
ON_DEMAND = ('zwave', )
@asyncio.coroutine
def async_setup(hass, config):
"""Setup the config component."""
register_built_in_panel(hass, 'config', 'Configuration', 'mdi:settings')
@asyncio.coroutine
def setup_panel(panel_name):
"""Setup a panel."""
panel = yield from async_prepare_setup_platform(hass, config, DOMAIN,
panel_name)
if not panel:
return
success = yield from panel.async_setup(hass)
if success:
key = '{}.{}'.format(DOMAIN, panel_name)
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
hass.config.components.add(key)
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
for panel_name in ON_DEMAND:
if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name))
if tasks:
yield from asyncio.wait(tasks, loop=hass.loop)
@callback
def component_loaded(event):
"""Respond to components being loaded."""
panel_name = event.data.get(ATTR_COMPONENT)
if panel_name in ON_DEMAND:
hass.async_add_job(setup_panel(panel_name))
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
return True
class EditKeyBasedConfigView(HomeAssistantView):
"""Configure a Group endpoint."""
def __init__(self, component, config_type, path, key_schema, data_schema,
*, post_write_hook=None):
"""Initialize a config view."""
self.url = '/api/config/%s/%s/{config_key}' % (component, config_type)
self.name = 'api:config:%s:%s' % (component, config_type)
self.path = path
self.key_schema = key_schema
self.data_schema = data_schema
self.post_write_hook = post_write_hook
@asyncio.coroutine
def get(self, request, config_key):
"""Fetch device specific config."""
hass = request.app['hass']
current = yield from hass.loop.run_in_executor(
None, _read, hass.config.path(self.path))
return self.json(current.get(config_key, {}))
@asyncio.coroutine
def post(self, request, config_key):
"""Validate config and return results."""
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON specified', 400)
try:
self.key_schema(config_key)
except vol.Invalid as err:
return self.json_message('Key malformed: {}'.format(err), 400)
try:
# We just validate, we don't store that data because
# we don't want to store the defaults.
self.data_schema(data)
except vol.Invalid as err:
return self.json_message('Message malformed: {}'.format(err), 400)
hass = request.app['hass']
path = hass.config.path(self.path)
current = yield from hass.loop.run_in_executor(None, _read, path)
current.setdefault(config_key, {}).update(data)
yield from hass.loop.run_in_executor(None, _write, path, current)
if self.post_write_hook is not None:
hass.async_add_job(self.post_write_hook(hass))
return self.json({
'result': 'ok',
})
def _read(path):
"""Read YAML helper."""
if not os.path.isfile(path):
with open(path, 'w'):
pass
return {}
return load_yaml(path)
def _write(path, data):
"""Write YAML helper."""
with open(path, 'w', encoding='utf-8') as outfile:
outfile.write(dump(data))

View File

@ -0,0 +1,31 @@
"""Component to interact with Hassbian tools."""
import asyncio
from homeassistant.components.http import HomeAssistantView
from homeassistant.config import async_check_ha_config_file
@asyncio.coroutine
def async_setup(hass):
"""Setup the hassbian config."""
hass.http.register_view(CheckConfigView)
return True
class CheckConfigView(HomeAssistantView):
"""Hassbian packages endpoint."""
url = '/api/config/core/check_config'
name = 'api:config:core:check_config'
@asyncio.coroutine
def post(self, request):
"""Validate config and return results."""
errors = yield from async_check_ha_config_file(request.app['hass'])
state = 'invalid' if errors else 'valid'
return self.json({
"result": state,
"errors": errors,
})

View File

@ -0,0 +1,19 @@
"""Provide configuration end points for Groups."""
import asyncio
from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components.group import GROUP_SCHEMA
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'groups.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Setup the Group config API."""
hass.http.register_view(EditKeyBasedConfigView(
'group', 'config', CONFIG_PATH, cv.slug,
GROUP_SCHEMA
))
return True

View File

@ -0,0 +1,91 @@
"""Component to interact with Hassbian tools."""
import asyncio
import json
import os
from homeassistant.components.http import HomeAssistantView
_TEST_OUTPUT = """
{
"suites":{
"libcec":{
"state":"Uninstalled",
"description":"Installs the libcec package for controlling CEC devices from this Pi"
},
"mosquitto":{
"state":"failed",
"description":"Installs the Mosquitto package for setting up a local MQTT server"
},
"openzwave":{
"state":"Uninstalled",
"description":"Installs the Open Z-wave package for setting up your zwave network"
},
"samba":{
"state":"installing",
"description":"Installs the samba package for sharing the hassbian configuration files over the Pi's network."
}
}
}
""" # noqa
@asyncio.coroutine
def async_setup(hass):
"""Setup the hassbian config."""
# Test if is hassbian
test_mode = 'FORCE_HASSBIAN' in os.environ
is_hassbian = test_mode
if not is_hassbian:
return False
hass.http.register_view(HassbianSuitesView(test_mode))
hass.http.register_view(HassbianSuiteInstallView(test_mode))
return True
@asyncio.coroutine
def hassbian_status(hass, test_mode=False):
"""Query for the Hassbian status."""
# fetch real output when not in test mode
if test_mode:
return json.loads(_TEST_OUTPUT)
raise Exception('Real mode not implemented yet.')
class HassbianSuitesView(HomeAssistantView):
"""Hassbian packages endpoint."""
url = '/api/config/hassbian/suites'
name = 'api:config:hassbian:suites'
def __init__(self, test_mode):
"""Initialize suites view."""
self._test_mode = test_mode
@asyncio.coroutine
def get(self, request):
"""Request suite status."""
inp = yield from hassbian_status(request.app['hass'], self._test_mode)
return self.json(inp['suites'])
class HassbianSuiteInstallView(HomeAssistantView):
"""Hassbian packages endpoint."""
url = '/api/config/hassbian/suites/{suite}/install'
name = 'api:config:hassbian:suite'
def __init__(self, test_mode):
"""Initialize suite view."""
self._test_mode = test_mode
@asyncio.coroutine
def post(self, request, suite):
"""Request suite status."""
# do real install if not in test mode
return self.json({"status": "ok"})

View File

@ -0,0 +1,19 @@
"""Provide configuration end points for Z-Wave."""
import asyncio
from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY
import homeassistant.helpers.config_validation as cv
CONFIG_PATH = 'zwave_device_config.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Setup the Z-Wave config API."""
hass.http.register_view(EditKeyBasedConfigView(
'zwave', 'device_config', CONFIG_PATH, cv.entity_id,
DEVICE_CONFIG_SCHEMA_ENTRY
))
return True

View File

@ -6,15 +6,16 @@ This will return a request id that has to be used for future calls.
A callback has to be provided to `request_config` which will be called when A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information. the user has submitted configuration information.
""" """
import asyncio
import logging import logging
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity import generate_entity_id
_INSTANCES = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_REQUESTS = {} _REQUESTS = {}
_KEY_INSTANCE = 'configurator'
ATTR_CONFIGURE_ID = 'configure_id' ATTR_CONFIGURE_ID = 'configure_id'
ATTR_DESCRIPTION = 'description' ATTR_DESCRIPTION = 'description'
@ -72,22 +73,20 @@ def request_done(request_id):
pass pass
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup the configurator component.""" """Setup the configurator component."""
return True return True
def _get_instance(hass): def _get_instance(hass):
"""Get an instance per hass object.""" """Get an instance per hass object."""
try: instance = hass.data.get(_KEY_INSTANCE)
return _INSTANCES[hass]
except KeyError:
_INSTANCES[hass] = Configurator(hass)
if DOMAIN not in hass.config.components: if instance is None:
hass.config.components.append(DOMAIN) instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
return _INSTANCES[hass] return instance
class Configurator(object): class Configurator(object):

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['fuzzywuzzy==0.14.0'] REQUIREMENTS = ['fuzzywuzzy==0.15.0']
ATTR_TEXT = 'text' ATTR_TEXT = 'text'

View File

@ -33,6 +33,20 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [
'window', # Window control
'garage', # Garage door control
]
SUPPORT_OPEN = 1
SUPPORT_CLOSE = 2
SUPPORT_SET_POSITION = 4
SUPPORT_STOP = 8
SUPPORT_OPEN_TILT = 16
SUPPORT_CLOSE_TILT = 32
SUPPORT_STOP_TILT = 64
SUPPORT_SET_TILT_POSITION = 128
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POSITION = 'current_position' ATTR_CURRENT_POSITION = 'current_position'
@ -221,6 +235,21 @@ class CoverDevice(Entity):
return data return data
@property
def supported_features(self):
"""Flag supported features."""
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.current_cover_position is not None:
supported_features |= SUPPORT_SET_POSITION
if self.current_cover_tilt_position is not None:
supported_features |= (
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
return supported_features
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed or not.""" """Return if the cover is closed or not."""

View File

@ -4,7 +4,8 @@ Demo platform for the cover component.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
@ -14,6 +15,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
DemoCover(hass, 'Kitchen Window'), DemoCover(hass, 'Kitchen Window'),
DemoCover(hass, 'Hall Window', 10), DemoCover(hass, 'Hall Window', 10),
DemoCover(hass, 'Living Room Window', 70, 50), DemoCover(hass, 'Living Room Window', 70, 50),
DemoCover(hass, 'Garage Door', device_class='garage',
supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE)),
]) ])
@ -21,11 +24,14 @@ class DemoCover(CoverDevice):
"""Representation of a demo cover.""" """Representation of a demo cover."""
# pylint: disable=no-self-use # pylint: disable=no-self-use
def __init__(self, hass, name, position=None, tilt_position=None): def __init__(self, hass, name, position=None, tilt_position=None,
device_class=None, supported_features=None):
"""Initialize the cover.""" """Initialize the cover."""
self.hass = hass self.hass = hass
self._name = name self._name = name
self._position = position self._position = position
self._device_class = device_class
self._supported_features = supported_features
self._set_position = None self._set_position = None
self._set_tilt_position = None self._set_tilt_position = None
self._tilt_position = tilt_position self._tilt_position = tilt_position
@ -33,6 +39,10 @@ class DemoCover(CoverDevice):
self._closing_tilt = True self._closing_tilt = True
self._unsub_listener_cover = None self._unsub_listener_cover = None
self._unsub_listener_cover_tilt = None self._unsub_listener_cover_tilt = None
if position is None:
self._closed = True
else:
self._closed = self.current_cover_position <= 0
@property @property
def name(self): def name(self):
@ -57,17 +67,28 @@ class DemoCover(CoverDevice):
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
if self._position is not None: return self._closed
if self.current_cover_position > 0:
return False @property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
@property
def supported_features(self):
"""Flag supported features."""
if self._supported_features is not None:
return self._supported_features
else: else:
return True return super().supported_features
else:
return None
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
if self._position in (0, None): if self._position == 0:
return
elif self._position is None:
self._closed = True
self.schedule_update_ha_state()
return return
self._listen_cover() self._listen_cover()
@ -83,7 +104,11 @@ class DemoCover(CoverDevice):
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
if self._position in (100, None): if self._position == 100:
return
elif self._position is None:
self._closed = False
self.schedule_update_ha_state()
return return
self._listen_cover() self._listen_cover()
@ -149,6 +174,9 @@ class DemoCover(CoverDevice):
if self._position in (100, 0, self._set_position): if self._position in (100, 0, self._set_position):
self.stop_cover() self.stop_cover()
self._closed = self.current_cover_position <= 0
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _listen_cover_tilt(self): def _listen_cover_tilt(self):

View File

@ -168,6 +168,11 @@ class GaradgetCover(CoverDevice):
else: else:
return self._state == STATE_CLOSED return self._state == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
def get_token(self): def get_token(self):
"""Get new token for usage during this session.""" """Get new token for usage during this session."""
args = { args = {

View File

@ -10,28 +10,26 @@ properly configured.
import logging import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.cover import CoverDevice,\ from homeassistant.components.cover import CoverDevice, ATTR_POSITION
ATTR_POSITION from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.components.homematic import HMDevice
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the platform.""" """Setup the platform."""
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic") devices = []
return homematic.setup_hmdevice_discovery_helper( for config in discovery_info[ATTR_DISCOVER_DEVICES]:
hass, new_device = HMCover(hass, config)
HMCover, new_device.link_homematic()
discovery_info, devices.append(new_device)
add_callback_devices
) add_devices(devices)
class HMCover(HMDevice, CoverDevice): class HMCover(HMDevice, CoverDevice):
@ -44,13 +42,10 @@ class HMCover(HMDevice, CoverDevice):
None is unknown, 0 is closed, 100 is fully open. None is unknown, 0 is closed, 100 is fully open.
""" """
if self.available:
return int(self._hm_get_state() * 100) return int(self._hm_get_state() * 100)
return None
def set_cover_position(self, **kwargs): def set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
if self.available:
if ATTR_POSITION in kwargs: if ATTR_POSITION in kwargs:
position = float(kwargs[ATTR_POSITION]) position = float(kwargs[ATTR_POSITION])
position = min(100, max(0, position)) position = min(100, max(0, position))
@ -68,17 +63,14 @@ class HMCover(HMDevice, CoverDevice):
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
if self.available:
self._hmdevice.move_up(self._channel) self._hmdevice.move_up(self._channel)
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
if self.available:
self._hmdevice.move_down(self._channel) self._hmdevice.move_down(self._channel)
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the device if in motion.""" """Stop the device if in motion."""
if self.available:
self._hmdevice.stop(self._channel) self._hmdevice.stop(self._channel)
def _init_data_struct(self): def _init_data_struct(self):

View File

@ -4,6 +4,7 @@ Support for MQTT cover devices.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/ https://home-assistant.io/components/cover.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -46,13 +47,14 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the MQTT Cover.""" """Setup the MQTT Cover."""
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
add_devices([MqttCover(
hass, yield from async_add_devices([MqttCover(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
config.get(CONF_COMMAND_TOPIC), config.get(CONF_COMMAND_TOPIC),
@ -71,13 +73,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttCover(CoverDevice): class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT.""" """Representation of a cover that can be controlled using MQTT."""
def __init__(self, hass, name, state_topic, command_topic, qos, def __init__(self, name, state_topic, command_topic, qos, retain,
retain, state_open, state_closed, payload_open, payload_close, state_open, state_closed, payload_open, payload_close,
payload_stop, optimistic, value_template): payload_stop, optimistic, value_template):
"""Initialize the cover.""" """Initialize the cover."""
self._position = None self._position = None
self._state = None self._state = None
self._hass = hass
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
self._command_topic = command_topic self._command_topic = command_topic
@ -89,37 +90,45 @@ class MqttCover(CoverDevice):
self._state_closed = state_closed self._state_closed = state_closed
self._retain = retain self._retain = retain
self._optimistic = optimistic or state_topic is None self._optimistic = optimistic or state_topic is None
self._template = value_template
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method is a coroutine.
"""
@callback @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
if value_template is not None: if self._template is not None:
payload = value_template.async_render_with_possible_json_value( payload = self._template.async_render_with_possible_json_value(
payload) payload)
if payload == self._state_open: if payload == self._state_open:
self._state = False self._state = False
hass.async_add_job(self.async_update_ha_state())
elif payload == self._state_closed: elif payload == self._state_closed:
self._state = True self._state = True
hass.async_add_job(self.async_update_ha_state())
elif payload.isnumeric() and 0 <= int(payload) <= 100: elif payload.isnumeric() and 0 <= int(payload) <= 100:
if int(payload) > 0: if int(payload) > 0:
self._state = False self._state = False
else: else:
self._state = True self._state = True
self._position = int(payload) self._position = int(payload)
hass.async_add_job(self.async_update_ha_state())
else: else:
_LOGGER.warning( _LOGGER.warning(
"Payload is not True, False, or integer (0-100): %s", "Payload is not True, False, or integer (0-100): %s",
payload) payload)
return
self.hass.async_add_job(self.async_update_ha_state())
if self._state_topic is None: if self._state_topic is None:
# Force into optimistic mode. # Force into optimistic mode.
self._optimistic = True self._optimistic = True
else: else:
mqtt.subscribe(hass, self._state_topic, message_received, yield from mqtt.async_subscribe(
self._qos) self.hass, self._state_topic, message_received, self._qos)
@property @property
def should_poll(self): def should_poll(self):
@ -144,25 +153,40 @@ class MqttCover(CoverDevice):
""" """
return self._position return self._position
def open_cover(self, **kwargs): @asyncio.coroutine
"""Move the cover up.""" def async_open_cover(self, **kwargs):
mqtt.publish(self.hass, self._command_topic, self._payload_open, """Move the cover up.
self._qos, self._retain)
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_open, self._qos,
self._retain)
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = False self._state = False
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
def close_cover(self, **kwargs): @asyncio.coroutine
"""Move the cover down.""" def async_close_cover(self, **kwargs):
mqtt.publish(self.hass, self._command_topic, self._payload_close, """Move the cover down.
self._qos, self._retain)
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_close, self._qos,
self._retain)
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = True self._state = True
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
def stop_cover(self, **kwargs): @asyncio.coroutine
"""Stop the device.""" def async_stop_cover(self, **kwargs):
mqtt.publish(self.hass, self._command_topic, self._payload_stop, """Stop the device.
self._qos, self._retain)
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._command_topic, self._payload_stop, self._qos,
self._retain)

View File

@ -0,0 +1,90 @@
"""
Support for MyQ-Enabled Garage Doors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.myq/
"""
import logging
import voluptuous as vol
from homeassistant.components.cover import CoverDevice
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip'
'#pymyq==0.0.6']
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_TYPE): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
DEFAULT_NAME = 'myq'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MyQ component."""
from pymyq import MyQAPI as pymyq
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
brand = config.get(CONF_TYPE)
logger = logging.getLogger(__name__)
myq = pymyq(username, password, brand)
if not myq.is_supported_brand():
logger.error('MyQ Cover - Unsupported Type. See documentation')
return
if not myq.is_login_valid():
logger.error('MyQ Cover - Username or Password is incorrect')
return
try:
add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors())
except (TypeError, KeyError, NameError) as ex:
logger.error("MyQ Cover - %s", ex)
class MyQDevice(CoverDevice):
"""Representation of a MyQ cover."""
def __init__(self, myq, device):
"""Initialize with API object, device id."""
self.myq = myq
self.device_id = device['deviceid']
self._name = device['name']
self._status = STATE_CLOSED
@property
def should_poll(self):
"""Poll for state."""
return True
@property
def name(self):
"""Return the name of the garage door if any."""
return self._name if self._name else DEFAULT_NAME
@property
def is_closed(self):
"""Return True if cover is closed, else False."""
return self._status == STATE_CLOSED
def close_cover(self):
"""Issue close command to cover."""
self.myq.close_device(self.device_id)
def open_cover(self):
"""Issue open command to cover."""
self.myq.open_device(self.device_id)
def update(self):
"""Update status of cover."""
self._status = self.myq.get_status(self.device_id)

View File

@ -7,22 +7,17 @@ https://home-assistant.io/components/cover.zwave/
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
from homeassistant.components.cover import DOMAIN from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import workaround
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
SOMFY = 0x47
SOMFY_ZRTSI = 0x5a52
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
WORKAROUND = 'workaround'
DEVICE_MAPPINGS = {
SOMFY_ZRTSI_CONTROLLER: WORKAROUND
}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Z-Wave covers.""" """Find and return Z-Wave covers."""
@ -58,16 +53,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
self._open_id = None self._open_id = None
self._close_id = None self._close_id = None
self._current_position = None self._current_position = None
self._workaround = None
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_type, 16))
if specific_sensor_key in DEVICE_MAPPINGS: self._workaround = workaround.get_device_mapping(value)
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND: if self._workaround:
_LOGGER.debug("Controller without positioning feedback") _LOGGER.debug("Using workaround %s", self._workaround)
self._workaround = 1 self.update_properties()
def update_properties(self): def update_properties(self):
"""Callback on data changes for node values.""" """Callback on data changes for node values."""
@ -81,6 +71,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
self._close_id = self.get_value( self._close_id = self.get_value(
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
label=['Close', 'Down'], member='value_id') label=['Close', 'Down'], member='value_id')
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
self._open_id, self._close_id = self._close_id, self._open_id
@property @property
def is_closed(self): def is_closed(self):
@ -95,7 +87,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
@property @property
def current_cover_position(self): def current_cover_position(self):
"""Return the current position of Zwave roller shutter.""" """Return the current position of Zwave roller shutter."""
if not self._workaround: if self._workaround == workaround.WORKAROUND_NO_POSITION:
return None
if self._current_position is not None: if self._current_position is not None:
if self._current_position <= 5: if self._current_position <= 5:
return 0 return 0
@ -127,11 +120,16 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
def __init__(self, value): def __init__(self, value):
"""Initialize the zwave garage door.""" """Initialize the zwave garage door."""
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self.update_properties()
def update_properties(self):
"""Callback on data changes for node values."""
self._state = self._value.data
@property @property
def is_closed(self): def is_closed(self):
"""Return the current position of Zwave garage door.""" """Return the current position of Zwave garage door."""
return not self._value.data return not self._state
def close_cover(self): def close_cover(self):
"""Close the garage door.""" """Close the garage door."""
@ -140,3 +138,13 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
def open_cover(self): def open_cover(self):
"""Open the garage door.""" """Open the garage door."""
self._value.data = True self._value.data = True
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_GARAGE

View File

@ -25,6 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util as util import homeassistant.util as util
@ -132,6 +133,12 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
devices = yield from async_load_config(yaml_path, hass, consider_home) devices = yield from async_load_config(yaml_path, hass, consider_home)
tracker = DeviceTracker(hass, consider_home, track_new, devices) tracker = DeviceTracker(hass, consider_home, track_new, devices)
# added_to_hass
add_tasks = [device.async_added_to_hass() for device in devices
if device.track]
if add_tasks:
yield from asyncio.wait(add_tasks, loop=hass.loop)
# update tracked devices # update tracked devices
update_tasks = [device.async_update_ha_state() for device in devices update_tasks = [device.async_update_ha_state() for device in devices
if device.track] if device.track]
@ -167,8 +174,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
raise HomeAssistantError("Invalid device_tracker platform.") raise HomeAssistantError("Invalid device_tracker platform.")
if scanner: if scanner:
yield from async_setup_scanner_platform( async_setup_scanner_platform(
hass, p_config, scanner, tracker.async_see) hass, p_config, scanner, tracker.async_see, p_type)
return return
if not setup: if not setup:
@ -561,6 +568,26 @@ class Device(Entity):
if resp is not None: if resp is not None:
yield from resp.release() yield from resp.release()
@asyncio.coroutine
def async_added_to_hass(self):
"""Called when entity about to be added to hass."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if not state:
return
self._state = state.state
for attr, var in (
(ATTR_SOURCE_TYPE, 'source_type'),
(ATTR_GPS_ACCURACY, 'gps_accuracy'),
(ATTR_BATTERY, 'battery'),
):
if attr in state.attributes:
setattr(self, var, state.attributes[attr])
if ATTR_LONGITUDE in state.attributes:
self.gps = (state.attributes[ATTR_LATITUDE],
state.attributes[ATTR_LONGITUDE])
class DeviceScanner(object): class DeviceScanner(object):
"""Device scanner object.""" """Device scanner object."""
@ -638,14 +665,16 @@ def async_load_config(path: str, hass: HomeAssistantType,
return [] return []
@asyncio.coroutine @callback
def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, async_see_device: Callable): scanner: Any, async_see_device: Callable,
platform: str):
"""Helper method to connect scanner-based platform to device tracker. """Helper method to connect scanner-based platform to device tracker.
This method is a coroutine. This method must be run in the event loop.
""" """
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
update_lock = asyncio.Lock(loop=hass.loop)
scanner.hass = hass scanner.hass = hass
# Initial scan of each mac we also tell about host name for config # Initial scan of each mac we also tell about host name for config
@ -654,6 +683,13 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
@asyncio.coroutine @asyncio.coroutine
def async_device_tracker_scan(now: dt_util.dt.datetime): def async_device_tracker_scan(now: dt_util.dt.datetime):
"""Called when interval matches.""" """Called when interval matches."""
if update_lock.locked():
_LOGGER.warning(
"Updating device list from %s took longer than the scheduled "
"scan interval %s", platform, interval)
return
with (yield from update_lock):
found_devices = yield from scanner.async_scan_devices() found_devices = yield from scanner.async_scan_devices()
for mac in found_devices: for mac in found_devices:
@ -678,7 +714,7 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
hass.async_add_job(async_see_device(**kwargs)) hass.async_add_job(async_see_device(**kwargs))
async_track_time_interval(hass, async_device_tracker_scan, interval) async_track_time_interval(hass, async_device_tracker_scan, interval)
hass.async_add_job(async_device_tracker_scan, None) hass.async_add_job(async_device_tracker_scan(None))
def update_config(path: str, dev_id: str, device: Device): def update_config(path: str, dev_id: str, device: Device):

View File

@ -16,7 +16,8 @@ import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -25,6 +26,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
CONF_PROTOCOL = 'protocol' CONF_PROTOCOL = 'protocol'
CONF_MODE = 'mode' CONF_MODE = 'mode'
DEFAULT_SSH_PORT = 22
CONF_SSH_KEY = 'ssh_key' CONF_SSH_KEY = 'ssh_key'
CONF_PUB_KEY = 'pub_key' CONF_PUB_KEY = 'pub_key'
SECRET_GROUP = 'Password or SSH Key' SECRET_GROUP = 'Password or SSH Key'
@ -38,6 +40,7 @@ PLATFORM_SCHEMA = vol.All(
vol.In(['ssh', 'telnet']), vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.Optional(CONF_MODE, default='router'):
vol.In(['router', 'ap']), vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
@ -112,12 +115,16 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.ssh_key = config.get('ssh_key', config.get('pub_key', '')) self.ssh_key = config.get('ssh_key', config.get('pub_key', ''))
self.protocol = config[CONF_PROTOCOL] self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE] self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.ssh_args = {}
if self.protocol == 'ssh': if self.protocol == 'ssh':
self.ssh_args['port'] = self.port
if self.ssh_key: if self.ssh_key:
self.ssh_secret = {'ssh_key': self.ssh_key} self.ssh_args['ssh_key'] = self.ssh_key
elif self.password: elif self.password:
self.ssh_secret = {'password': self.password} self.ssh_args['password'] = self.password
else: else:
_LOGGER.error('No password or private key specified') _LOGGER.error('No password or private key specified')
self.success_init = False self.success_init = False
@ -179,7 +186,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
ssh = pxssh.pxssh() ssh = pxssh.pxssh()
try: try:
ssh.login(self.host, self.username, **self.ssh_secret) ssh.login(self.host, self.username, **self.ssh_args)
except exceptions.EOF as err: except exceptions.EOF as err:
_LOGGER.error('Connection refused. Is SSH enabled?') _LOGGER.error('Connection refused. Is SSH enabled?')
return None return None

View File

@ -13,28 +13,31 @@ import aiohttp
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.aiohttp_client import async_create_clientsession
# Configuration constant specific for tado
CONF_HOME_ID = 'home_id'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string vol.Optional(CONF_HOME_ID): cv.string
}) })
def get_scanner(hass, config): def get_scanner(hass, config):
"""Return a Tado scanner.""" """Return a Tado scanner."""
scanner = TadoDeviceScanner(hass, config[DOMAIN]) scanner = TadoDeviceScanner(hass, config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
@ -50,8 +53,19 @@ class TadoDeviceScanner(DeviceScanner):
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
self.tadoapiurl = 'https://my.tado.com/api/v2/me' \
'?username={}&password={}' # The Tado device tracker can work with or without a home_id
self.home_id = config[CONF_HOME_ID] if CONF_HOME_ID in config else None
# If there's a home_id, we need a different API URL
if self.home_id is None:
self.tadoapiurl = 'https://my.tado.com/api/v2/me'
else:
self.tadoapiurl = 'https://my.tado.com/api/v2' \
'/homes/{home_id}/mobileDevices'
# The API URL always needs a username and password
self.tadoapiurl += '?username={username}&password={password}'
self.websession = async_create_clientsession( self.websession = async_create_clientsession(
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)) hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
@ -62,7 +76,11 @@ class TadoDeviceScanner(DeviceScanner):
@asyncio.coroutine @asyncio.coroutine
def async_scan_devices(self): def async_scan_devices(self):
"""Scan for devices and return a list containing found device ids.""" """Scan for devices and return a list containing found device ids."""
yield from self._update_info() info = self._update_info()
# Don't yield if we got None
if info is not None:
yield from info
return [device.mac for device in self.last_results] return [device.mac for device in self.last_results]
@ -87,25 +105,27 @@ class TadoDeviceScanner(DeviceScanner):
_LOGGER.debug("Requesting Tado") _LOGGER.debug("Requesting Tado")
last_results = [] last_results = []
response = None response = None
tadojson = None tado_json = None
try: try:
# get first token
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
url = self.tadoapiurl.format(self.username, self.password) # Format the URL here, so we can log the template URL if
response = yield from self.websession.get( # anything goes wrong without exposing username and password.
url url = self.tadoapiurl.format(home_id=self.home_id,
) username=self.username,
password=self.password)
# Go get 'em!
response = yield from self.websession.get(url)
# error on Tado webservice # error on Tado webservice
if response.status != 200: if response.status != 200:
_LOGGER.warning( _LOGGER.warning(
"Error %d on %s.", response.status, self.tadoapiurl) "Error %d on %s.", response.status, self.tadoapiurl)
self.token = None
return return
tadojson = yield from response.json() tado_json = yield from response.json()
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Cannot load Tado data") _LOGGER.error("Cannot load Tado data")
@ -115,15 +135,24 @@ class TadoDeviceScanner(DeviceScanner):
if response is not None: if response is not None:
yield from response.release() yield from response.release()
# Find devices that have geofencing enabled, and are currently at home # Without a home_id, we fetched an URL where the mobile devices can be
for mobiledevice in tadojson['mobileDevices']: # found under the mobileDevices key.
if 'location' in mobiledevice: if 'mobileDevices' in tado_json:
if mobiledevice['location']['atHome']: tado_json = tado_json['mobileDevices']
deviceid = mobiledevice['id']
devicename = mobiledevice['name'] # Find devices that have geofencing enabled, and are currently at home.
last_results.append(Device(deviceid, devicename)) for mobile_device in tado_json:
if 'location' in mobile_device:
if mobile_device['location']['atHome']:
device_id = mobile_device['id']
device_name = mobile_device['name']
last_results.append(Device(device_id, device_name))
self.last_results = last_results self.last_results = last_results
_LOGGER.info("Tado presence query successful") _LOGGER.info(
"Tado presence query successful, %d device(s) at home",
len(self.last_results)
)
return True return True

View File

@ -12,6 +12,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -58,7 +59,7 @@ class TrackRDeviceScanner(object):
trackr_id = trackr.tracker_id() trackr_id = trackr.tracker_id()
trackr_device_id = trackr.id() trackr_device_id = trackr.id()
lost = trackr.lost() lost = trackr.lost()
dev_id = trackr.name().replace(" ", "_") dev_id = slugify(trackr.name())
if dev_id is None: if dev_id is None:
dev_id = trackr_id dev_id = trackr_id
location = trackr.last_known_location() location = trackr.last_known_location()

View File

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,19 +63,13 @@ class UPCDeviceScanner(DeviceScanner):
"Chrome/47.0.2526.106 Safari/537.36") "Chrome/47.0.2526.106 Safari/537.36")
} }
self.websession = async_create_clientsession( self.websession = async_get_clientsession(hass)
hass, auto_cleanup=False,
cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)
)
@asyncio.coroutine @asyncio.coroutine
def async_logout(event): def async_logout(event):
"""Logout from upc connect box.""" """Logout from upc connect box."""
try:
yield from self._async_ws_function(CMD_LOGOUT) yield from self._async_ws_function(CMD_LOGOUT)
self.token = None self.token = None
finally:
self.websession.detach()
hass.bus.async_listen_once( hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, async_logout) EVENT_HOMEASSISTANT_STOP, async_logout)
@ -92,8 +86,7 @@ class UPCDeviceScanner(DeviceScanner):
raw = yield from self._async_ws_function(CMD_DEVICES) raw = yield from self._async_ws_function(CMD_DEVICES)
try: try:
xml_root = yield from self.hass.loop.run_in_executor( xml_root = ET.fromstring(raw)
None, ET.fromstring, raw)
return [mac.text for mac in xml_root.iter('MACAddr')] return [mac.text for mac in xml_root.iter('MACAddr')]
except (ET.ParseError, TypeError): except (ET.ParseError, TypeError):
_LOGGER.warning("Can't read device from %s", self.host) _LOGGER.warning("Can't read device from %s", self.host)
@ -111,7 +104,6 @@ class UPCDeviceScanner(DeviceScanner):
response = None response = None
try: try:
# get first token # get first token
self.websession.cookie_jar.clear()
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.get( response = yield from self.websession.get(
"http://{}/common_page/login.html".format(self.host) "http://{}/common_page/login.html".format(self.host)
@ -150,27 +142,26 @@ class UPCDeviceScanner(DeviceScanner):
if additional_form: if additional_form:
form_data.update(additional_form) form_data.update(additional_form)
redirects = True if function != CMD_DEVICES else False
response = None response = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.websession.post( response = yield from self.websession.post(
"http://{}/xml/getter.xml".format(self.host), "http://{}/xml/getter.xml".format(self.host),
data=form_data, data=form_data,
headers=self.headers headers=self.headers,
allow_redirects=redirects
) )
# error on UPC webservice # error?
if response.status != 200: if response.status != 200:
_LOGGER.warning( _LOGGER.warning("Receive http code %d", response.status)
"Error %d on %s.", response.status, function)
self.token = None self.token = None
return return
# load data, store token for next request # load data, store token for next request
raw = yield from response.text()
self.token = response.cookies['sessionToken'].value self.token = response.cookies['sessionToken'].value
return (yield from response.text())
return raw
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.error("Error on %s", function) _LOGGER.error("Error on %s", function)

View File

@ -1,97 +1,36 @@
""" """
Support for Volvo On Call. Support for tracking a Volvo.
http://www.volvocars.com/intl/own/owner-info/volvo-on-call
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.volvooncall/ https://home-assistant.io/components/device_tracker.volvooncall/
""" """
import logging import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import ( from homeassistant.components.volvooncall import DOMAIN
CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME)
from homeassistant.components.device_tracker import (
DEFAULT_SCAN_INTERVAL, PLATFORM_SCHEMA)
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['volvooncall==0.1.1']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config, see, discovery_info=None): def setup_scanner(hass, config, see, discovery_info=None):
"""Validate the configuration and return a scanner.""" """Setup Volvo tracker."""
from volvooncall import Connection if discovery_info is None:
connection = Connection( return
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
interval = max(MIN_TIME_BETWEEN_SCANS, vin, _ = discovery_info
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) vehicle = hass.data[DOMAIN].vehicles[vin]
def _see_vehicle(vehicle): host_name = vehicle.registration_number
position = vehicle["position"] dev_id = 'volvo_' + slugify(host_name)
dev_id = "volvo_" + slugify(vehicle["registrationNumber"])
host_name = "%s (%s/%s)" % (
vehicle["registrationNumber"],
vehicle["vehicleType"],
vehicle["modelYear"])
def any_opened(door):
"""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")
def see_vehicle(vehicle):
"""Callback for reporting vehicle position."""
see(dev_id=dev_id, see(dev_id=dev_id,
host_name=host_name, host_name=host_name,
gps=(position["latitude"], gps=(vehicle.position['latitude'],
position["longitude"]), vehicle.position['longitude']))
attributes=attributes)
def update(now): hass.data[DOMAIN].entities[vin].append(see_vehicle)
"""Update status from the online service.""" see_vehicle(vehicle)
_LOGGER.info("Updating")
try:
res, vehicles = connection.update()
if not res:
_LOGGER.error("Could not query server")
return False
for vehicle in vehicles:
_see_vehicle(vehicle)
return True return True
finally:
track_point_in_utc_time(hass, update, now + interval)
_LOGGER.info('Logging in to service')
return update(utcnow())

View File

@ -11,6 +11,7 @@ import threading
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
@ -41,10 +42,16 @@ SERVICE_HANDLERS = {
'yeelight': ('light', 'yeelight'), 'yeelight': ('light', 'yeelight'),
'flux_led': ('light', 'flux_led'), 'flux_led': ('light', 'flux_led'),
'apple_tv': ('media_player', 'apple_tv'), 'apple_tv': ('media_player', 'apple_tv'),
'openhome': ('media_player', 'openhome'),
} }
CONF_IGNORE = 'ignore'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}), DOMAIN: vol.Schema({
vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)])
}),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -57,10 +64,17 @@ def setup(hass, config):
# Disable zeroconf logging, it spams # Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL) logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
# Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE]
lock = threading.Lock() lock = threading.Lock()
def new_service_listener(service, info): def new_service_listener(service, info):
"""Called when a new service is found.""" """Called when a new service is found."""
if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info)
return
with lock: with lock:
logger.info("Found new service: %s %s", service, info) logger.info("Found new service: %s %s", service, info)

View File

@ -4,20 +4,24 @@ Support for Envisalink devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/envisalink/ https://home-assistant.io/components/envisalink/
""" """
import asyncio
import logging import logging
import time
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.discovery import load_platform from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['pyenvisalink==2.0', 'pydispatcher==2.0.5'] REQUIREMENTS = ['pyenvisalink==2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'envisalink' DOMAIN = 'envisalink'
EVL_CONTROLLER = None DATA_EVL = 'envisalink'
CONF_EVL_HOST = 'host' CONF_EVL_HOST = 'host'
CONF_EVL_PORT = 'port' CONF_EVL_PORT = 'port'
@ -43,9 +47,9 @@ DEFAULT_ZONEDUMP_INTERVAL = 30
DEFAULT_ZONETYPE = 'opening' DEFAULT_ZONETYPE = 'opening'
DEFAULT_PANIC = 'Police' DEFAULT_PANIC = 'Police'
SIGNAL_ZONE_UPDATE = 'zones_updated' SIGNAL_ZONE_UPDATE = 'envisalink.zones_updated'
SIGNAL_PARTITION_UPDATE = 'partition_updated' SIGNAL_PARTITION_UPDATE = 'envisalink.partition_updated'
SIGNAL_KEYPAD_UPDATE = 'keypad_updated' SIGNAL_KEYPAD_UPDATE = 'envisalink.keypad_updated'
ZONE_SCHEMA = vol.Schema({ ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONENAME): cv.string, vol.Required(CONF_ZONENAME): cv.string,
@ -77,119 +81,111 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument @asyncio.coroutine
def setup(hass, base_config): def async_setup(hass, config):
"""Common setup for Envisalink devices.""" """Common setup for Envisalink devices."""
from pyenvisalink import EnvisalinkAlarmPanel from pyenvisalink import EnvisalinkAlarmPanel
from pydispatch import dispatcher
global EVL_CONTROLLER conf = config.get(DOMAIN)
config = base_config.get(DOMAIN) host = conf.get(CONF_EVL_HOST)
port = conf.get(CONF_EVL_PORT)
code = conf.get(CONF_CODE)
panel_type = conf.get(CONF_PANEL_TYPE)
panic_type = conf.get(CONF_PANIC)
version = conf.get(CONF_EVL_VERSION)
user = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASS)
keep_alive = conf.get(CONF_EVL_KEEPALIVE)
zone_dump = conf.get(CONF_ZONEDUMP_INTERVAL)
zones = conf.get(CONF_ZONES)
partitions = conf.get(CONF_PARTITIONS)
sync_connect = asyncio.Future(loop=hass.loop)
_host = config.get(CONF_EVL_HOST) controller = EnvisalinkAlarmPanel(
_port = config.get(CONF_EVL_PORT) host, port, panel_type, version, user, password, zone_dump,
_code = config.get(CONF_CODE) keep_alive, hass.loop)
_panel_type = config.get(CONF_PANEL_TYPE) hass.data[DATA_EVL] = controller
_panic_type = config.get(CONF_PANIC)
_version = config.get(CONF_EVL_VERSION)
_user = config.get(CONF_USERNAME)
_pass = config.get(CONF_PASS)
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
_zones = config.get(CONF_ZONES)
_partitions = config.get(CONF_PARTITIONS)
_connect_status = {}
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
_port,
_panel_type,
_version,
_user,
_pass,
_zone_dump,
_keep_alive,
hass.loop)
@callback
def login_fail_callback(data): def login_fail_callback(data):
"""Callback for when the evl rejects our login.""" """Callback for when the evl rejects our login."""
_LOGGER.error("The envisalink rejected your credentials.") _LOGGER.error("The envisalink rejected your credentials.")
_connect_status['fail'] = 1 sync_connect.set_result(False)
@callback
def connection_fail_callback(data): def connection_fail_callback(data):
"""Network failure callback.""" """Network failure callback."""
_LOGGER.error("Could not establish a connection with the envisalink.") _LOGGER.error("Could not establish a connection with the envisalink.")
_connect_status['fail'] = 1 sync_connect.set_result(False)
@callback
def connection_success_callback(data): def connection_success_callback(data):
"""Callback for a successful connection.""" """Callback for a successful connection."""
_LOGGER.info("Established a connection with the envisalink.") _LOGGER.info("Established a connection with the envisalink.")
_connect_status['success'] = 1 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
sync_connect.set_result(True)
@callback
def zones_updated_callback(data): def zones_updated_callback(data):
"""Handle zone timer updates.""" """Handle zone timer updates."""
_LOGGER.info("Envisalink sent a zone update event. Updating zones...") _LOGGER.info("Envisalink sent a zone update event. Updating zones...")
dispatcher.send(signal=SIGNAL_ZONE_UPDATE, async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
sender=None,
zone=data)
@callback
def alarm_data_updated_callback(data): def alarm_data_updated_callback(data):
"""Handle non-alarm based info updates.""" """Handle non-alarm based info updates."""
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...") _LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE, async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
sender=None,
partition=data)
@callback
def partition_updated_callback(data): def partition_updated_callback(data):
"""Handle partition changes thrown by evl (including alarms).""" """Handle partition changes thrown by evl (including alarms)."""
_LOGGER.info("The envisalink sent a partition update event.") _LOGGER.info("The envisalink sent a partition update event.")
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE, async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
sender=None,
partition=data)
@callback
def stop_envisalink(event): def stop_envisalink(event):
"""Shutdown envisalink connection and thread on exit.""" """Shutdown envisalink connection and thread on exit."""
_LOGGER.info("Shutting down envisalink.") _LOGGER.info("Shutting down envisalink.")
EVL_CONTROLLER.stop() controller.stop()
def start_envisalink(event): controller.callback_zone_timer_dump = zones_updated_callback
"""Startup process for the Envisalink.""" controller.callback_zone_state_change = zones_updated_callback
hass.loop.call_soon_threadsafe(EVL_CONTROLLER.start) controller.callback_partition_state_change = partition_updated_callback
for _ in range(10): controller.callback_keypad_update = alarm_data_updated_callback
if 'success' in _connect_status: controller.callback_login_failure = login_fail_callback
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) controller.callback_login_timeout = connection_fail_callback
return True controller.callback_login_success = connection_success_callback
elif 'fail' in _connect_status:
return False
else:
time.sleep(1)
_LOGGER.error("Timeout occurred while establishing evl connection.") _LOGGER.info("Start envisalink.")
return False controller.start()
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback result = yield from sync_connect
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback if not result:
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
EVL_CONTROLLER.callback_login_failure = login_fail_callback
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
EVL_CONTROLLER.callback_login_success = connection_success_callback
_result = start_envisalink(None)
if not _result:
return False return False
# Load sub-components for Envisalink # Load sub-components for Envisalink
if _partitions: if partitions:
load_platform(hass, 'alarm_control_panel', 'envisalink', hass.async_add_job(async_load_platform(
{CONF_PARTITIONS: _partitions, hass, 'alarm_control_panel', 'envisalink', {
CONF_CODE: _code, CONF_PARTITIONS: partitions,
CONF_PANIC: _panic_type}, base_config) CONF_CODE: code,
load_platform(hass, 'sensor', 'envisalink', CONF_PANIC: panic_type
{CONF_PARTITIONS: _partitions, }, config
CONF_CODE: _code}, base_config) ))
if _zones: hass.async_add_job(async_load_platform(
load_platform(hass, 'binary_sensor', 'envisalink', hass, 'sensor', 'envisalink', {
{CONF_ZONES: _zones}, base_config) CONF_PARTITIONS: partitions,
CONF_CODE: code
}, config
))
if zones:
hass.async_add_job(async_load_platform(
hass, 'binary_sensor', 'envisalink', {
CONF_ZONES: zones
}, config
))
return True return True

View File

@ -4,10 +4,12 @@ Support for MQTT fans.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.mqtt/ https://home-assistant.io/components/fan.mqtt/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF,
@ -73,11 +75,10 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
}) })
# pylint: disable=unused-argument @asyncio.coroutine
def setup_platform(hass, config, add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup MQTT fan platform.""" """Setup MQTT fan platform."""
add_devices([MqttFan( yield from async_add_devices([MqttFan(
hass,
config.get(CONF_NAME), config.get(CONF_NAME),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
@ -113,15 +114,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttFan(FanEntity): class MqttFan(FanEntity):
"""A MQTT fan component.""" """A MQTT fan component."""
def __init__(self, hass, name, topic, templates, qos, retain, payload, def __init__(self, name, topic, templates, qos, retain, payload,
speed_list, optimistic): speed_list, optimistic):
"""Initialize the MQTT fan.""" """Initialize the MQTT fan."""
self._hass = hass
self._name = name self._name = name
self._topic = topic self._topic = topic
self._qos = qos self._qos = qos
self._retain = retain self._retain = retain
self._payload = payload self._payload = payload
self._templates = templates
self._speed_list = speed_list self._speed_list = speed_list
self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None
self._optimistic_oscillation = ( self._optimistic_oscillation = (
@ -129,19 +130,29 @@ class MqttFan(FanEntity):
self._optimistic_speed = ( self._optimistic_speed = (
optimistic or topic[CONF_SPEED_STATE_TOPIC] is None) optimistic or topic[CONF_SPEED_STATE_TOPIC] is None)
self._state = False self._state = False
self._speed = None
self._oscillation = None
self._supported_features = 0 self._supported_features = 0
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC] self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC]
is not None and SUPPORT_OSCILLATE) is not None and SUPPORT_OSCILLATE)
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
is not None and SUPPORT_SET_SPEED) is not None and SUPPORT_SET_SPEED)
for key, tpl in list(templates.items()): @asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe mqtt events.
This method is a coroutine.
"""
templates = {}
for key, tpl in list(self._templates.items()):
if tpl is None: if tpl is None:
templates[key] = lambda value: value templates[key] = lambda value: value
else: else:
tpl.hass = hass tpl.hass = self.hass
templates[key] = tpl.render_with_possible_json_value templates[key] = tpl.async_render_with_possible_json_value
@callback
def state_received(topic, payload, qos): def state_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
payload = templates[CONF_STATE](payload) payload = templates[CONF_STATE](payload)
@ -149,13 +160,14 @@ class MqttFan(FanEntity):
self._state = True self._state = True
elif payload == self._payload[STATE_OFF]: elif payload == self._payload[STATE_OFF]:
self._state = False self._state = False
self.hass.async_add_job(self.async_update_ha_state())
self.schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None: if self._topic[CONF_STATE_TOPIC] is not None:
mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC], yield from mqtt.async_subscribe(
state_received, self._qos) self.hass, self._topic[CONF_STATE_TOPIC], state_received,
self._qos)
@callback
def speed_received(topic, payload, qos): def speed_received(topic, payload, qos):
"""A new MQTT message for the speed has been received.""" """A new MQTT message for the speed has been received."""
payload = templates[ATTR_SPEED](payload) payload = templates[ATTR_SPEED](payload)
@ -165,17 +177,15 @@ class MqttFan(FanEntity):
self._speed = SPEED_MEDIUM self._speed = SPEED_MEDIUM
elif payload == self._payload[SPEED_HIGH]: elif payload == self._payload[SPEED_HIGH]:
self._speed = SPEED_HIGH self._speed = SPEED_HIGH
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_SPEED_STATE_TOPIC] is not None: if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC], yield from mqtt.async_subscribe(
speed_received, self._qos) self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
self._speed = SPEED_OFF self._qos)
elif self._topic[CONF_SPEED_COMMAND_TOPIC] is not None:
self._speed = SPEED_OFF
else:
self._speed = SPEED_OFF self._speed = SPEED_OFF
@callback
def oscillation_received(topic, payload, qos): def oscillation_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
payload = templates[OSCILLATION](payload) payload = templates[OSCILLATION](payload)
@ -183,17 +193,13 @@ class MqttFan(FanEntity):
self._oscillation = True self._oscillation = True
elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]:
self._oscillation = False self._oscillation = False
self.schedule_update_ha_state() self.hass.async_add_job(self.async_update_ha_state())
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
mqtt.subscribe(self._hass, yield from mqtt.async_subscribe(
self._topic[CONF_OSCILLATION_STATE_TOPIC], self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
oscillation_received, self._qos) oscillation_received, self._qos)
self._oscillation = False self._oscillation = False
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None:
self._oscillation = False
else:
self._oscillation = False
@property @property
def should_poll(self): def should_poll(self):
@ -235,22 +241,37 @@ class MqttFan(FanEntity):
"""Return the oscillation state.""" """Return the oscillation state."""
return self._oscillation return self._oscillation
def turn_on(self, speed: str=None) -> None: @asyncio.coroutine
"""Turn on the entity.""" def async_turn_on(self, speed: str=None) -> None:
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], """Turn on the entity.
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_ON], self._qos, self._retain) self._payload[STATE_ON], self._qos, self._retain)
if speed: if speed:
self.set_speed(speed) yield from self.async_set_speed(speed)
def turn_off(self) -> None: @asyncio.coroutine
"""Turn off the entity.""" def async_turn_off(self) -> None:
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC], """Turn off the entity.
This method is a coroutine.
"""
mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC],
self._payload[STATE_OFF], self._qos, self._retain) self._payload[STATE_OFF], self._qos, self._retain)
def set_speed(self, speed: str) -> None: @asyncio.coroutine
"""Set the speed of the fan.""" def async_set_speed(self, speed: str) -> None:
if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None: """Set the speed of the fan.
mqtt_payload = SPEED_OFF
This method is a coroutine.
"""
if self._topic[CONF_SPEED_COMMAND_TOPIC] is None:
return
if speed == SPEED_LOW: if speed == SPEED_LOW:
mqtt_payload = self._payload[SPEED_LOW] mqtt_payload = self._payload[SPEED_LOW]
elif speed == SPEED_MEDIUM: elif speed == SPEED_MEDIUM:
@ -259,19 +280,33 @@ class MqttFan(FanEntity):
mqtt_payload = self._payload[SPEED_HIGH] mqtt_payload = self._payload[SPEED_HIGH]
else: else:
mqtt_payload = speed mqtt_payload = speed
self._speed = speed
mqtt.publish(self._hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
mqtt_payload, self._qos, self._retain)
self.schedule_update_ha_state()
def oscillate(self, oscillating: bool) -> None: mqtt.async_publish(
"""Set oscillation.""" self.hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None: mqtt_payload, self._qos, self._retain)
self._oscillation = oscillating
payload = self._payload[OSCILLATE_ON_PAYLOAD] if self._optimistic_speed:
self._speed = speed
self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_oscillate(self, oscillating: bool) -> None:
"""Set oscillation.
This method is a coroutine.
"""
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is None:
return
if oscillating is False: if oscillating is False:
payload = self._payload[OSCILLATE_OFF_PAYLOAD] payload = self._payload[OSCILLATE_OFF_PAYLOAD]
mqtt.publish(self._hass, else:
self._topic[CONF_OSCILLATION_COMMAND_TOPIC], payload = self._payload[OSCILLATE_ON_PAYLOAD]
mqtt.async_publish(
self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
payload, self._qos, self._retain) payload, self._qos, self._retain)
self.schedule_update_ha_state()
if self._optimistic_oscillation:
self._oscillation = oscillating
self.hass.async_add_job(self.async_update_ha_state())

View File

@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'
REQUIREMENTS = ["ha-ffmpeg==1.4"] REQUIREMENTS = ["ha-ffmpeg==1.5"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api'] DEPENDENCIES = ['api', 'websocket_api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html' URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
MANIFEST_JSON = { MANIFEST_JSON = {
"background_color": "#FFFFFF", "background_color": "#FFFFFF",
"description": "Open-source home automation platform running on Python 3.", "description": "Open-source home automation platform running on Python 3.",
@ -51,17 +51,22 @@ _LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, sidebar_title=None, def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None): sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel.""" """Register a built-in panel."""
path = 'panels/ha-panel-{}.html'.format(component_name) nondev_path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.http.development: if hass.http.development:
url = ('/static/home-assistant-polymer/panels/' url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name)) '{0}/ha-panel-{0}.html'.format(component_name))
path = os.path.join(
STATIC_PATH, 'home-assistant-polymer/panels/',
'{0}/ha-panel-{0}.html'.format(component_name))
else: else:
url = None # use default url generate mechanism url = None # use default url generate mechanism
path = os.path.join(STATIC_PATH, nondev_path)
register_panel(hass, component_name, os.path.join(STATIC_PATH, path), # Fingerprint doesn't exist when adding new built-in panel
FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path, register_panel(hass, component_name, path,
url, config) FINGERPRINTS.get(nondev_path, 'dev'), sidebar_title,
sidebar_icon, url_path, url, config)
def register_panel(hass, component_name, path, md5=None, sidebar_title=None, def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
@ -230,10 +235,14 @@ class IndexView(HomeAssistantView):
if request.app[KEY_DEVELOPMENT]: if request.app[KEY_DEVELOPMENT]:
core_url = '/static/home-assistant-polymer/build/core.js' core_url = '/static/home-assistant-polymer/build/core.js'
compatibility_url = \
'/static/home-assistant-polymer/build/compatibility.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html' ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else: else:
core_url = '/static/core-{}.js'.format( core_url = '/static/core-{}.js'.format(
FINGERPRINTS['core.js']) FINGERPRINTS['core.js'])
compatibility_url = '/static/compatibility-{}.js'.format(
FINGERPRINTS['compatibility.js'])
ui_url = '/static/frontend-{}.html'.format( ui_url = '/static/frontend-{}.html'.format(
FINGERPRINTS['frontend.html']) FINGERPRINTS['frontend.html'])
@ -263,7 +272,8 @@ class IndexView(HomeAssistantView):
# pylint: disable=no-member # pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'. # This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth, core_url=core_url, ui_url=ui_url,
compatibility_url=compatibility_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=hass.data[DATA_PANELS]) panel_url=panel_url, panels=hass.data[DATA_PANELS])

View File

@ -78,6 +78,16 @@
</div> </div>
<home-assistant icons='{{ icons }}'></home-assistant> <home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #} {# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script>
var compatibilityRequired = (
typeof Object.assign != 'function');
if (compatibilityRequired) {
var e = document.createElement('script');
e.onerror = initError;
e.src = '{{ compatibility_url }}';
document.head.appendChild(e);
}
</script>
<script src='{{ core_url }}'></script> <script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'> <link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url -%} {% if panel_url -%}

View File

@ -1,18 +1,20 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e", "core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
"frontend.html": "a179cbf60629f02e1d6f4b0034a783bf", "frontend.html": "be258a53166b82f4ebd5232037e1cbd5",
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668", "mdi.html": "c1dde43ccf5667f687c418fc8daf9668",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "5c82300b3cf543a92cf4297506e450e7", "panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
"panels/ha-panel-dev-info.html": "5f8119c8aa18cba51106e1bb8ca9cba2", "panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
"panels/ha-panel-dev-service.html": "306243e11766266c967d1163ca678fe6", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
"panels/ha-panel-dev-state.html": "7d069ba8fd5379fa8f59858b8c0a7473", "panels/ha-panel-dev-service.html": "a9247f255174b084fad2c04bdb9ec7a9",
"panels/ha-panel-dev-template.html": "219d66c469982b6d6ae015d21156ac82", "panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
"panels/ha-panel-history.html": "c5cf0aa3eba31f40231b0479b08913eb", "panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2", "panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
"panels/ha-panel-map.html": "ecd2ee7ff6e2837e67950d3ab1a6ec65", "panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450" "websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

View File

@ -0,0 +1 @@
!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o<arguments.length;o++){var i=t[o];if(void 0!==i&&null!==i)for(var l=Object.keys(Object(i)),a=0,c=l.length;a<c;a++){var b=l[a],f=Object.getOwnPropertyDescriptor(i,b);void 0!==f&&f.enumerable&&(n[b]=i[b])}}return n}function r(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}var t={assign:e,polyfill:r};t.polyfill()})();

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit f3808ff4d44733f5810e21131e0daa1425bf5f22 Subproject commit 5d223c8da4b4380bf79d8f0285ce7824063e89ef

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,19A5,5 0 0,1 1,14A5,5 0 0,1 6,9C7,6.65 9.3,5 12,5C15.43,5 18.24,7.66 18.5,11.03L19,11A4,4 0 0,1 23,15A4,4 0 0,1 19,19H6M19,13H17V12A5,5 0 0,0 12,7C9.5,7 7.45,8.82 7.06,11.19C6.73,11.07 6.37,11 6,11A3,3 0 0,0 3,14A3,3 0 0,0 6,17H19A2,2 0 0,0 21,15A2,2 0 0,0 19,13Z" /></svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,15H13A1,1 0 0,1 14,16A1,1 0 0,1 13,17H3A1,1 0 0,1 2,16A1,1 0 0,1 3,15M16,15H21A1,1 0 0,1 22,16A1,1 0 0,1 21,17H16A1,1 0 0,1 15,16A1,1 0 0,1 16,15M1,12A5,5 0 0,1 6,7C7,4.65 9.3,3 12,3C15.43,3 18.24,5.66 18.5,9.03L19,9C21.19,9 22.97,10.76 23,13H21A2,2 0 0,0 19,11H17V10A5,5 0 0,0 12,5C9.5,5 7.45,6.82 7.06,9.19C6.73,9.07 6.37,9 6,9A3,3 0 0,0 3,12C3,12.35 3.06,12.69 3.17,13H1.1L1,12M3,19H5A1,1 0 0,1 6,20A1,1 0 0,1 5,21H3A1,1 0 0,1 2,20A1,1 0 0,1 3,19M8,19H21A1,1 0 0,1 22,20A1,1 0 0,1 21,21H8A1,1 0 0,1 7,20A1,1 0 0,1 8,19Z" /></svg>

After

Width:  |  Height:  |  Size: 820 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M10,18A2,2 0 0,1 12,20A2,2 0 0,1 10,22A2,2 0 0,1 8,20A2,2 0 0,1 10,18M14.5,16A1.5,1.5 0 0,1 16,17.5A1.5,1.5 0 0,1 14.5,19A1.5,1.5 0 0,1 13,17.5A1.5,1.5 0 0,1 14.5,16M10.5,12A1.5,1.5 0 0,1 12,13.5A1.5,1.5 0 0,1 10.5,15A1.5,1.5 0 0,1 9,13.5A1.5,1.5 0 0,1 10.5,12Z" /></svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z" /></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12.74,5.47C15.1,6.5 16.35,9.03 15.92,11.46C17.19,12.56 18,14.19 18,16V16.17C18.31,16.06 18.65,16 19,16A3,3 0 0,1 22,19A3,3 0 0,1 19,22H6A4,4 0 0,1 2,18A4,4 0 0,1 6,14H6.27C5,12.45 4.6,10.24 5.5,8.26C6.72,5.5 9.97,4.24 12.74,5.47M11.93,7.3C10.16,6.5 8.09,7.31 7.31,9.07C6.85,10.09 6.93,11.22 7.41,12.13C8.5,10.83 10.16,10 12,10C12.7,10 13.38,10.12 14,10.34C13.94,9.06 13.18,7.86 11.93,7.3M13.55,3.64C13,3.4 12.45,3.23 11.88,3.12L14.37,1.82L15.27,4.71C14.76,4.29 14.19,3.93 13.55,3.64M6.09,4.44C5.6,4.79 5.17,5.19 4.8,5.63L4.91,2.82L7.87,3.5C7.25,3.71 6.65,4.03 6.09,4.44M18,9.71C17.91,9.12 17.78,8.55 17.59,8L19.97,9.5L17.92,11.73C18.03,11.08 18.05,10.4 18,9.71M3.04,11.3C3.11,11.9 3.24,12.47 3.43,13L1.06,11.5L3.1,9.28C3,9.93 2.97,10.61 3.04,11.3M19,18H16V16A4,4 0 0,0 12,12A4,4 0 0,0 8,16H6A2,2 0 0,0 4,18A2,2 0 0,0 6,20H19A1,1 0 0,0 20,19A1,1 0 0,0 19,18Z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9,12C9.53,12.14 9.85,12.69 9.71,13.22L8.41,18.05C8.27,18.59 7.72,18.9 7.19,18.76C6.65,18.62 6.34,18.07 6.5,17.54L7.78,12.71C7.92,12.17 8.47,11.86 9,12M13,12C13.53,12.14 13.85,12.69 13.71,13.22L11.64,20.95C11.5,21.5 10.95,21.8 10.41,21.66C9.88,21.5 9.56,20.97 9.7,20.43L11.78,12.71C11.92,12.17 12.47,11.86 13,12M17,12C17.53,12.14 17.85,12.69 17.71,13.22L16.41,18.05C16.27,18.59 15.72,18.9 15.19,18.76C14.65,18.62 14.34,18.07 14.5,17.54L15.78,12.71C15.92,12.17 16.47,11.86 17,12M17,10V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11C3,12.11 3.6,13.08 4.5,13.6V13.59C5,13.87 5.14,14.5 4.87,14.96C4.59,15.43 4,15.6 3.5,15.32V15.33C2,14.47 1,12.85 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12C23,13.5 22.2,14.77 21,15.46V15.46C20.5,15.73 19.91,15.57 19.63,15.09C19.36,14.61 19.5,14 20,13.72V13.73C20.6,13.39 21,12.74 21,12A2,2 0 0,0 19,10H17Z" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M14.83,15.67C16.39,17.23 16.39,19.5 14.83,21.08C14.05,21.86 13,22 12,22C11,22 9.95,21.86 9.17,21.08C7.61,19.5 7.61,17.23 9.17,15.67L12,11L14.83,15.67M13.41,16.69L12,14.25L10.59,16.69C9.8,17.5 9.8,18.7 10.59,19.5C11,19.93 11.5,20 12,20C12.5,20 13,19.93 13.41,19.5C14.2,18.7 14.2,17.5 13.41,16.69Z" /></svg>

After

Width:  |  Height:  |  Size: 905 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,14A1,1 0 0,1 7,15A1,1 0 0,1 6,16A5,5 0 0,1 1,11A5,5 0 0,1 6,6C7,3.65 9.3,2 12,2C15.43,2 18.24,4.66 18.5,8.03L19,8A4,4 0 0,1 23,12A4,4 0 0,1 19,16H18A1,1 0 0,1 17,15A1,1 0 0,1 18,14H19A2,2 0 0,0 21,12A2,2 0 0,0 19,10H17V9A5,5 0 0,0 12,4C9.5,4 7.45,5.82 7.06,8.19C6.73,8.07 6.37,8 6,8A3,3 0 0,0 3,11A3,3 0 0,0 6,14M7.88,18.07L10.07,17.5L8.46,15.88C8.07,15.5 8.07,14.86 8.46,14.46C8.85,14.07 9.5,14.07 9.88,14.46L11.5,16.07L12.07,13.88C12.21,13.34 12.76,13.03 13.29,13.17C13.83,13.31 14.14,13.86 14,14.4L13.41,16.59L15.6,16C16.14,15.86 16.69,16.17 16.83,16.71C16.97,17.24 16.66,17.79 16.12,17.93L13.93,18.5L15.54,20.12C15.93,20.5 15.93,21.15 15.54,21.54C15.15,21.93 14.5,21.93 14.12,21.54L12.5,19.93L11.93,22.12C11.79,22.66 11.24,22.97 10.71,22.83C10.17,22.69 9.86,22.14 10,21.6L10.59,19.41L8.4,20C7.86,20.14 7.31,19.83 7.17,19.29C7.03,18.76 7.34,18.21 7.88,18.07Z" /></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z" /></svg>

After

Width:  |  Height:  |  Size: 940 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M4,10A1,1 0 0,1 3,9A1,1 0 0,1 4,8H12A2,2 0 0,0 14,6A2,2 0 0,0 12,4C11.45,4 10.95,4.22 10.59,4.59C10.2,5 9.56,5 9.17,4.59C8.78,4.2 8.78,3.56 9.17,3.17C9.9,2.45 10.9,2 12,2A4,4 0 0,1 16,6A4,4 0 0,1 12,10H4M19,12A1,1 0 0,0 20,11A1,1 0 0,0 19,10C18.72,10 18.47,10.11 18.29,10.29C17.9,10.68 17.27,10.68 16.88,10.29C16.5,9.9 16.5,9.27 16.88,8.88C17.42,8.34 18.17,8 19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14H5A1,1 0 0,1 4,13A1,1 0 0,1 5,12H19M18,18H4A1,1 0 0,1 3,17A1,1 0 0,1 4,16H18A3,3 0 0,1 21,19A3,3 0 0,1 18,22C17.17,22 16.42,21.66 15.88,21.12C15.5,20.73 15.5,20.1 15.88,19.71C16.27,19.32 16.9,19.32 17.29,19.71C17.47,19.89 17.72,20 18,20A1,1 0 0,0 19,19A1,1 0 0,0 18,18Z" /></svg>

After

Width:  |  Height:  |  Size: 959 B

Some files were not shown because too many files have changed in this diff Show More