23
.coveragerc
|
@ -41,6 +41,9 @@ omit =
|
|||
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
|
||||
|
||||
|
@ -88,6 +91,9 @@ omit =
|
|||
homeassistant/components/verisure.py
|
||||
homeassistant/components/*/verisure.py
|
||||
|
||||
homeassistant/components/volvooncall.py
|
||||
homeassistant/components/*/volvooncall.py
|
||||
|
||||
homeassistant/components/*/webostv.py
|
||||
|
||||
homeassistant/components/wemo.py
|
||||
|
@ -149,10 +155,12 @@ omit =
|
|||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
homeassistant/components/climate/knx.py
|
||||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/myq.py
|
||||
homeassistant/components/cover/rpi_gpio.py
|
||||
homeassistant/components/cover/scsgate.py
|
||||
homeassistant/components/cover/wink.py
|
||||
|
@ -181,9 +189,7 @@ omit =
|
|||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/device_tracker/xiaomi.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/emulated_hue/upnp.py
|
||||
|
@ -207,6 +213,7 @@ omit =
|
|||
homeassistant/components/light/tikteck.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
|
@ -216,6 +223,7 @@ omit =
|
|||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/clementine.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
|
@ -224,6 +232,7 @@ omit =
|
|||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
homeassistant/components/media_player/gstreamer.py
|
||||
homeassistant/components/media_player/hdmi_cec.py
|
||||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
|
@ -233,6 +242,7 @@ omit =
|
|||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
homeassistant/components/media_player/onkyo.py
|
||||
homeassistant/components/media_player/openhome.py
|
||||
homeassistant/components/media_player/panasonic_viera.py
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
|
@ -267,6 +277,7 @@ omit =
|
|||
homeassistant/components/notify/pushbullet.py
|
||||
homeassistant/components/notify/pushetta.py
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/pushsafer.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
|
@ -281,6 +292,7 @@ omit =
|
|||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
|
@ -299,13 +311,17 @@ omit =
|
|||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
homeassistant/components/sensor/fedex.py
|
||||
homeassistant/components/sensor/fido.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
homeassistant/components/sensor/fixer.py
|
||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||
homeassistant/components/sensor/fritzbox_netmonitor.py
|
||||
homeassistant/components/sensor/glances.py
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
|
@ -334,6 +350,7 @@ omit =
|
|||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
|
@ -358,6 +375,7 @@ omit =
|
|||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/usps.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
|
@ -386,6 +404,7 @@ omit =
|
|||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/telegram_webhooks.py
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
homeassistant/components/tts/picotts.py
|
||||
|
|
|
@ -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**
|
||||
- [ ] 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-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51
|
||||
[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#L54
|
||||
|
|
|
@ -75,7 +75,8 @@ Build home automation on top of your devices:
|
|||
`Instapush <https://instapush.im>`__, `Notify My Android
|
||||
(NMA) <http://www.notifymyandroid.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
|
||||
(XMPP) <http://xmpp.org>`__
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import homeassistant.components as core_components
|
|||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.util.package as pkg_util
|
||||
from homeassistant.util.async import (
|
||||
|
@ -166,7 +167,7 @@ def _async_setup_component(hass: core.HomeAssistant,
|
|||
loader.set_component(domain, None)
|
||||
return False
|
||||
|
||||
hass.config.components.append(component.DOMAIN)
|
||||
hass.config.components.add(component.DOMAIN)
|
||||
|
||||
hass.bus.async_fire(
|
||||
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
|
||||
|
@ -298,6 +299,10 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
|||
|
||||
# Load 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)
|
||||
if not res:
|
||||
_LOGGER.error(
|
||||
|
@ -386,7 +391,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
None, conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
if enable_log:
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
|
@ -429,7 +434,13 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
service.HASS = hass
|
||||
|
||||
# Setup the components
|
||||
dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(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)
|
||||
|
||||
setup_lock.release()
|
||||
|
@ -488,7 +499,7 @@ def async_from_config_file(config_path: str,
|
|||
yield from hass.loop.run_in_executor(
|
||||
None, mount_local_lib_path, config_dir)
|
||||
|
||||
enable_logging(hass, verbose, log_rotate_days)
|
||||
async_enable_logging(hass, verbose, log_rotate_days)
|
||||
|
||||
try:
|
||||
config_dict = yield from hass.loop.run_in_executor(
|
||||
|
@ -503,11 +514,12 @@ def async_from_config_file(config_path: str,
|
|||
return hass
|
||||
|
||||
|
||||
def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
log_rotate_days=None) -> None:
|
||||
"""Setup the logging.
|
||||
|
||||
Async friendly.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
|
@ -537,10 +549,6 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
|||
except ImportError:
|
||||
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
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
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))
|
||||
|
||||
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.addHandler(async_handler)
|
||||
|
|
|
@ -156,10 +156,18 @@ def async_setup(hass, config):
|
|||
return
|
||||
|
||||
try:
|
||||
yield from conf_util.async_check_ha_config_file(hass)
|
||||
errors = yield from conf_util.async_check_ha_config_file(hass)
|
||||
except HomeAssistantError:
|
||||
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:
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
|
|
|
@ -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
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
from os import path
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
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.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.envisalink import (
|
||||
EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
|
||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
|
@ -22,8 +26,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
|
||||
DEVICES = []
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
|
@ -32,68 +34,72 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
def alarm_keypress_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
_target_devices = [device for device in DEVICES
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in _target_devices:
|
||||
EnvisalinkAlarm.alarm_keypress(device, keypress)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@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]
|
||||
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)
|
||||
configured_partitions = discovery_info['partitions']
|
||||
code = discovery_info[CONF_CODE]
|
||||
panic_type = discovery_info[CONF_PANIC]
|
||||
|
||||
add_devices(DEVICES)
|
||||
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):
|
||||
"""Map services to methods on Alarm."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
target_devices = [device for device in devices
|
||||
if device.entity_id in entity_ids]
|
||||
|
||||
for device in target_devices:
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
# Register Envisalink specific services
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
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
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation of an Envisalink-based alarm panel."""
|
||||
|
||||
def __init__(self, partition_number, alarm_name, code, panic_type, info,
|
||||
controller):
|
||||
def __init__(self, hass, partition_number, alarm_name, code, panic_type,
|
||||
info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
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):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
|
@ -126,39 +132,44 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.disarm_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
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."""
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
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."""
|
||||
if code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
else:
|
||||
EVL_CONTROLLER.arm_away_partition(str(self._code),
|
||||
self._partition_number)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
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."""
|
||||
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."""
|
||||
if keypress:
|
||||
EVL_CONTROLLER.keypresses_to_partition(self._partition_number,
|
||||
keypress)
|
||||
self.hass.data[DATA_EVL].keypresses_to_partition(
|
||||
self._partition_number, keypress)
|
||||
|
|
|
@ -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
|
||||
https://home-assistant.io/components/alarm_control_panel.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
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."""
|
||||
add_devices([MqttAlarm(
|
||||
hass,
|
||||
yield from async_add_devices([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
@ -58,11 +60,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MqttAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code):
|
||||
def __init__(self, name, state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, code):
|
||||
"""Initalize the MQTT alarm panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
|
@ -72,6 +73,12 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
|||
self._payload_arm_away = payload_arm_away
|
||||
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):
|
||||
"""A new MQTT message has been received."""
|
||||
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)
|
||||
return
|
||||
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
|
||||
def should_poll(self):
|
||||
|
@ -104,26 +112,38 @@ class MqttAlarm(alarm.AlarmControlPanel):
|
|||
"""One or more characters if code is defined."""
|
||||
return None if self._code is None else '.+'
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_disarm, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@asyncio.coroutine
|
||||
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'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_home, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@asyncio.coroutine
|
||||
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'):
|
||||
return
|
||||
mqtt.publish(self.hass, self._command_topic,
|
||||
self._payload_arm_away, self._qos)
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
|
|
@ -12,16 +12,19 @@ import homeassistant.components.alarm_control_panel as alarm
|
|||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
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.loader as loader
|
||||
|
||||
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
|
||||
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
|
||||
'simplisafe-python==0.0.1']
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DOMAIN = 'simplisafe'
|
||||
NOTIFICATION_ID = 'simplisafe_notification'
|
||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, get_systems
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
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):
|
||||
"""Representation a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, name, username, password, code):
|
||||
def __init__(self, simplisafe, name, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
from simplisafe import SimpliSafe
|
||||
self.simplisafe = SimpliSafe(username, password)
|
||||
self.simplisafe = simplisafe
|
||||
self._name = name
|
||||
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
|
||||
def name(self):
|
||||
|
@ -67,7 +81,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self._id)
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
@ -77,21 +91,32 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def state(self):
|
||||
"""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):
|
||||
"""Update alarm status."""
|
||||
self.simplisafe.get_location()
|
||||
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
|
||||
self.simplisafe.update()
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
|
@ -13,10 +13,9 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME,
|
||||
CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
@ -32,13 +31,16 @@ CONF_NOTIFIERS = 'notifiers'
|
|||
CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
|
||||
DEFAULT_CAN_ACK = True
|
||||
DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
vol.Required(CONF_CAN_ACK, default=True): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=False): cv.boolean,
|
||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
@ -60,7 +62,8 @@ def is_on(hass, entity_id):
|
|||
|
||||
def turn_on(hass, entity_id):
|
||||
"""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
|
||||
|
@ -73,7 +76,8 @@ def async_turn_on(hass, entity_id):
|
|||
|
||||
def turn_off(hass, entity_id):
|
||||
"""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
|
||||
|
@ -99,7 +103,7 @@ def async_toggle(hass, entity_id):
|
|||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup alert component."""
|
||||
"""Set up the Alert component."""
|
||||
alerts = config.get(DOMAIN)
|
||||
all_alerts = {}
|
||||
|
||||
|
@ -117,7 +121,7 @@ def async_setup(hass, config):
|
|||
else:
|
||||
yield from alert.async_turn_off()
|
||||
|
||||
# setup alerts
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
|
@ -126,13 +130,13 @@ def async_setup(hass, config):
|
|||
alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# read descriptions
|
||||
# Read descriptions
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = descriptions.get(DOMAIN, {})
|
||||
|
||||
# setup service calls
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
|
||||
|
@ -171,8 +175,8 @@ class Alert(ToggleEntity):
|
|||
self._cancel = None
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(hass, watched_entity_id,
|
||||
self.watched_entity_change)
|
||||
event.async_track_state_change(
|
||||
hass, watched_entity_id, self.watched_entity_change)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -201,7 +205,7 @@ class Alert(ToggleEntity):
|
|||
@asyncio.coroutine
|
||||
def watched_entity_change(self, entity, from_state, to_state):
|
||||
"""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:
|
||||
yield from self.begin_alerting()
|
||||
if to_state.state != self._alert_state and self._firing:
|
||||
|
@ -210,7 +214,7 @@ class Alert(ToggleEntity):
|
|||
@asyncio.coroutine
|
||||
def begin_alerting(self):
|
||||
"""Begin the alert procedures."""
|
||||
_LOGGER.debug('Beginning Alert: %s', self._name)
|
||||
_LOGGER.debug("Beginning Alert: %s", self._name)
|
||||
self._ack = False
|
||||
self._firing = True
|
||||
self._next_delay = 0
|
||||
|
@ -225,7 +229,7 @@ class Alert(ToggleEntity):
|
|||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
"""End the alert procedures."""
|
||||
_LOGGER.debug('Ending Alert: %s', self._name)
|
||||
_LOGGER.debug("Ending Alert: %s", self._name)
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
|
@ -247,7 +251,7 @@ class Alert(ToggleEntity):
|
|||
return
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info('Alerting: %s', self._name)
|
||||
_LOGGER.info("Alerting: %s", self._name)
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
|
@ -256,14 +260,14 @@ class Alert(ToggleEntity):
|
|||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
"""Async Unacknowledge alert."""
|
||||
_LOGGER.debug('Reset Alert: %s', self._name)
|
||||
_LOGGER.debug("Reset Alert: %s", self._name)
|
||||
self._ack = False
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug('Acknowledged Alert: %s', self._name)
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
|
|
|
@ -77,14 +77,14 @@ class ApiaiIntentsView(HomeAssistantView):
|
|||
"""Handle API.AI."""
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Apiai request: %s', data)
|
||||
_LOGGER.debug("Received api.ai request: %s", data)
|
||||
|
||||
req = data.get('result')
|
||||
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Apiai: %s', data)
|
||||
return self.json_message('Expected result value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
_LOGGER.error("Received invalid data from api.ai: %s", data)
|
||||
return self.json_message(
|
||||
"Expected result value not received", HTTP_BAD_REQUEST)
|
||||
|
||||
action_incomplete = req['actionIncomplete']
|
||||
|
||||
|
@ -106,7 +106,7 @@ class ApiaiIntentsView(HomeAssistantView):
|
|||
# return self.json(response)
|
||||
|
||||
if intent == "":
|
||||
_LOGGER.warning('Received intent with empty action')
|
||||
_LOGGER.warning("Received intent with empty action")
|
||||
response.add_speech(
|
||||
"You have not defined an action in your api.ai intent.")
|
||||
return self.json(response)
|
||||
|
@ -114,7 +114,7 @@ class ApiaiIntentsView(HomeAssistantView):
|
|||
config = self.intents.get(intent)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent)
|
||||
_LOGGER.warning("Received unknown intent %s", intent)
|
||||
response.add_speech(
|
||||
"Intent '%s' is not yet configured within Home Assistant." %
|
||||
intent)
|
||||
|
|
|
@ -412,7 +412,7 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
|
|||
if platform is None:
|
||||
return None
|
||||
|
||||
remove = platform.async_trigger(hass, conf, action)
|
||||
remove = yield from platform.async_trigger(hass, conf, action)
|
||||
|
||||
if not remove:
|
||||
_LOGGER.error("Error setting up trigger %s", name)
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer event listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#event-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
|
|
|
@ -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
|
||||
https://home-assistant.io/components/automation.litejet/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -32,6 +33,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
number = config.get(CONF_NUMBER)
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer MQTT listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -24,6 +25,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
topic = config.get(CONF_TOPIC)
|
||||
|
@ -49,4 +51,6 @@ def async_trigger(hass, config, action):
|
|||
'trigger': data
|
||||
})
|
||||
|
||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
remove = yield from mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener)
|
||||
return remove
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer numeric state listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#numeric-state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer state listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#state-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
@ -34,6 +35,7 @@ TRIGGER_SCHEMA = vol.All(
|
|||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
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_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
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
|
@ -64,18 +79,11 @@ def async_trigger(hass, config, action):
|
|||
call_action()
|
||||
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
|
||||
def state_for_listener(now):
|
||||
"""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()
|
||||
call_action()
|
||||
|
||||
|
@ -84,10 +92,11 @@ def async_trigger(hass, config, action):
|
|||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_cancel()
|
||||
clear_listener()
|
||||
|
||||
# cleanup previous listener
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
|
@ -97,14 +106,10 @@ def async_trigger(hass, config, action):
|
|||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
# pylint: disable=not-callable
|
||||
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()
|
||||
clear_listener()
|
||||
|
||||
return async_remove
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer sun based automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#sun-trigger
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event = config.get(CONF_EVENT)
|
||||
|
|
|
@ -4,14 +4,14 @@ Offer template automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#template-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import async_track_template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
|
@ -23,33 +23,22 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
|
||||
# Local variable to keep track of if the action has already been triggered
|
||||
already_triggered = False
|
||||
|
||||
@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."""
|
||||
nonlocal already_triggered
|
||||
template_result = condition.async_template(hass, value_template)
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
|
||||
# Check to see if template returns true
|
||||
if template_result and not already_triggered:
|
||||
already_triggered = True
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
},
|
||||
})
|
||||
elif not template_result:
|
||||
already_triggered = False
|
||||
|
||||
return async_track_state_change(hass, value_template.extract_entities(),
|
||||
state_changed_listener)
|
||||
return async_track_template(hass, value_template, template_listener)
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer time listening automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#time-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -29,6 +30,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||
CONF_SECONDS, CONF_AFTER))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
if CONF_AFTER in config:
|
||||
|
|
|
@ -4,6 +4,7 @@ Offer zone automation rules.
|
|||
For more details about this automation rule, please refer to the documentation
|
||||
at https://home-assistant.io/components/automation/#zone-trigger
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
@ -26,6 +27,7 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for state changes based on configuration."""
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
|
|
@ -14,13 +14,13 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.deprecation import deprecated_substitute
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
SENSOR_CLASSES = [
|
||||
None, # Generic on/off
|
||||
DEVICE_CLASSES = [
|
||||
'cold', # On means cold (or too cold)
|
||||
'connectivity', # On means connection present, Off = no connection
|
||||
'gas', # CO, CO2, etc.
|
||||
|
@ -38,7 +38,7 @@ SENSOR_CLASSES = [
|
|||
'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
|
||||
|
@ -66,16 +66,7 @@ class BinarySensorDevice(Entity):
|
|||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
@deprecated_substitute('sensor_class')
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
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
|
||||
|
|
|
@ -11,11 +11,12 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -25,7 +26,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_NAME): 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."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS)
|
||||
|
||||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
|
@ -49,18 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
add_devices([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
sensor_class, pin)])
|
||||
device_class, pin)])
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
self.arest = arest
|
||||
self._resource = resource
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._pin = pin
|
||||
self.update()
|
||||
|
||||
|
@ -81,9 +83,9 @@ class ArestBinarySensor(BinarySensorDevice):
|
|||
return bool(self.arest.data.get('state'))
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from aREST API."""
|
||||
|
|
|
@ -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]
|
|
@ -64,8 +64,8 @@ class BloomSkySensor(BinarySensorDevice):
|
|||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
|
|
|
@ -10,12 +10,13 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
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.const import (
|
||||
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
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_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_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): 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,
|
||||
})
|
||||
|
||||
|
@ -42,27 +44,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
command = config.get(CONF_COMMAND)
|
||||
payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
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)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(command)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass, data, name, sensor_class, payload_on, payload_off,
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
value_template)])
|
||||
|
||||
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""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):
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
|
@ -80,9 +82,9 @@ class CommandBinarySensor(BinarySensorDevice):
|
|||
return self._state
|
||||
|
||||
@ property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
|
|
|
@ -11,7 +11,7 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -30,7 +30,7 @@ DEFAULT_SSL = False
|
|||
SCAN_INTERVAL = datetime.timedelta(seconds=1)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -102,8 +102,8 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
|||
self.update()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
|
|
|
@ -18,14 +18,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class DemoBinarySensor(BinarySensorDevice):
|
||||
"""A Demo binary sensor."""
|
||||
|
||||
def __init__(self, name, state, sensor_class):
|
||||
def __init__(self, name, state, device_class):
|
||||
"""Initialize the demo sensor."""
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._sensor_type = sensor_class
|
||||
self._sensor_type = device_class
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
|||
return self.data.status == 'active'
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
self.sensor_name = sensor_name
|
||||
self.index = sensor_index
|
||||
self._state = None
|
||||
self._sensor_class = 'occupancy'
|
||||
self._device_class = 'occupancy'
|
||||
self.update()
|
||||
|
||||
@property
|
||||
|
@ -57,9 +57,9 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._sensor_class
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
|
|
|
@ -9,10 +9,12 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
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.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
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,7 +24,8 @@ DEFAULT_NAME = 'EnOcean binary sensor'
|
|||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
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."""
|
||||
dev_id = config.get(CONF_ID)
|
||||
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):
|
||||
"""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."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "listener"
|
||||
|
@ -46,7 +49,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
|||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -54,9 +57,9 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
|||
return self.devname
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
"""Fire an event with the data that have changed.
|
||||
|
|
|
@ -4,48 +4,56 @@ Support for Envisalink zone states- represented as binary sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
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.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_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."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
configured_zones = discovery_info['zones']
|
||||
|
||||
devices = []
|
||||
for zone_num in configured_zones:
|
||||
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
device = EnvisalinkBinarySensor(
|
||||
hass,
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
device_config_data[CONF_ZONENAME],
|
||||
device_config_data[CONF_ZONETYPE],
|
||||
hass.data[DATA_EVL].alarm_state['zone'][zone_num],
|
||||
hass.data[DATA_EVL]
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
yield from async_add_devices(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""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."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -60,11 +68,12 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
|||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
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())
|
||||
|
|
|
@ -122,6 +122,6 @@ class FFmpegMotion(FFmpegBinarySensor):
|
|||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return "motion"
|
||||
|
|
|
@ -91,6 +91,6 @@ class FFmpegNoise(FFmpegBinarySensor):
|
|||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return "sound"
|
||||
|
|
|
@ -179,12 +179,9 @@ class FlicButton(BinarySensorDevice):
|
|||
return False
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = super(FlicButton, self).state_attributes
|
||||
attr["address"] = self.address
|
||||
|
||||
return attr
|
||||
return {"address": self.address}
|
||||
|
||||
def _queued_event_check(self, click_type, time_diff):
|
||||
"""Generate a log message and returns true if timeout exceeded."""
|
||||
|
|
|
@ -29,7 +29,7 @@ DEFAULT_DELAY = 0
|
|||
|
||||
ATTR_DELAY = 'delay'
|
||||
|
||||
SENSOR_CLASS_MAP = {
|
||||
DEVICE_CLASS_MAP = {
|
||||
'Motion': 'motion',
|
||||
'Line Crossing': 'motion',
|
||||
'IO Trigger': None,
|
||||
|
@ -201,10 +201,10 @@ class HikvisionBinarySensor(BinarySensorDevice):
|
|||
return self._sensor_state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
try:
|
||||
return SENSOR_CLASS_MAP[self._sensor]
|
||||
return DEVICE_CLASS_MAP[self._sensor]
|
||||
except KeyError:
|
||||
# Sensor must be unknown to us, add as generic
|
||||
return None
|
||||
|
|
|
@ -7,8 +7,7 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
|||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
|
||||
_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."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
|
@ -54,11 +53,8 @@ class HMBinarySensor(HMDevice, BinarySensorDevice):
|
|||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
if not self.available:
|
||||
return None
|
||||
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
# If state is MOTION (RemoteMotion works only)
|
||||
if self._state == "MOTION":
|
||||
return "motion"
|
||||
|
|
|
@ -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())
|
|
@ -26,7 +26,7 @@ ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
|
|||
CONF_SHOW_ON_MAP = 'show_on_map'
|
||||
|
||||
DEFAULT_NAME = 'ISS'
|
||||
DEFAULT_SENSOR_CLASS = 'visible'
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_SENSOR_CLASS
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for MQTT binary sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -11,12 +12,13 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, SENSOR_CLASSES)
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_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_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None):
|
||||
vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)),
|
||||
vol.Optional(CONF_SENSOR_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT binary sensor."""
|
||||
if discovery_info is not None:
|
||||
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)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttBinarySensor(
|
||||
hass,
|
||||
|
||||
yield from async_add_devices([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
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_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
|
@ -58,32 +60,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MqttBinarySensor(BinarySensorDevice):
|
||||
"""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):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._state_topic = state_topic
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
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
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
self._state = True
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._payload_off:
|
||||
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
|
||||
def should_poll(self):
|
||||
|
@ -101,6 +109,6 @@ class MqttBinarySensor(BinarySensorDevice):
|
|||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.mysensors/
|
|||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.components.binary_sensor import (SENSOR_CLASSES,
|
||||
from homeassistant.components.binary_sensor import (DEVICE_CLASSES,
|
||||
BinarySensorDevice)
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
|
@ -62,8 +62,8 @@ class MySensorsBinarySensor(
|
|||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
pres = self.gateway.const.Presentation
|
||||
class_map = {
|
||||
pres.S_DOOR: 'opening',
|
||||
|
@ -78,5 +78,5 @@ class MySensorsBinarySensor(
|
|||
pres.S_VIBRATION: 'vibration',
|
||||
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)
|
||||
|
|
|
@ -154,8 +154,8 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
|||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
if self._cameratype == "NACamera":
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == "NOC":
|
||||
|
|
|
@ -12,7 +12,7 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
DEVICE_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -28,7 +28,7 @@ DEFAULT_PORT = '5007'
|
|||
DEFAULT_SSL = False
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -85,8 +85,8 @@ class NX584ZoneSensor(BinarySensorDevice):
|
|||
self._zone_type = zone_type
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
|
|
|
@ -99,8 +99,8 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
|||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
|
|
|
@ -10,14 +10,15 @@ import voluptuous as vol
|
|||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
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.const import (
|
||||
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
|
||||
CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
|
||||
HTTP_DIGEST_AUTHENTICATION)
|
||||
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_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_PASSWORD): 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_VALUE_TEMPLATE): cv.template,
|
||||
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)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
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)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
@ -72,18 +74,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, name, sensor_class, value_template)])
|
||||
hass, rest, name, device_class, value_template)])
|
||||
|
||||
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
"""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."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._state = False
|
||||
self._previous_data = None
|
||||
self._value_template = value_template
|
||||
|
@ -95,9 +97,9 @@ class RestBinarySensor(BinarySensorDevice):
|
|||
return self._name
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -42,7 +42,7 @@ class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
|
|||
return self._state is True
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return "occupancy"
|
||||
|
||||
|
|
|
@ -12,14 +12,15 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,7 +28,8 @@ SENSOR_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
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({
|
||||
|
@ -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
|
||||
value_template.extract_entities())
|
||||
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:
|
||||
value_template.hass = hass
|
||||
|
@ -55,7 +58,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
sensor_class,
|
||||
device_class,
|
||||
value_template,
|
||||
entity_ids)
|
||||
)
|
||||
|
@ -70,14 +73,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
class BinarySensorTemplate(BinarySensorDevice):
|
||||
"""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):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
|
@ -100,9 +103,9 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
|
@ -11,11 +11,12 @@ import voluptuous as vol
|
|||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -37,7 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
|
||||
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
|
||||
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)
|
||||
threshold = config.get(CONF_THRESHOLD)
|
||||
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(
|
||||
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class)], True)
|
||||
device_class)], True)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -60,14 +62,14 @@ class ThresholdSensor(BinarySensorDevice):
|
|||
"""Representation of a Threshold sensor."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, threshold, limit_type,
|
||||
sensor_class):
|
||||
device_class):
|
||||
"""Initialize the Threshold sensor."""
|
||||
self._hass = hass
|
||||
self._entity_id = entity_id
|
||||
self.is_upper = limit_type == 'upper'
|
||||
self._name = name
|
||||
self._threshold = threshold
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._deviation = False
|
||||
self.sensor_value = 0
|
||||
|
||||
|
@ -105,9 +107,9 @@ class ThresholdSensor(BinarySensorDevice):
|
|||
return False
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
|
|
@ -15,12 +15,14 @@ from homeassistant.components.binary_sensor import (
|
|||
BinarySensorDevice,
|
||||
ENTITY_ID_FORMAT,
|
||||
PLATFORM_SCHEMA,
|
||||
SENSOR_CLASSES_SCHEMA)
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_SENSOR_CLASS,
|
||||
CONF_DEVICE_CLASS,
|
||||
STATE_UNKNOWN,)
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
||||
|
@ -34,8 +36,8 @@ SENSOR_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
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({
|
||||
|
@ -52,7 +54,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
entity_id = device_config[ATTR_ENTITY_ID]
|
||||
attribute = device_config.get(CONF_ATTRIBUTE)
|
||||
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]
|
||||
|
||||
sensors.append(
|
||||
|
@ -62,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
friendly_name,
|
||||
entity_id,
|
||||
attribute,
|
||||
sensor_class,
|
||||
device_class,
|
||||
invert)
|
||||
)
|
||||
if not sensors:
|
||||
|
@ -76,7 +79,7 @@ class SensorTrend(BinarySensorDevice):
|
|||
"""Representation of a trend Sensor."""
|
||||
|
||||
def __init__(self, hass, device_id, friendly_name,
|
||||
target_entity, attribute, sensor_class, invert):
|
||||
target_entity, attribute, device_class, invert):
|
||||
"""Initialize the sensor."""
|
||||
self._hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||
|
@ -84,7 +87,7 @@ class SensorTrend(BinarySensorDevice):
|
|||
self._name = friendly_name
|
||||
self._target_entity = target_entity
|
||||
self._attribute = attribute
|
||||
self._sensor_class = sensor_class
|
||||
self._device_class = device_class
|
||||
self._invert = invert
|
||||
self._state = None
|
||||
self.from_state = None
|
||||
|
@ -111,9 +114,9 @@ class SensorTrend(BinarySensorDevice):
|
|||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._sensor_class
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
|
@ -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'
|
|
@ -92,13 +92,13 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
try:
|
||||
if hasattr(self.wink, 'unit'):
|
||||
self._unit_of_measurement = self.wink.unit()
|
||||
except AttributeError:
|
||||
else:
|
||||
self._unit_of_measurement = None
|
||||
try:
|
||||
if hasattr(self.wink, 'capability'):
|
||||
self.capability = self.wink.capability()
|
||||
except AttributeError:
|
||||
else:
|
||||
self.capability = None
|
||||
|
||||
@property
|
||||
|
@ -107,8 +107,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
|
||||
|
@ -161,8 +161,8 @@ class WinkRemote(WinkBinarySensorDevice):
|
|||
}
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -48,19 +48,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, sensor_class):
|
||||
def __init__(self, value, device_class):
|
||||
"""Initialize the sensor."""
|
||||
self._sensor_type = sensor_class
|
||||
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
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._value.data
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
|
@ -72,34 +77,31 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
|||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""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."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, sensor_class)
|
||||
super(ZWaveTriggerSensor, self).__init__(value, device_class)
|
||||
self._hass = hass
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
# If it's active make sure that we set the timeout tracker
|
||||
if value.data:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
def value_changed(self, value):
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
if self._value.value_id == value.value_id:
|
||||
self.schedule_update_ha_state()
|
||||
if value.data:
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
self._state = self._value.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
self.invalidate_after = dt_util.utcnow() + datetime.timedelta(
|
||||
seconds=self.re_arm_sec)
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self.invalidate_after)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""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 > dt_util.utcnow())
|
||||
|
|
|
@ -118,12 +118,13 @@ class GenericCamera(Camera):
|
|||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
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)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
self.hass.async_add_job(response.release())
|
||||
yield from response.release()
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
|
|
@ -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)
|
|
@ -6,13 +6,14 @@ https://home-assistant.io/components/climate.homematic/
|
|||
"""
|
||||
import logging
|
||||
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.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MANUAL = "manual"
|
||||
STATE_BOOST = "boost"
|
||||
|
||||
|
@ -22,21 +23,31 @@ HM_STATE_MAP = {
|
|||
"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."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
|
@ -50,7 +61,7 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.available:
|
||||
if HM_CONTROL_MODE not in self._data:
|
||||
return None
|
||||
|
||||
# read state and search
|
||||
|
@ -62,8 +73,6 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
if not self.available:
|
||||
return None
|
||||
op_list = []
|
||||
|
||||
# generate list
|
||||
|
@ -76,31 +85,29 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('ACTUAL_HUMIDITY', None)
|
||||
for node in HM_HUMI_MAP:
|
||||
if node in self._data:
|
||||
return self._data[node]
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('ACTUAL_TEMPERATURE', None)
|
||||
for node in HM_TEMP_MAP:
|
||||
if node in self._data:
|
||||
return self._data[node]
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature."""
|
||||
if not self.available:
|
||||
return None
|
||||
return self._data.get('SET_TEMPERATURE', None)
|
||||
return self._data.get(self._state)
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self.available or temperature is None:
|
||||
if temperature is None:
|
||||
return None
|
||||
|
||||
self._hmdevice.set_temperature(temperature)
|
||||
self._hmdevice.writeNodeData(self._state, float(temperature))
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
|
@ -122,10 +129,12 @@ class HMThermostat(HMDevice, ClimateDevice):
|
|||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
# Add state to data dict
|
||||
self._data.update({"CONTROL_MODE": STATE_UNKNOWN,
|
||||
"SET_TEMPERATURE": STATE_UNKNOWN,
|
||||
"ACTUAL_TEMPERATURE": STATE_UNKNOWN})
|
||||
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
|
||||
self._data[self._state] = STATE_UNKNOWN
|
||||
|
||||
# support humidity
|
||||
if 'ACTUAL_HUMIDITY' in self._hmdevice.SENSORNODE:
|
||||
self._data.update({'ACTUAL_HUMIDITY': STATE_UNKNOWN})
|
||||
# support state
|
||||
if HM_CONTROL_MODE in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data[HM_CONTROL_MODE] = STATE_UNKNOWN
|
||||
|
||||
for node in self._hmdevice.SENSORNODE.keys():
|
||||
self._data[node] = STATE_UNKNOWN
|
||||
|
|
|
@ -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
|
|
@ -68,7 +68,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
self._unit = temp_unit
|
||||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
self.update_properties()
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
|
@ -79,6 +78,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
" workaround")
|
||||
self._zxt_120 = 1
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
|
|
|
@ -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))
|
|
@ -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,
|
||||
})
|
|
@ -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
|
|
@ -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"})
|
|
@ -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
|
|
@ -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
|
||||
the user has submitted configuration information.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
_INSTANCES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_REQUESTS = {}
|
||||
_KEY_INSTANCE = 'configurator'
|
||||
|
||||
ATTR_CONFIGURE_ID = 'configure_id'
|
||||
ATTR_DESCRIPTION = 'description'
|
||||
|
@ -72,22 +73,20 @@ def request_done(request_id):
|
|||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the configurator component."""
|
||||
return True
|
||||
|
||||
|
||||
def _get_instance(hass):
|
||||
"""Get an instance per hass object."""
|
||||
try:
|
||||
return _INSTANCES[hass]
|
||||
except KeyError:
|
||||
_INSTANCES[hass] = Configurator(hass)
|
||||
instance = hass.data.get(_KEY_INSTANCE)
|
||||
|
||||
if DOMAIN not in hass.config.components:
|
||||
hass.config.components.append(DOMAIN)
|
||||
if instance is None:
|
||||
instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
|
||||
|
||||
return _INSTANCES[hass]
|
||||
return instance
|
||||
|
||||
|
||||
class Configurator(object):
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.14.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.15.0']
|
||||
|
||||
ATTR_TEXT = 'text'
|
||||
|
||||
|
|
|
@ -33,6 +33,20 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
|
|||
|
||||
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__)
|
||||
|
||||
ATTR_CURRENT_POSITION = 'current_position'
|
||||
|
@ -221,6 +235,21 @@ class CoverDevice(Entity):
|
|||
|
||||
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
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
|
|
|
@ -4,7 +4,8 @@ Demo platform for the cover component.
|
|||
For more details about this platform, please refer to the documentation
|
||||
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
|
||||
|
||||
|
||||
|
@ -14,6 +15,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
DemoCover(hass, 'Kitchen Window'),
|
||||
DemoCover(hass, 'Hall Window', 10),
|
||||
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."""
|
||||
|
||||
# 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."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._position = position
|
||||
self._device_class = device_class
|
||||
self._supported_features = supported_features
|
||||
self._set_position = None
|
||||
self._set_tilt_position = None
|
||||
self._tilt_position = tilt_position
|
||||
|
@ -33,6 +39,10 @@ class DemoCover(CoverDevice):
|
|||
self._closing_tilt = True
|
||||
self._unsub_listener_cover = None
|
||||
self._unsub_listener_cover_tilt = None
|
||||
if position is None:
|
||||
self._closed = True
|
||||
else:
|
||||
self._closed = self.current_cover_position <= 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -57,17 +67,28 @@ class DemoCover(CoverDevice):
|
|||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self._position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self._closed
|
||||
|
||||
@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:
|
||||
return None
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""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
|
||||
|
||||
self._listen_cover()
|
||||
|
@ -83,7 +104,11 @@ class DemoCover(CoverDevice):
|
|||
|
||||
def open_cover(self, **kwargs):
|
||||
"""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
|
||||
|
||||
self._listen_cover()
|
||||
|
@ -149,6 +174,9 @@ class DemoCover(CoverDevice):
|
|||
|
||||
if self._position in (100, 0, self._set_position):
|
||||
self.stop_cover()
|
||||
|
||||
self._closed = self.current_cover_position <= 0
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _listen_cover_tilt(self):
|
||||
|
|
|
@ -168,6 +168,11 @@ class GaradgetCover(CoverDevice):
|
|||
else:
|
||||
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):
|
||||
"""Get new token for usage during this session."""
|
||||
args = {
|
||||
|
|
|
@ -10,28 +10,26 @@ properly configured.
|
|||
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_POSITION
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMCover,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
|
@ -44,18 +42,15 @@ class HMCover(HMDevice, CoverDevice):
|
|||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
if self.available:
|
||||
return int(self._hm_get_state() * 100)
|
||||
return None
|
||||
return int(self._hm_get_state() * 100)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if self.available:
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = position / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = float(kwargs[ATTR_POSITION])
|
||||
position = min(100, max(0, position))
|
||||
level = position / 100.0
|
||||
self._hmdevice.set_level(level, self._channel)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -68,18 +63,15 @@ class HMCover(HMDevice, CoverDevice):
|
|||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if self.available:
|
||||
self._hmdevice.move_up(self._channel)
|
||||
self._hmdevice.move_up(self._channel)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if self.available:
|
||||
self._hmdevice.move_down(self._channel)
|
||||
self._hmdevice.move_down(self._channel)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device if in motion."""
|
||||
if self.available:
|
||||
self._hmdevice.stop(self._channel)
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from hm metadata."""
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for MQTT cover devices.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
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."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
add_devices([MqttCover(
|
||||
hass,
|
||||
|
||||
yield from async_add_devices([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
@ -71,13 +73,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MqttCover(CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||
retain, state_open, state_closed, payload_open, payload_close,
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
state_open, state_closed, payload_open, payload_close,
|
||||
payload_stop, optimistic, value_template):
|
||||
"""Initialize the cover."""
|
||||
self._position = None
|
||||
self._state = None
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
|
@ -89,37 +90,45 @@ class MqttCover(CoverDevice):
|
|||
self._state_closed = state_closed
|
||||
self._retain = retain
|
||||
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
|
||||
def message_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
|
||||
if payload == self._state_open:
|
||||
self._state = False
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload == self._state_closed:
|
||||
self._state = True
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
elif payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||
if int(payload) > 0:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self._position = int(payload)
|
||||
hass.async_add_job(self.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Payload is not True, False, or integer (0-100): %s",
|
||||
payload)
|
||||
return
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
if self._state_topic is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
else:
|
||||
mqtt.subscribe(hass, self._state_topic, message_received,
|
||||
self._qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -144,25 +153,40 @@ class MqttCover(CoverDevice):
|
|||
"""
|
||||
return self._position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_open,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_open, self._qos,
|
||||
self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_close,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_close, self._qos,
|
||||
self._retain)
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
mqtt.publish(self.hass, self._command_topic, self._payload_stop,
|
||||
self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Stop the device.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_stop, self._qos,
|
||||
self._retain)
|
||||
|
|
|
@ -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)
|
|
@ -7,22 +7,17 @@ https://home-assistant.io/components/cover.zwave/
|
|||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
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 import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
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__)
|
||||
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Find and return Z-Wave covers."""
|
||||
|
@ -58,16 +53,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
self._open_id = None
|
||||
self._close_id = 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:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND:
|
||||
_LOGGER.debug("Controller without positioning feedback")
|
||||
self._workaround = 1
|
||||
self._workaround = workaround.get_device_mapping(value)
|
||||
if self._workaround:
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
|
@ -81,6 +71,8 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
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
|
||||
def is_closed(self):
|
||||
|
@ -95,14 +87,15 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of Zwave roller shutter."""
|
||||
if not self._workaround:
|
||||
if self._current_position is not None:
|
||||
if self._current_position <= 5:
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
if self._workaround == workaround.WORKAROUND_NO_POSITION:
|
||||
return None
|
||||
if self._current_position is not None:
|
||||
if self._current_position <= 5:
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
|
@ -127,11 +120,16 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
def __init__(self, value):
|
||||
"""Initialize the zwave garage door."""
|
||||
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
|
||||
def is_closed(self):
|
||||
"""Return the current position of Zwave garage door."""
|
||||
return not self._value.data
|
||||
return not self._state
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
|
@ -140,3 +138,13 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
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
|
||||
|
|
|
@ -25,6 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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)
|
||||
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_tasks = [device.async_update_ha_state() for device in devices
|
||||
if device.track]
|
||||
|
@ -167,8 +174,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
|
||||
if scanner:
|
||||
yield from async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see)
|
||||
async_setup_scanner_platform(
|
||||
hass, p_config, scanner, tracker.async_see, p_type)
|
||||
return
|
||||
|
||||
if not setup:
|
||||
|
@ -561,6 +568,26 @@ class Device(Entity):
|
|||
if resp is not None:
|
||||
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):
|
||||
"""Device scanner object."""
|
||||
|
@ -638,14 +665,16 @@ def async_load_config(path: str, hass: HomeAssistantType,
|
|||
return []
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
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.
|
||||
|
||||
This method is a coroutine.
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
update_lock = asyncio.Lock(loop=hass.loop)
|
||||
scanner.hass = hass
|
||||
|
||||
# Initial scan of each mac we also tell about host name for config
|
||||
|
@ -654,7 +683,14 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
|||
@asyncio.coroutine
|
||||
def async_device_tracker_scan(now: dt_util.dt.datetime):
|
||||
"""Called when interval matches."""
|
||||
found_devices = yield from scanner.async_scan_devices()
|
||||
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()
|
||||
|
||||
for mac in found_devices:
|
||||
if mac in seen:
|
||||
|
@ -678,7 +714,7 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
|
|||
hass.async_add_job(async_see_device(**kwargs))
|
||||
|
||||
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):
|
||||
|
|
|
@ -16,7 +16,8 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.device_tracker import (
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -25,6 +26,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
|||
|
||||
CONF_PROTOCOL = 'protocol'
|
||||
CONF_MODE = 'mode'
|
||||
DEFAULT_SSH_PORT = 22
|
||||
CONF_SSH_KEY = 'ssh_key'
|
||||
CONF_PUB_KEY = 'pub_key'
|
||||
SECRET_GROUP = 'Password or SSH Key'
|
||||
|
@ -38,6 +40,7 @@ PLATFORM_SCHEMA = vol.All(
|
|||
vol.In(['ssh', 'telnet']),
|
||||
vol.Optional(CONF_MODE, default='router'):
|
||||
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_SSH_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.protocol = config[CONF_PROTOCOL]
|
||||
self.mode = config[CONF_MODE]
|
||||
self.port = config[CONF_PORT]
|
||||
self.ssh_args = {}
|
||||
|
||||
if self.protocol == 'ssh':
|
||||
|
||||
self.ssh_args['port'] = self.port
|
||||
if self.ssh_key:
|
||||
self.ssh_secret = {'ssh_key': self.ssh_key}
|
||||
self.ssh_args['ssh_key'] = self.ssh_key
|
||||
elif self.password:
|
||||
self.ssh_secret = {'password': self.password}
|
||||
self.ssh_args['password'] = self.password
|
||||
else:
|
||||
_LOGGER.error('No password or private key specified')
|
||||
self.success_init = False
|
||||
|
@ -179,7 +186,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
|||
|
||||
ssh = pxssh.pxssh()
|
||||
try:
|
||||
ssh.login(self.host, self.username, **self.ssh_secret)
|
||||
ssh.login(self.host, self.username, **self.ssh_args)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error('Connection refused. Is SSH enabled?')
|
||||
return None
|
||||
|
|
|
@ -13,28 +13,31 @@ import aiohttp
|
|||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
# Configuration constant specific for tado
|
||||
CONF_HOME_ID = 'home_id'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): 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):
|
||||
"""Return a Tado scanner."""
|
||||
scanner = TadoDeviceScanner(hass, config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
|
@ -50,8 +53,19 @@ class TadoDeviceScanner(DeviceScanner):
|
|||
|
||||
self.username = config[CONF_USERNAME]
|
||||
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(
|
||||
hass, cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop))
|
||||
|
@ -62,7 +76,11 @@ class TadoDeviceScanner(DeviceScanner):
|
|||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""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]
|
||||
|
||||
|
@ -87,43 +105,54 @@ class TadoDeviceScanner(DeviceScanner):
|
|||
_LOGGER.debug("Requesting Tado")
|
||||
|
||||
last_results = []
|
||||
|
||||
response = None
|
||||
tadojson = None
|
||||
tado_json = None
|
||||
|
||||
try:
|
||||
# get first token
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
url = self.tadoapiurl.format(self.username, self.password)
|
||||
response = yield from self.websession.get(
|
||||
url
|
||||
)
|
||||
# Format the URL here, so we can log the template URL if
|
||||
# anything goes wrong without exposing username and password.
|
||||
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
|
||||
if response.status != 200:
|
||||
_LOGGER.warning(
|
||||
"Error %d on %s.", response.status, self.tadoapiurl)
|
||||
self.token = None
|
||||
return
|
||||
|
||||
tadojson = yield from response.json()
|
||||
tado_json = yield from response.json()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.error("Can not load Tado data")
|
||||
_LOGGER.error("Cannot load Tado data")
|
||||
return False
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
# Find devices that have geofencing enabled, and are currently at home
|
||||
for mobiledevice in tadojson['mobileDevices']:
|
||||
if 'location' in mobiledevice:
|
||||
if mobiledevice['location']['atHome']:
|
||||
deviceid = mobiledevice['id']
|
||||
devicename = mobiledevice['name']
|
||||
last_results.append(Device(deviceid, devicename))
|
||||
# Without a home_id, we fetched an URL where the mobile devices can be
|
||||
# found under the mobileDevices key.
|
||||
if 'mobileDevices' in tado_json:
|
||||
tado_json = tado_json['mobileDevices']
|
||||
|
||||
# Find devices that have geofencing enabled, and are currently at home.
|
||||
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
|
||||
|
||||
_LOGGER.info("Tado presence query successful")
|
||||
_LOGGER.info(
|
||||
"Tado presence query successful, %d device(s) at home",
|
||||
len(self.last_results)
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
|||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,7 +59,7 @@ class TrackRDeviceScanner(object):
|
|||
trackr_id = trackr.tracker_id()
|
||||
trackr_device_id = trackr.id()
|
||||
lost = trackr.lost()
|
||||
dev_id = trackr.name().replace(" ", "_")
|
||||
dev_id = slugify(trackr.name())
|
||||
if dev_id is None:
|
||||
dev_id = trackr_id
|
||||
location = trackr.last_known_location()
|
||||
|
|
|
@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
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__)
|
||||
|
@ -63,19 +63,13 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
"Chrome/47.0.2526.106 Safari/537.36")
|
||||
}
|
||||
|
||||
self.websession = async_create_clientsession(
|
||||
hass, auto_cleanup=False,
|
||||
cookie_jar=aiohttp.CookieJar(unsafe=True, loop=hass.loop)
|
||||
)
|
||||
self.websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_logout(event):
|
||||
"""Logout from upc connect box."""
|
||||
try:
|
||||
yield from self._async_ws_function(CMD_LOGOUT)
|
||||
self.token = None
|
||||
finally:
|
||||
self.websession.detach()
|
||||
yield from self._async_ws_function(CMD_LOGOUT)
|
||||
self.token = None
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_logout)
|
||||
|
@ -92,8 +86,7 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
raw = yield from self._async_ws_function(CMD_DEVICES)
|
||||
|
||||
try:
|
||||
xml_root = yield from self.hass.loop.run_in_executor(
|
||||
None, ET.fromstring, raw)
|
||||
xml_root = ET.fromstring(raw)
|
||||
return [mac.text for mac in xml_root.iter('MACAddr')]
|
||||
except (ET.ParseError, TypeError):
|
||||
_LOGGER.warning("Can't read device from %s", self.host)
|
||||
|
@ -111,7 +104,6 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
response = None
|
||||
try:
|
||||
# get first token
|
||||
self.websession.cookie_jar.clear()
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host)
|
||||
|
@ -150,27 +142,26 @@ class UPCDeviceScanner(DeviceScanner):
|
|||
if additional_form:
|
||||
form_data.update(additional_form)
|
||||
|
||||
redirects = True if function != CMD_DEVICES else False
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.post(
|
||||
"http://{}/xml/getter.xml".format(self.host),
|
||||
data=form_data,
|
||||
headers=self.headers
|
||||
headers=self.headers,
|
||||
allow_redirects=redirects
|
||||
)
|
||||
|
||||
# error on UPC webservice
|
||||
# error?
|
||||
if response.status != 200:
|
||||
_LOGGER.warning(
|
||||
"Error %d on %s.", response.status, function)
|
||||
_LOGGER.warning("Receive http code %d", response.status)
|
||||
self.token = None
|
||||
return
|
||||
|
||||
# load data, store token for next request
|
||||
raw = yield from response.text()
|
||||
self.token = response.cookies['sessionToken'].value
|
||||
|
||||
return raw
|
||||
return (yield from response.text())
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.error("Error on %s", function)
|
||||
|
|
|
@ -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
|
||||
https://home-assistant.io/components/device_tracker.volvooncall/
|
||||
"""
|
||||
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.const import (
|
||||
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)
|
||||
from homeassistant.components.volvooncall import DOMAIN
|
||||
|
||||
_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):
|
||||
"""Validate the configuration and return a scanner."""
|
||||
from volvooncall import Connection
|
||||
connection = Connection(
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD))
|
||||
"""Setup Volvo tracker."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
interval = max(MIN_TIME_BETWEEN_SCANS,
|
||||
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||
vin, _ = discovery_info
|
||||
vehicle = hass.data[DOMAIN].vehicles[vin]
|
||||
|
||||
def _see_vehicle(vehicle):
|
||||
position = vehicle["position"]
|
||||
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")
|
||||
host_name = vehicle.registration_number
|
||||
dev_id = 'volvo_' + slugify(host_name)
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
"""Callback for reporting vehicle position."""
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(position["latitude"],
|
||||
position["longitude"]),
|
||||
attributes=attributes)
|
||||
gps=(vehicle.position['latitude'],
|
||||
vehicle.position['longitude']))
|
||||
|
||||
def update(now):
|
||||
"""Update status from the online service."""
|
||||
_LOGGER.info("Updating")
|
||||
try:
|
||||
res, vehicles = connection.update()
|
||||
if not res:
|
||||
_LOGGER.error("Could not query server")
|
||||
return False
|
||||
hass.data[DOMAIN].entities[vin].append(see_vehicle)
|
||||
see_vehicle(vehicle)
|
||||
|
||||
for vehicle in vehicles:
|
||||
_see_vehicle(vehicle)
|
||||
|
||||
return True
|
||||
finally:
|
||||
track_point_in_utc_time(hass, update, now + interval)
|
||||
|
||||
_LOGGER.info('Logging in to service')
|
||||
return update(utcnow())
|
||||
return True
|
||||
|
|
|
@ -11,6 +11,7 @@ import threading
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
|
@ -41,10 +42,16 @@ SERVICE_HANDLERS = {
|
|||
'yeelight': ('light', 'yeelight'),
|
||||
'flux_led': ('light', 'flux_led'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -57,10 +64,17 @@ def setup(hass, config):
|
|||
# Disable zeroconf logging, it spams
|
||||
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
|
||||
|
||||
# Platforms ignore by config
|
||||
ignored_platforms = config[DOMAIN][CONF_IGNORE]
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
def new_service_listener(service, info):
|
||||
"""Called when a new service is found."""
|
||||
if service in ignored_platforms:
|
||||
logger.info("Ignoring service: %s %s", service, info)
|
||||
return
|
||||
|
||||
with lock:
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
|
|
|
@ -4,20 +4,24 @@ Support for Envisalink devices.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/envisalink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
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__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
EVL_CONTROLLER = None
|
||||
DATA_EVL = 'envisalink'
|
||||
|
||||
CONF_EVL_HOST = 'host'
|
||||
CONF_EVL_PORT = 'port'
|
||||
|
@ -43,9 +47,9 @@ DEFAULT_ZONEDUMP_INTERVAL = 30
|
|||
DEFAULT_ZONETYPE = 'opening'
|
||||
DEFAULT_PANIC = 'Police'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
|
||||
SIGNAL_ZONE_UPDATE = 'envisalink.zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'envisalink.partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'envisalink.keypad_updated'
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONENAME): cv.string,
|
||||
|
@ -77,119 +81,111 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, base_config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Common setup for Envisalink devices."""
|
||||
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)
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_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)
|
||||
controller = EnvisalinkAlarmPanel(
|
||||
host, port, panel_type, version, user, password, zone_dump,
|
||||
keep_alive, hass.loop)
|
||||
hass.data[DATA_EVL] = controller
|
||||
|
||||
@callback
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
_LOGGER.error("The envisalink rejected your credentials.")
|
||||
_connect_status['fail'] = 1
|
||||
sync_connect.set_result(False)
|
||||
|
||||
@callback
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_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):
|
||||
"""Callback for a successful connection."""
|
||||
_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):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=None,
|
||||
zone=data)
|
||||
async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event.")
|
||||
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
|
||||
|
||||
@callback
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
_LOGGER.info("Shutting down envisalink.")
|
||||
EVL_CONTROLLER.stop()
|
||||
controller.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the Envisalink."""
|
||||
hass.loop.call_soon_threadsafe(EVL_CONTROLLER.start)
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
return True
|
||||
elif 'fail' in _connect_status:
|
||||
return False
|
||||
else:
|
||||
time.sleep(1)
|
||||
controller.callback_zone_timer_dump = zones_updated_callback
|
||||
controller.callback_zone_state_change = zones_updated_callback
|
||||
controller.callback_partition_state_change = partition_updated_callback
|
||||
controller.callback_keypad_update = alarm_data_updated_callback
|
||||
controller.callback_login_failure = login_fail_callback
|
||||
controller.callback_login_timeout = connection_fail_callback
|
||||
controller.callback_login_success = connection_success_callback
|
||||
|
||||
_LOGGER.error("Timeout occurred while establishing evl connection.")
|
||||
return False
|
||||
_LOGGER.info("Start envisalink.")
|
||||
controller.start()
|
||||
|
||||
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
|
||||
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:
|
||||
result = yield from sync_connect
|
||||
if not result:
|
||||
return False
|
||||
|
||||
# Load sub-components for Envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{CONF_PARTITIONS: _partitions,
|
||||
CONF_CODE: _code,
|
||||
CONF_PANIC: _panic_type}, base_config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{CONF_PARTITIONS: _partitions,
|
||||
CONF_CODE: _code}, base_config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{CONF_ZONES: _zones}, base_config)
|
||||
if partitions:
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'alarm_control_panel', 'envisalink', {
|
||||
CONF_PARTITIONS: partitions,
|
||||
CONF_CODE: code,
|
||||
CONF_PANIC: panic_type
|
||||
}, config
|
||||
))
|
||||
hass.async_add_job(async_load_platform(
|
||||
hass, 'sensor', 'envisalink', {
|
||||
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
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@ Support for MQTT fans.
|
|||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/fan.mqtt/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (
|
||||
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
|
||||
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 MQTT fan platform."""
|
||||
add_devices([MqttFan(
|
||||
hass,
|
||||
yield from async_add_devices([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
|
@ -113,15 +114,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MqttFan(FanEntity):
|
||||
"""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):
|
||||
"""Initialize the MQTT fan."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload = payload
|
||||
self._templates = templates
|
||||
self._speed_list = speed_list
|
||||
self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None
|
||||
self._optimistic_oscillation = (
|
||||
|
@ -129,19 +130,29 @@ class MqttFan(FanEntity):
|
|||
self._optimistic_speed = (
|
||||
optimistic or topic[CONF_SPEED_STATE_TOPIC] is None)
|
||||
self._state = False
|
||||
self._speed = None
|
||||
self._oscillation = None
|
||||
self._supported_features = 0
|
||||
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC]
|
||||
is not None and SUPPORT_OSCILLATE)
|
||||
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
|
||||
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:
|
||||
templates[key] = lambda value: value
|
||||
else:
|
||||
tpl.hass = hass
|
||||
templates[key] = tpl.render_with_possible_json_value
|
||||
tpl.hass = self.hass
|
||||
templates[key] = tpl.async_render_with_possible_json_value
|
||||
|
||||
@callback
|
||||
def state_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
payload = templates[CONF_STATE](payload)
|
||||
|
@ -149,13 +160,14 @@ class MqttFan(FanEntity):
|
|||
self._state = True
|
||||
elif payload == self._payload[STATE_OFF]:
|
||||
self._state = False
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
if self._topic[CONF_STATE_TOPIC] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC],
|
||||
state_received, self._qos)
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
|
||||
self._qos)
|
||||
|
||||
@callback
|
||||
def speed_received(topic, payload, qos):
|
||||
"""A new MQTT message for the speed has been received."""
|
||||
payload = templates[ATTR_SPEED](payload)
|
||||
|
@ -165,17 +177,15 @@ class MqttFan(FanEntity):
|
|||
self._speed = SPEED_MEDIUM
|
||||
elif payload == self._payload[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:
|
||||
mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC],
|
||||
speed_received, self._qos)
|
||||
self._speed = SPEED_OFF
|
||||
elif self._topic[CONF_SPEED_COMMAND_TOPIC] is not None:
|
||||
self._speed = SPEED_OFF
|
||||
else:
|
||||
self._speed = SPEED_OFF
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
|
||||
self._qos)
|
||||
self._speed = SPEED_OFF
|
||||
|
||||
@callback
|
||||
def oscillation_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
payload = templates[OSCILLATION](payload)
|
||||
|
@ -183,17 +193,13 @@ class MqttFan(FanEntity):
|
|||
self._oscillation = True
|
||||
elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]:
|
||||
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:
|
||||
mqtt.subscribe(self._hass,
|
||||
self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
||||
oscillation_received, self._qos)
|
||||
self._oscillation = False
|
||||
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None:
|
||||
self._oscillation = False
|
||||
else:
|
||||
self._oscillation = False
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
||||
oscillation_received, self._qos)
|
||||
self._oscillation = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -235,43 +241,72 @@ class MqttFan(FanEntity):
|
|||
"""Return the oscillation state."""
|
||||
return self._oscillation
|
||||
|
||||
def turn_on(self, speed: str=None) -> None:
|
||||
"""Turn on the entity."""
|
||||
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_ON], self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, speed: str=None) -> None:
|
||||
"""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)
|
||||
if speed:
|
||||
self.set_speed(speed)
|
||||
yield from self.async_set_speed(speed)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
"""Turn off the entity."""
|
||||
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_OFF], self._qos, self._retain)
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self) -> None:
|
||||
"""Turn off the entity.
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if self._topic[CONF_SPEED_COMMAND_TOPIC] is not None:
|
||||
mqtt_payload = SPEED_OFF
|
||||
if speed == SPEED_LOW:
|
||||
mqtt_payload = self._payload[SPEED_LOW]
|
||||
elif speed == SPEED_MEDIUM:
|
||||
mqtt_payload = self._payload[SPEED_MEDIUM]
|
||||
elif speed == SPEED_HIGH:
|
||||
mqtt_payload = self._payload[SPEED_HIGH]
|
||||
else:
|
||||
mqtt_payload = speed
|
||||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_OFF], self._qos, self._retain)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if self._topic[CONF_SPEED_COMMAND_TOPIC] is None:
|
||||
return
|
||||
|
||||
if speed == SPEED_LOW:
|
||||
mqtt_payload = self._payload[SPEED_LOW]
|
||||
elif speed == SPEED_MEDIUM:
|
||||
mqtt_payload = self._payload[SPEED_MEDIUM]
|
||||
elif speed == SPEED_HIGH:
|
||||
mqtt_payload = self._payload[SPEED_HIGH]
|
||||
else:
|
||||
mqtt_payload = speed
|
||||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
|
||||
mqtt_payload, self._qos, self._retain)
|
||||
|
||||
if self._optimistic_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()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation."""
|
||||
if self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None:
|
||||
self._oscillation = oscillating
|
||||
@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:
|
||||
payload = self._payload[OSCILLATE_OFF_PAYLOAD]
|
||||
else:
|
||||
payload = self._payload[OSCILLATE_ON_PAYLOAD]
|
||||
if oscillating is False:
|
||||
payload = self._payload[OSCILLATE_OFF_PAYLOAD]
|
||||
mqtt.publish(self._hass,
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
payload, self._qos, self._retain)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
payload, self._qos, self._retain)
|
||||
|
||||
if self._optimistic_oscillation:
|
||||
self._oscillation = oscillating
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
|
|
@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = 'ffmpeg'
|
||||
REQUIREMENTS = ["ha-ffmpeg==1.4"]
|
||||
REQUIREMENTS = ["ha-ffmpeg==1.5"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ DOMAIN = 'frontend'
|
|||
DEPENDENCIES = ['api', 'websocket_api']
|
||||
URL_PANEL_COMPONENT = '/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 = {
|
||||
"background_color": "#FFFFFF",
|
||||
"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,
|
||||
sidebar_icon=None, url_path=None, config=None):
|
||||
"""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:
|
||||
url = ('/static/home-assistant-polymer/panels/'
|
||||
'{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:
|
||||
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),
|
||||
FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path,
|
||||
url, config)
|
||||
# Fingerprint doesn't exist when adding new built-in panel
|
||||
register_panel(hass, component_name, path,
|
||||
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,
|
||||
|
@ -230,10 +235,14 @@ class IndexView(HomeAssistantView):
|
|||
|
||||
if request.app[KEY_DEVELOPMENT]:
|
||||
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'
|
||||
else:
|
||||
core_url = '/static/core-{}.js'.format(
|
||||
FINGERPRINTS['core.js'])
|
||||
compatibility_url = '/static/compatibility-{}.js'.format(
|
||||
FINGERPRINTS['compatibility.js'])
|
||||
ui_url = '/static/frontend-{}.html'.format(
|
||||
FINGERPRINTS['frontend.html'])
|
||||
|
||||
|
@ -263,7 +272,8 @@ class IndexView(HomeAssistantView):
|
|||
# pylint: disable=no-member
|
||||
# This is a jinja2 template, not a HA template so we call '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'],
|
||||
panel_url=panel_url, panels=hass.data[DATA_PANELS])
|
||||
|
||||
|
|
|
@ -78,6 +78,16 @@
|
|||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <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>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url -%}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
|
||||
"frontend.html": "a179cbf60629f02e1d6f4b0034a783bf",
|
||||
"frontend.html": "be258a53166b82f4ebd5232037e1cbd5",
|
||||
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "5c82300b3cf543a92cf4297506e450e7",
|
||||
"panels/ha-panel-dev-info.html": "5f8119c8aa18cba51106e1bb8ca9cba2",
|
||||
"panels/ha-panel-dev-service.html": "306243e11766266c967d1163ca678fe6",
|
||||
"panels/ha-panel-dev-state.html": "7d069ba8fd5379fa8f59858b8c0a7473",
|
||||
"panels/ha-panel-dev-template.html": "219d66c469982b6d6ae015d21156ac82",
|
||||
"panels/ha-panel-history.html": "c5cf0aa3eba31f40231b0479b08913eb",
|
||||
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
|
||||
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "a9247f255174b084fad2c04bdb9ec7a9",
|
||||
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
|
||||
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
|
||||
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
|
||||
"panels/ha-panel-map.html": "ecd2ee7ff6e2837e67950d3ab1a6ec65",
|
||||
"panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
|
|
@ -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()})();
|
|
@ -1 +1 @@
|
|||
Subproject commit f3808ff4d44733f5810e21131e0daa1425bf5f22
|
||||
Subproject commit 5d223c8da4b4380bf79d8f0285ce7824063e89ef
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |