commit
e0f1c8ac67
13
.coveragerc
13
.coveragerc
|
@ -5,6 +5,7 @@ omit =
|
|||
homeassistant/__main__.py
|
||||
homeassistant/scripts/*.py
|
||||
homeassistant/helpers/typing.py
|
||||
homeassistant/helpers/signal.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/apcupsd.py
|
||||
|
@ -116,9 +117,6 @@ omit =
|
|||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/ffmpeg.py
|
||||
homeassistant/components/*/ffmpeg.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
|
@ -132,6 +130,7 @@ omit =
|
|||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/apiai.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
|
@ -141,6 +140,7 @@ omit =
|
|||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
|
@ -210,7 +210,9 @@ omit =
|
|||
homeassistant/components/light/piglow.py
|
||||
homeassistant/components/light/zengge.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/media_player/anthemav.py
|
||||
homeassistant/components/media_player/apple_tv.py
|
||||
homeassistant/components/media_player/aquostv.py
|
||||
homeassistant/components/media_player/braviatv.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
|
@ -226,6 +228,7 @@ omit =
|
|||
homeassistant/components/media_player/itunes.py
|
||||
homeassistant/components/media_player/kodi.py
|
||||
homeassistant/components/media_player/lg_netcast.py
|
||||
homeassistant/components/media_player/liveboxplaytv.py
|
||||
homeassistant/components/media_player/mpchc.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/media_player/nad.py
|
||||
|
@ -256,6 +259,7 @@ omit =
|
|||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/mailgun.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
|
@ -325,11 +329,13 @@ omit =
|
|||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
homeassistant/components/sensor/openevse.py
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
|
@ -365,6 +371,7 @@ omit =
|
|||
homeassistant/components/switch/digitalloggers.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/fritzdect.py
|
||||
homeassistant/components/switch/hdmi_cec.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
|
|
14
Dockerfile
14
Dockerfile
|
@ -1,14 +1,22 @@
|
|||
FROM python:3.5
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
# Uncomment any of the following lines to disable the installation.
|
||||
#ENV INSTALL_TELLSTICK no
|
||||
#ENV INSTALL_OPENALPR no
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_OPENZWAVE no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy build scripts
|
||||
COPY script/setup_docker_prereqs script/build_python_openzwave script/build_libcec script/install_phantomjs script/
|
||||
RUN script/setup_docker_prereqs
|
||||
COPY virtualization/Docker/ virtualization/Docker/
|
||||
RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
|
@ -18,4 +26,4 @@ RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
|||
# Copy source
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
|
@ -26,6 +26,7 @@ from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
event_decorators, service, config_per_platform, extract_domain_configs)
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -435,6 +436,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
async_register_signal_handling(hass)
|
||||
return hass
|
||||
|
||||
|
||||
|
|
|
@ -12,14 +12,19 @@ import itertools as it
|
|||
import logging
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
SERVICE_CHECK_CONFIG = 'check_config'
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
|
@ -75,6 +80,21 @@ def toggle(hass, entity_id=None, **service_data):
|
|||
hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data)
|
||||
|
||||
|
||||
def stop(hass):
|
||||
"""Stop Home Assistant."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP)
|
||||
|
||||
|
||||
def restart(hass):
|
||||
"""Stop Home Assistant."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART)
|
||||
|
||||
|
||||
def check_config(hass):
|
||||
"""Check the config files."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG)
|
||||
|
||||
|
||||
def reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
@ -84,7 +104,7 @@ def reload_core_config(hass):
|
|||
def async_setup(hass, config):
|
||||
"""Setup general services related to Home Assistant."""
|
||||
@asyncio.coroutine
|
||||
def handle_turn_service(service):
|
||||
def async_handle_turn_service(service):
|
||||
"""Method to handle calls to homeassistant.turn_on/off."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
|
||||
|
@ -122,18 +142,37 @@ def async_setup(hass, config):
|
|||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import config as conf_util
|
||||
def async_handle_core_service(call):
|
||||
"""Service handler for handling core services."""
|
||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||
hass.async_add_job(hass.async_stop())
|
||||
return
|
||||
|
||||
try:
|
||||
yield from conf_util.async_check_ha_config_file(hass)
|
||||
except HomeAssistantError:
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
"""Service handler for reloading core config."""
|
||||
try:
|
||||
conf = yield from conf_util.async_hass_config_yaml(hass)
|
||||
except HomeAssistantError as err:
|
||||
|
@ -144,6 +183,6 @@ def async_setup(hass, config):
|
|||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
|
||||
return True
|
||||
|
|
|
@ -11,7 +11,7 @@ from homeassistant.const import (STATE_UNKNOWN,
|
|||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY)
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,7 +24,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
import pywink
|
||||
|
||||
for camera in pywink.get_cameras():
|
||||
add_devices([WinkCameraDevice(camera, hass)])
|
||||
# get_cameras returns multiple device types.
|
||||
# Only add those that aren't sensors.
|
||||
try:
|
||||
camera.capability()
|
||||
except AttributeError:
|
||||
_id = camera.object_id() + camera.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCameraDevice(camera, hass)])
|
||||
|
||||
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
|
@ -32,7 +39,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
|||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink alarm."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
"""
|
||||
Support for repeating alerts when conditions are met.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alert/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
|
||||
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.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alert'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_CAN_ACK = 'can_acknowledge'
|
||||
CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
|
||||
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_NOTIFIERS): cv.ensure_list})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: ALERT_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
ALERT_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def is_on(hass, entity_id):
|
||||
"""Return if the alert is firing and not acknowledged."""
|
||||
return hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id):
|
||||
"""Reset the alert."""
|
||||
run_callback_threadsafe(hass.loop, async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_turn_on(hass, entity_id):
|
||||
"""Async reset the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
"""Acknowledge alert."""
|
||||
run_callback_threadsafe(hass.loop, async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_turn_off(hass, entity_id):
|
||||
"""Async acknowledge the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
"""Toggle acknowledgement of alert."""
|
||||
run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_toggle(hass, entity_id):
|
||||
"""Async toggle acknowledgement of alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup alert component."""
|
||||
alerts = config.get(DOMAIN)
|
||||
all_alerts = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_alert_service(service_call):
|
||||
"""Handle calls to alert services."""
|
||||
alert_ids = service.extract_entity_ids(hass, service_call)
|
||||
|
||||
for alert_id in alert_ids:
|
||||
alert = all_alerts[alert_id]
|
||||
if service_call.service == SERVICE_TURN_ON:
|
||||
yield from alert.async_turn_on()
|
||||
elif service_call.service == SERVICE_TOGGLE:
|
||||
yield from alert.async_toggle()
|
||||
else:
|
||||
yield from alert.async_turn_off()
|
||||
|
||||
# setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert[CONF_ENTITY_ID],
|
||||
alert[CONF_STATE], alert[CONF_REPEAT],
|
||||
alert[CONF_SKIP_FIRST], alert[CONF_NOTIFIERS],
|
||||
alert[CONF_CAN_ACK])
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# 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
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_OFF), schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TURN_ON), schema=ALERT_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||
descriptions.get(SERVICE_TOGGLE), schema=ALERT_SERVICE_SCHEMA)
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, watched_entity_id, state,
|
||||
repeat, skip_first, notifiers, can_ack):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._alert_state = state
|
||||
self._skip_first = skip_first
|
||||
self._notifiers = notifiers
|
||||
self._can_ack = can_ack
|
||||
|
||||
self._delay = [timedelta(minutes=val) for val in repeat]
|
||||
self._next_delay = 0
|
||||
|
||||
self._firing = False
|
||||
self._ack = False
|
||||
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)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the alert."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""HASS need not poll these entities."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the alert status."""
|
||||
if self._firing:
|
||||
if self._ack:
|
||||
return STATE_OFF
|
||||
return STATE_ON
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Hide the alert when it is not firing."""
|
||||
return not self._can_ack or not self._firing
|
||||
|
||||
@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)
|
||||
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:
|
||||
yield from self.end_alerting()
|
||||
|
||||
@asyncio.coroutine
|
||||
def begin_alerting(self):
|
||||
"""Begin the alert procedures."""
|
||||
_LOGGER.debug('Beginning Alert: %s', self._name)
|
||||
self._ack = False
|
||||
self._firing = True
|
||||
self._next_delay = 0
|
||||
|
||||
if not self._skip_first:
|
||||
yield from self._notify()
|
||||
else:
|
||||
yield from self._schedule_notify()
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
"""End the alert procedures."""
|
||||
_LOGGER.debug('Ending Alert: %s', self._name)
|
||||
self._cancel()
|
||||
self._ack = False
|
||||
self._firing = False
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _schedule_notify(self):
|
||||
"""Schedule a notification."""
|
||||
delay = self._delay[self._next_delay]
|
||||
next_msg = datetime.now() + delay
|
||||
self._cancel = \
|
||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _notify(self, *args):
|
||||
"""Send the alert notification."""
|
||||
if not self._firing:
|
||||
return
|
||||
|
||||
if not self._ack:
|
||||
_LOGGER.info('Alerting: %s', self._name)
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
yield from self._schedule_notify()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
"""Async Unacknowledge alert."""
|
||||
_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)
|
||||
self._ack = True
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_toggle(self):
|
||||
"""Async toggle alert."""
|
||||
if self._ack:
|
||||
return self.async_turn_on()
|
||||
return self.async_turn_off()
|
|
@ -0,0 +1,172 @@
|
|||
"""
|
||||
Support for API.AI webhook.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/apiai/
|
||||
"""
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/apiai'
|
||||
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_ASYNC_ACTION = 'async_action'
|
||||
|
||||
DEFAULT_CONF_ASYNC_ACTION = False
|
||||
|
||||
DOMAIN = 'apiai'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_INTENTS: {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_SPEECH): cv.template,
|
||||
vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_ASYNC_ACTION,
|
||||
default=DEFAULT_CONF_ASYNC_ACTION): cv.boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate API.AI component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
|
||||
hass.http.register_view(ApiaiIntentsView(hass, intents))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ApiaiIntentsView(HomeAssistantView):
|
||||
"""Handle API.AI requests."""
|
||||
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:apiai'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize API.AI view."""
|
||||
super().__init__()
|
||||
|
||||
self.hass = hass
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Apiai intent {}".format(name))
|
||||
|
||||
self.intents = intents
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle API.AI."""
|
||||
data = yield from request.json()
|
||||
|
||||
_LOGGER.debug('Received Apiai 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)
|
||||
|
||||
action_incomplete = req['actionIncomplete']
|
||||
|
||||
if action_incomplete:
|
||||
return None
|
||||
|
||||
# use intent to no mix HASS actions with this parameter
|
||||
intent = req.get('action')
|
||||
parameters = req.get('parameters')
|
||||
# contexts = req.get('contexts')
|
||||
response = ApiaiResponse(parameters)
|
||||
|
||||
# Default Welcome Intent
|
||||
# Maybe is better to handle this in api.ai directly?
|
||||
#
|
||||
# if intent == 'input.welcome':
|
||||
# response.add_speech(
|
||||
# "Hello, and welcome to the future. How may I help?")
|
||||
# return self.json(response)
|
||||
|
||||
if intent == "":
|
||||
_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)
|
||||
|
||||
config = self.intents.get(intent)
|
||||
|
||||
if config is None:
|
||||
_LOGGER.warning('Received unknown intent %s', intent)
|
||||
response.add_speech(
|
||||
"Intent '%s' is not yet configured within Home Assistant." %
|
||||
intent)
|
||||
return self.json(response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
action = config.get(CONF_ACTION)
|
||||
async_action = config.get(CONF_ASYNC_ACTION)
|
||||
|
||||
if action is not None:
|
||||
# API.AI expects a response in less than 5s
|
||||
if async_action:
|
||||
# Do not wait for the action to be executed.
|
||||
# Needed if the action will take longer than 5s to execute
|
||||
self.hass.async_add_job(action.async_run(response.parameters))
|
||||
else:
|
||||
# Wait for the action to be executed so we can use results to
|
||||
# render the answer
|
||||
yield from action.async_run(response.parameters)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(speech)
|
||||
|
||||
return self.json(response)
|
||||
|
||||
|
||||
class ApiaiResponse(object):
|
||||
"""Help generating the response for API.AI."""
|
||||
|
||||
def __init__(self, parameters):
|
||||
"""Initialize the response."""
|
||||
self.speech = None
|
||||
self.parameters = {}
|
||||
# Parameter names replace '.' and '-' for '_'
|
||||
for key, value in parameters.items():
|
||||
underscored_key = key.replace('.', '_').replace('-', '_')
|
||||
self.parameters[underscored_key] = value
|
||||
|
||||
def add_speech(self, text):
|
||||
"""Add speech to the response."""
|
||||
assert self.speech is None
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.async_render(self.parameters)
|
||||
|
||||
self.speech = text
|
||||
|
||||
def as_dict(self):
|
||||
"""Return response in an API.AI valid dict."""
|
||||
return {
|
||||
'speech': self.speech,
|
||||
'displayText': self.speech,
|
||||
'source': PROJECT_NAME,
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
"""
|
||||
Provides a binary sensor which is a collection of ffmpeg tools.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ffmpeg/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN)
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME,
|
||||
ATTR_ENTITY_ID)
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_START = 'ffmpeg_start'
|
||||
SERVICE_STOP = 'ffmpeg_stop'
|
||||
SERVICE_RESTART = 'ffmpeg_restart'
|
||||
|
||||
DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor'
|
||||
|
||||
FFMPEG_SENSOR_NOISE = 'noise'
|
||||
FFMPEG_SENSOR_MOTION = 'motion'
|
||||
|
||||
MAP_FFMPEG_BIN = [
|
||||
FFMPEG_SENSOR_NOISE,
|
||||
FFMPEG_SENSOR_MOTION
|
||||
]
|
||||
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
CONF_TOOL = 'tool'
|
||||
CONF_PEAK = 'peak'
|
||||
CONF_DURATION = 'duration'
|
||||
CONF_RESET = 'reset'
|
||||
CONF_CHANGES = 'changes'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_REPEAT_TIME = 'repeat_time'
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg'
|
||||
DEFAULT_INIT_STATE = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN),
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_OUTPUT): cv.string,
|
||||
vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int),
|
||||
vol.Optional(CONF_DURATION, default=1):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_RESET, default=10):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_CHANGES, default=10):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
|
||||
vol.Optional(CONF_REPEAT, default=0):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Optional(CONF_REPEAT_TIME, default=0):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
})
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def restart(hass, entity_id=None):
|
||||
"""Restart a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_RESTART, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
from haffmpeg import SensorNoise, SensorMotion
|
||||
|
||||
# check source
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE:
|
||||
entity = FFmpegNoise(hass, SensorNoise, config)
|
||||
else:
|
||||
entity = FFmpegMotion(hass, SensorMotion, config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_shutdown(event):
|
||||
"""Stop ffmpeg."""
|
||||
yield from entity.async_shutdown_ffmpeg()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_shutdown)
|
||||
|
||||
# start on startup
|
||||
if config.get(CONF_INITIAL_STATE):
|
||||
@asyncio.coroutine
|
||||
def async_start(event):
|
||||
"""Start ffmpeg."""
|
||||
yield from entity.async_start_ffmpeg()
|
||||
yield from entity.async_update_ha_state()
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_start)
|
||||
|
||||
# add to system
|
||||
yield from async_add_devices([entity])
|
||||
|
||||
# exists service?
|
||||
if hass.services.has_service(DOMAIN, SERVICE_RESTART):
|
||||
hass.data[DATA_FFMPEG_DEVICE].append(entity)
|
||||
return
|
||||
hass.data[DATA_FFMPEG_DEVICE] = [entity]
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
# register service
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Handle service binary_sensor.ffmpeg_restart."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
|
||||
if entity_ids:
|
||||
_devices = [device for device in hass.data[DATA_FFMPEG_DEVICE]
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
_devices = hass.data[DATA_FFMPEG_DEVICE]
|
||||
|
||||
tasks = []
|
||||
for device in _devices:
|
||||
if service.service == SERVICE_START:
|
||||
tasks.append(device.async_start_ffmpeg())
|
||||
elif service.service == SERVICE_STOP:
|
||||
tasks.append(device.async_shutdown_ffmpeg())
|
||||
else:
|
||||
tasks.append(device.async_restart_ffmpeg())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle,
|
||||
descriptions.get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, async_service_handle,
|
||||
descriptions.get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, async_service_handle,
|
||||
descriptions.get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
|
||||
class FFmpegBinarySensor(BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, ffobj, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._state = False
|
||||
self._config = config
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._ffmpeg = ffobj(
|
||||
self._manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def _async_callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_shutdown_ffmpeg(self):
|
||||
"""For STOP event to shutdown ffmpeg.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._ffmpeg.close()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_restart_ffmpeg(self):
|
||||
"""Restart processing."""
|
||||
yield from self.async_shutdown_ffmpeg()
|
||||
yield from self.async_start_ffmpeg()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._ffmpeg.is_running
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self._ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
peak=self._config.get(CONF_PEAK),
|
||||
)
|
||||
|
||||
# run
|
||||
return self._ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return "sound"
|
||||
|
||||
|
||||
class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self._ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
time_repeat=self._config.get(CONF_REPEAT_TIME),
|
||||
repeat=self._config.get(CONF_REPEAT),
|
||||
changes=self._config.get(CONF_CHANGES),
|
||||
)
|
||||
|
||||
# run
|
||||
return self._ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return "motion"
|
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
Provides a binary sensor which is a collection of ffmpeg tools.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ffmpeg_motion/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.ffmpeg import (
|
||||
FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS,
|
||||
CONF_INITIAL_STATE)
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESET = 'reset'
|
||||
CONF_CHANGES = 'changes'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_REPEAT_TIME = 'repeat_time'
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg Motion'
|
||||
DEFAULT_INIT_STATE = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_RESET, default=10):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_CHANGES, default=10):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
|
||||
vol.Inclusive(CONF_REPEAT, 'repeat'):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Inclusive(CONF_REPEAT_TIME, 'repeat'):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
# check source
|
||||
if not manager.async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Constructor for binary sensor noise detection."""
|
||||
super().__init__(config.get(CONF_INITIAL_STATE))
|
||||
|
||||
self._state = False
|
||||
self._config = config
|
||||
self._name = config.get(CONF_NAME)
|
||||
|
||||
@callback
|
||||
def _async_callback(self, state):
|
||||
"""HA-FFmpeg callback for noise detection."""
|
||||
self._state = state
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
|
||||
class FFmpegMotion(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, manager, config):
|
||||
"""Initialize ffmpeg motion binary sensor."""
|
||||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(hass, config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
time_repeat=self._config.get(CONF_REPEAT_TIME, 0),
|
||||
repeat=self._config.get(CONF_REPEAT, 0),
|
||||
changes=self._config.get(CONF_CHANGES),
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return "motion"
|
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
Provides a binary sensor which is a collection of ffmpeg tools.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ffmpeg_noise/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.components.binary_sensor.ffmpeg_motion import (
|
||||
FFmpegBinarySensor)
|
||||
from homeassistant.components.ffmpeg import (
|
||||
DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS,
|
||||
CONF_INITIAL_STATE)
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PEAK = 'peak'
|
||||
CONF_DURATION = 'duration'
|
||||
CONF_RESET = 'reset'
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg Noise'
|
||||
DEFAULT_INIT_STATE = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_OUTPUT): cv.string,
|
||||
vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int),
|
||||
vol.Optional(CONF_DURATION, default=1):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_RESET, default=10):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
# check source
|
||||
if not manager.async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
|
||||
# generate sensor object
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
|
||||
# add to system
|
||||
manager.async_register_device(entity)
|
||||
yield from async_add_devices([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
"""A binary sensor which use ffmpeg for noise detection."""
|
||||
|
||||
def __init__(self, hass, manager, config):
|
||||
"""Initialize ffmpeg noise binary sensor."""
|
||||
from haffmpeg import SensorNoise
|
||||
|
||||
super().__init__(hass, config)
|
||||
self.ffmpeg = SensorNoise(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a FFmpeg instance.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
# init config
|
||||
self.ffmpeg.set_options(
|
||||
time_duration=self._config.get(CONF_DURATION),
|
||||
time_reset=self._config.get(CONF_RESET),
|
||||
peak=self._config.get(CONF_PEAK),
|
||||
)
|
||||
|
||||
# run
|
||||
return self.ffmpeg.open_sensor(
|
||||
input_source=self._config.get(CONF_INPUT),
|
||||
output_dest=self._config.get(CONF_OUTPUT),
|
||||
extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS),
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return "sound"
|
|
@ -36,7 +36,10 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
|||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the MQTT binary sensor."""
|
||||
"""Set up the MQTT binary sensor."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
# Describes the format for available binary_sensor services
|
||||
|
||||
ffmpeg_start:
|
||||
description: Send a start command to a ffmpeg based sensor.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will start. Platform dependent.
|
||||
example: 'binary_sensor.ffmpeg_noise'
|
||||
|
||||
ffmpeg_stop:
|
||||
description: Send a stop command to a ffmpeg based sensor.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will stop. Platform dependent.
|
||||
example: 'binary_sensor.ffmpeg_noise'
|
||||
|
||||
ffmpeg_restart:
|
||||
description: Send a restart command to a ffmpeg based sensor.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entites that will restart. Platform dependent.
|
||||
example: 'binary_sensor.ffmpeg_noise'
|
|
@ -118,7 +118,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
# Common during HA startup - so just a warning
|
||||
_LOGGER.warning(ex)
|
||||
_LOGGER.warning('Could not render template %s,'
|
||||
' the state is unknown.', self._name)
|
||||
return
|
||||
_LOGGER.error(ex)
|
||||
_LOGGER.error('Could not render template %s: %s', self._name, ex)
|
||||
self._state = False
|
||||
|
|
|
@ -4,11 +4,14 @@ Support for Wink binary sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.wink import WinkDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
|
@ -17,11 +20,14 @@ SENSOR_TYPES = {
|
|||
"brightness": "light",
|
||||
"vibration": "vibration",
|
||||
"loudness": "sound",
|
||||
"noise": "sound",
|
||||
"capturing_audio": "sound",
|
||||
"liquid_detected": "moisture",
|
||||
"motion": "motion",
|
||||
"presence": "occupancy",
|
||||
"co_detected": "gas",
|
||||
"smoke_detected": "smoke"
|
||||
"smoke_detected": "smoke",
|
||||
"capturing_video": None
|
||||
}
|
||||
|
||||
|
||||
|
@ -30,26 +36,54 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
import pywink
|
||||
|
||||
for sensor in pywink.get_sensors():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
_id = sensor.object_id() + sensor.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||
_id = key.object_id() + key.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
_id = sensor.object_id() + sensor.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkSmokeDetector(sensor, hass)])
|
||||
|
||||
for hub in pywink.get_hubs():
|
||||
add_devices([WinkHub(hub, hass)])
|
||||
_id = hub.object_id() + hub.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkHub(hub, hass)])
|
||||
|
||||
for remote in pywink.get_remotes():
|
||||
add_devices([WinkRemote(remote, hass)])
|
||||
_id = remote.object_id() + remote.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkRemote(remote, hass)])
|
||||
|
||||
for button in pywink.get_buttons():
|
||||
add_devices([WinkButton(button, hass)])
|
||||
_id = button.object_id() + button.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkButton(button, hass)])
|
||||
|
||||
for gang in pywink.get_gangs():
|
||||
add_devices([WinkGang(gang, hass)])
|
||||
_id = gang.object_id() + gang.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkGang(gang, hass)])
|
||||
|
||||
for door_bell_sensor in pywink.get_door_bells():
|
||||
_id = door_bell_sensor.object_id() + door_bell_sensor.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkBinarySensorDevice(door_bell_sensor, hass)])
|
||||
|
||||
for camera_sensor in pywink.get_cameras():
|
||||
_id = camera_sensor.object_id() + camera_sensor.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
try:
|
||||
if camera_sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(camera_sensor, hass)])
|
||||
except AttributeError:
|
||||
_LOGGER.info("Device isn't a sensor, skipping.")
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
|
@ -58,8 +92,14 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
self._unit_of_measurement = self.wink.unit()
|
||||
self.capability = self.wink.capability()
|
||||
try:
|
||||
self._unit_of_measurement = self.wink.unit()
|
||||
except AttributeError:
|
||||
self._unit_of_measurement = None
|
||||
try:
|
||||
self.capability = self.wink.capability()
|
||||
except AttributeError:
|
||||
self.capability = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -72,17 +112,27 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
|
||||
class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink Hub."""
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the hub sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.wink.state()
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'test_activated': self.wink.test_activated()
|
||||
}
|
||||
|
||||
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Hub."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -93,17 +143,12 @@ class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
|||
}
|
||||
|
||||
|
||||
class WinkRemote(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Lutron Connected bulb remote."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the hub sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.wink.state()
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -115,18 +160,18 @@ class WinkRemote(WinkDevice, BinarySensorDevice, Entity):
|
|||
'button_down_pressed': self.wink.button_down_pressed()
|
||||
}
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return None
|
||||
|
||||
class WinkButton(WinkDevice, BinarySensorDevice, Entity):
|
||||
|
||||
class WinkButton(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay button."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the hub sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.wink.state()
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -137,12 +182,12 @@ class WinkButton(WinkDevice, BinarySensorDevice, Entity):
|
|||
}
|
||||
|
||||
|
||||
class WinkGang(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay gang."""
|
||||
|
||||
def __init(self, wink, hass):
|
||||
"""Initialize the gang sensor."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -9,6 +9,7 @@ import datetime
|
|||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
|
@ -16,22 +17,6 @@ from homeassistant.components.binary_sensor import (
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = []
|
||||
|
||||
PHILIO = 0x013c
|
||||
PHILIO_SLIM_SENSOR = 0x0002
|
||||
PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0)
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000d
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION = (PHILIO, PHILIO_3_IN_1_SENSOR_GEN_4, 0)
|
||||
WENZHOU = 0x0118
|
||||
WENZHOU_SLIM_SENSOR_MOTION = (WENZHOU, PHILIO_SLIM_SENSOR, 0)
|
||||
|
||||
WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
PHILIO_3_IN_1_SENSOR_GEN_4_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
WENZHOU_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Z-Wave platform for binary sensors."""
|
||||
|
@ -42,23 +27,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
|
||||
# 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()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16),
|
||||
value.index)
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node,
|
||||
9) or 4)
|
||||
add_devices([
|
||||
ZWaveTriggerSensor(value, "motion",
|
||||
hass, re_arm_multiplier * 8)
|
||||
])
|
||||
return
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
return
|
||||
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
add_devices([ZWaveBinarySensor(value, None)])
|
||||
|
|
|
@ -122,8 +122,6 @@ def set_away_mode(hass, away_mode, entity_id=None):
|
|||
if entity_id:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
_LOGGER.warning(
|
||||
'This service has been deprecated; use climate.set_hold_mode')
|
||||
hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data)
|
||||
|
||||
|
||||
|
@ -243,14 +241,6 @@ def async_setup(hass, config):
|
|||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
if away_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE)
|
||||
return
|
||||
|
||||
_LOGGER.warning(
|
||||
'This service has been deprecated; use climate.set_hold_mode')
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
|
@ -288,12 +278,6 @@ def async_setup(hass, config):
|
|||
|
||||
aux_heat = service.data.get(ATTR_AUX_HEAT)
|
||||
|
||||
if aux_heat is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT)
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
|
@ -340,12 +324,6 @@ def async_setup(hass, config):
|
|||
|
||||
humidity = service.data.get(ATTR_HUMIDITY)
|
||||
|
||||
if humidity is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_HUMIDITY, ATTR_HUMIDITY)
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_humidity(humidity)
|
||||
|
||||
|
@ -363,12 +341,6 @@ def async_setup(hass, config):
|
|||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
if fan is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_FAN_MODE, ATTR_FAN_MODE)
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_fan_mode(fan)
|
||||
|
||||
|
@ -386,12 +358,6 @@ def async_setup(hass, config):
|
|||
|
||||
operation_mode = service.data.get(ATTR_OPERATION_MODE)
|
||||
|
||||
if operation_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_OPERATION_MODE, ATTR_OPERATION_MODE)
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_operation_mode(operation_mode)
|
||||
|
||||
|
@ -409,12 +375,6 @@ def async_setup(hass, config):
|
|||
|
||||
swing_mode = service.data.get(ATTR_SWING_MODE)
|
||||
|
||||
if swing_mode is None:
|
||||
_LOGGER.error(
|
||||
"Received call to %s without attribute %s",
|
||||
SERVICE_SET_SWING_MODE, ATTR_SWING_MODE)
|
||||
return
|
||||
|
||||
for climate in target_climate:
|
||||
yield from climate.async_set_swing_mode(swing_mode)
|
||||
|
||||
|
|
|
@ -135,27 +135,27 @@ class DemoClimate(ClimateDevice):
|
|||
kwargs.get(ATTR_TARGET_TEMP_LOW) is not None:
|
||||
self._target_temperature_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_humidity(self, humidity):
|
||||
"""Set new target temperature."""
|
||||
self._target_humidity = humidity
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_swing_mode = swing_mode
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
self._current_fan_mode = fan
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
self._current_operation = operation_mode
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
|
@ -170,24 +170,24 @@ class DemoClimate(ClimateDevice):
|
|||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_hold_mode(self, hold):
|
||||
"""Update hold mode on."""
|
||||
self._hold = hold
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_on(self):
|
||||
"""Turn away auxillary heater on."""
|
||||
self._aux = True
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_aux_heat_off(self):
|
||||
"""Turn auxillary heater off."""
|
||||
self._aux = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -69,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
for thermostat in target_thermostats:
|
||||
thermostat.set_fan_min_on_time(str(fan_min_on_time))
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
thermostat.schedule_update_ha_state(True)
|
||||
|
||||
def resume_program_set_service(service):
|
||||
"""Resume the program on the target thermostats."""
|
||||
|
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
for thermostat in target_thermostats:
|
||||
thermostat.resume_program(resume_all)
|
||||
|
||||
thermostat.update_ha_state(True)
|
||||
thermostat.schedule_update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
@ -186,12 +186,27 @@ class Thermostat(ClimateDevice):
|
|||
@property
|
||||
def current_hold_mode(self):
|
||||
"""Return current hold mode."""
|
||||
if self.is_away_mode_on:
|
||||
events = self.thermostat['events']
|
||||
if any((event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1)
|
||||
or event['type'] == 'autoAway'
|
||||
for event in events):
|
||||
# away hold is auto away or a temporary hold from away climate
|
||||
hold = 'away'
|
||||
elif self.is_home_mode_on:
|
||||
elif any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events):
|
||||
# a permanent away is not considered a hold, but away_mode
|
||||
hold = None
|
||||
elif any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events):
|
||||
# home mode is auto home or any home hold
|
||||
hold = 'home'
|
||||
elif self.is_temp_hold_on():
|
||||
elif any(event['type'] == 'hold' and event['running']
|
||||
for event in events):
|
||||
hold = 'temp'
|
||||
# temperature hold is any other hold not based on climate
|
||||
else:
|
||||
hold = None
|
||||
return hold
|
||||
|
@ -255,42 +270,23 @@ class Thermostat(ClimateDevice):
|
|||
return any(event['type'] == 'vacation' and event['running']
|
||||
for event in events)
|
||||
|
||||
def is_temp_hold_on(self):
|
||||
"""Return true if temperature hold is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['type'] == 'hold' and event['running']
|
||||
for event in events)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'away' or
|
||||
event['type'] == 'autoAway'
|
||||
return any(event['holdClimateRef'] == 'away' and
|
||||
int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1
|
||||
for event in events)
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
"away", 'indefinite')
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
self.set_hold_mode(None)
|
||||
|
||||
@property
|
||||
def is_home_mode_on(self):
|
||||
"""Return true if home mode is on."""
|
||||
events = self.thermostat['events']
|
||||
return any(event['holdClimateRef'] == 'home' or
|
||||
event['type'] == 'autoHome'
|
||||
for event in events)
|
||||
|
||||
def turn_home_mode_on(self):
|
||||
"""Turn home on."""
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
self.data.ecobee.resume_program(self.thermostat_index)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def set_hold_mode(self, hold_mode):
|
||||
|
@ -298,11 +294,14 @@ class Thermostat(ClimateDevice):
|
|||
hold = self.current_hold_mode
|
||||
|
||||
if hold == hold_mode:
|
||||
# no change, so no action required
|
||||
return
|
||||
elif hold_mode == 'away':
|
||||
self.turn_away_mode_on()
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"away", self.hold_preference())
|
||||
elif hold_mode == 'home':
|
||||
self.turn_home_mode_on()
|
||||
self.data.ecobee.set_climate_hold(self.thermostat_index,
|
||||
"home", self.hold_preference())
|
||||
elif hold_mode == 'temp':
|
||||
self.set_temp_hold(int(self.current_temperature))
|
||||
else:
|
||||
|
@ -378,17 +377,8 @@ class Thermostat(ClimateDevice):
|
|||
default = self.thermostat['settings']['holdAction']
|
||||
if default == 'nextTransition':
|
||||
return default
|
||||
elif default == 'indefinite':
|
||||
return default
|
||||
# add further conditions if other hold durations should be
|
||||
# supported; note that this should not include 'indefinite'
|
||||
# as an indefinite away hold is interpreted as away_mode
|
||||
else:
|
||||
return 'nextTransition'
|
||||
|
||||
# Sleep mode isn't used in UI yet:
|
||||
|
||||
# def turn_sleep_mode_on(self):
|
||||
# """ Turns sleep mode on. """
|
||||
# self.data.ecobee.set_climate_hold(self.thermostat_index, "sleep")
|
||||
|
||||
# def turn_sleep_mode_off(self):
|
||||
# """ Turns sleep mode off. """
|
||||
# self.data.ecobee.resume_program(self.thermostat_index)
|
||||
|
|
|
@ -10,14 +10,14 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
|
||||
STATE_UNKNOWN, STATE_AUTO, STATE_ON, STATE_OFF,
|
||||
STATE_AUTO, STATE_ON, STATE_OFF,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.4']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -61,15 +61,12 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
# we want to avoid name clash with this module..
|
||||
import eq3bt as eq3
|
||||
|
||||
self.modes = {None: STATE_UNKNOWN, # When not yet connected.
|
||||
eq3.Mode.Unknown: STATE_UNKNOWN,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
# away handled separately, here just for reverse mapping
|
||||
eq3.Mode.Away: STATE_AWAY,
|
||||
self.modes = {eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST}
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
|
||||
|
@ -79,7 +76,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if thermostat is available."""
|
||||
return self.current_operation != STATE_UNKNOWN
|
||||
return self.current_operation is not None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -116,6 +113,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
|||
@property
|
||||
def current_operation(self):
|
||||
"""Current mode."""
|
||||
if self._thermostat.mode < 0:
|
||||
return None
|
||||
return self.modes[self._thermostat.mode]
|
||||
|
||||
@property
|
||||
|
|
|
@ -4,17 +4,19 @@ Adds support for generic thermostat units.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.generic_thermostat/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import switch
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE)
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -48,7 +50,8 @@ PLATFORM_SCHEMA = 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 generic thermostat."""
|
||||
name = config.get(CONF_NAME)
|
||||
heater_entity_id = config.get(CONF_HEATER)
|
||||
|
@ -60,7 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
|
||||
add_devices([GenericThermostat(
|
||||
yield from async_add_devices([GenericThermostat(
|
||||
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
|
||||
target_temp, ac_mode, min_cycle_duration, tolerance)])
|
||||
|
||||
|
@ -86,12 +89,14 @@ class GenericThermostat(ClimateDevice):
|
|||
self._target_temp = target_temp
|
||||
self._unit = hass.config.units.temperature_unit
|
||||
|
||||
track_state_change(hass, sensor_entity_id, self._sensor_changed)
|
||||
track_state_change(hass, heater_entity_id, self._switch_changed)
|
||||
async_track_state_change(
|
||||
hass, sensor_entity_id, self._async_sensor_changed)
|
||||
async_track_state_change(
|
||||
hass, heater_entity_id, self._async_switch_changed)
|
||||
|
||||
sensor_state = hass.states.get(sensor_entity_id)
|
||||
if sensor_state:
|
||||
self._update_temp(sensor_state)
|
||||
self._async_update_temp(sensor_state)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -128,14 +133,15 @@ class GenericThermostat(ClimateDevice):
|
|||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
self._async_control_heating()
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@ -157,22 +163,25 @@ class GenericThermostat(ClimateDevice):
|
|||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
def _sensor_changed(self, entity_id, old_state, new_state):
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
"""Called when temperature changes."""
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._update_temp(new_state)
|
||||
self._control_heating()
|
||||
self.schedule_update_ha_state()
|
||||
self._async_update_temp(new_state)
|
||||
self._async_control_heating()
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
def _switch_changed(self, entity_id, old_state, new_state):
|
||||
@callback
|
||||
def _async_switch_changed(self, entity_id, old_state, new_state):
|
||||
"""Called when heater switch changes state."""
|
||||
if new_state is None:
|
||||
return
|
||||
self.schedule_update_ha_state()
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
def _update_temp(self, state):
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from sensor."""
|
||||
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
|
@ -182,7 +191,8 @@ class GenericThermostat(ClimateDevice):
|
|||
except ValueError as ex:
|
||||
_LOGGER.error('Unable to update from sensor: %s', ex)
|
||||
|
||||
def _control_heating(self):
|
||||
@callback
|
||||
def _async_control_heating(self):
|
||||
"""Check if we need to turn heating on or off."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
|
@ -198,9 +208,9 @@ class GenericThermostat(ClimateDevice):
|
|||
current_state = STATE_ON
|
||||
else:
|
||||
current_state = STATE_OFF
|
||||
long_enough = condition.state(self.hass, self.heater_entity_id,
|
||||
current_state,
|
||||
self.min_cycle_duration)
|
||||
long_enough = condition.state(
|
||||
self.hass, self.heater_entity_id, current_state,
|
||||
self.min_cycle_duration)
|
||||
if not long_enough:
|
||||
return
|
||||
|
||||
|
@ -210,12 +220,12 @@ class GenericThermostat(ClimateDevice):
|
|||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||
if too_hot:
|
||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
is_heating = self._is_device_active
|
||||
if is_heating:
|
||||
|
@ -223,12 +233,12 @@ class GenericThermostat(ClimateDevice):
|
|||
if too_hot:
|
||||
_LOGGER.info('Turning off heater %s',
|
||||
self.heater_entity_id)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||
if too_cold:
|
||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||
switch.turn_on(self.hass, self.heater_entity_id)
|
||||
switch.async_turn_on(self.hass, self.heater_entity_id)
|
||||
|
||||
@property
|
||||
def _is_device_active(self):
|
||||
|
|
|
@ -135,7 +135,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
|||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[value_type] = value
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target temperature."""
|
||||
|
@ -145,7 +145,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
|||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[set_req.V_HVAC_SPEED] = fan
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target temperature."""
|
||||
|
@ -156,7 +156,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
|||
if self.gateway.optimistic:
|
||||
# optimistically assume that switch has changed state
|
||||
self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the controller with the latest value from a sensor."""
|
||||
|
|
|
@ -111,7 +111,6 @@ class NetatmoThermostat(ClimateDevice):
|
|||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
|
@ -119,7 +118,6 @@ class NetatmoThermostat(ClimateDevice):
|
|||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
|
@ -131,7 +129,6 @@ class NetatmoThermostat(ClimateDevice):
|
|||
mode, temperature, endTimeOffset)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
|
|
|
@ -78,7 +78,7 @@ set_fan_mode:
|
|||
description: Name(s) of entities to change
|
||||
example: 'climate.nest'
|
||||
|
||||
fan:
|
||||
fan_mode:
|
||||
description: New value of fan mode
|
||||
example: On Low
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ Support for Wink thermostats.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
|
@ -13,12 +13,16 @@ from homeassistant.components.climate import (
|
|||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
STATE_AUX = 'aux'
|
||||
STATE_ECO = 'eco'
|
||||
STATE_FAN = 'fan'
|
||||
SPEED_LOWEST = 'lowest'
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
|
@ -30,8 +34,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink thermostat."""
|
||||
import pywink
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
add_devices(WinkThermostat(thermostat, hass, temp_unit)
|
||||
for thermostat in pywink.get_thermostats())
|
||||
for climate in pywink.get_thermostats():
|
||||
_id = climate.object_id() + climate.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkThermostat(climate, hass, temp_unit)])
|
||||
for climate in pywink.get_air_conditioners():
|
||||
_id = climate.object_id() + climate.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkAC(climate, hass, temp_unit)])
|
||||
|
||||
|
||||
# pylint: disable=abstract-method,too-many-public-methods, too-many-branches
|
||||
|
@ -41,7 +51,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
|||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
|
@ -329,3 +338,131 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
|||
else:
|
||||
return_value = maximum
|
||||
return return_value
|
||||
|
||||
|
||||
class WinkAC(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink air conditioner."""
|
||||
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
# The Wink API always returns temp in Celsius
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data["total_consumption"] = self.wink.toatl_consumption()
|
||||
data["schedule_enabled"] = self.wink.toatl_consumption()
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.wink.current_temperature()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
elif self.wink.current_mode() == 'cool_only':
|
||||
current_op = STATE_COOL
|
||||
elif self.wink.current_mode() == 'auto_eco':
|
||||
current_op = STATE_ECO
|
||||
elif self.wink.current_mode() == 'fan_only':
|
||||
current_op = STATE_FAN
|
||||
else:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
if 'cool_only' in modes:
|
||||
op_list.append(STATE_COOL)
|
||||
if 'auto_eco' in modes:
|
||||
op_list.append(STATE_ECO)
|
||||
if 'fan_eco' in modes:
|
||||
op_list.append(STATE_FAN)
|
||||
return op_list
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
target_temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
self.wink.set_temperature(target_temp)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_COOL:
|
||||
self.wink.set_operation_mode('cool_only')
|
||||
elif operation_mode == STATE_ECO:
|
||||
self.wink.set_operation_mode('auto_eco')
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.wink.set_operation_mode('off')
|
||||
elif operation_mode == STATE_FAN:
|
||||
self.wink.set_operation_mode('fan_only')
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.wink.current_max_set_point()
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Only supports cool."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Only supports cool."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
speed = self.wink.current_fan_speed()
|
||||
if speed <= 0.3 and speed >= 0.0:
|
||||
return SPEED_LOWEST
|
||||
elif speed <= 0.5 and speed > 0.3:
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.8 and speed > 0.5:
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return [SPEED_LOWEST, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, mode):
|
||||
"""Set fan speed."""
|
||||
if mode == SPEED_LOWEST:
|
||||
speed = 0.3
|
||||
elif mode == SPEED_LOW:
|
||||
speed = 0.5
|
||||
elif mode == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
elif mode == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
|
|
|
@ -83,45 +83,52 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
self._current_operation = value.data
|
||||
self._operation_list = list(value.data_items)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s",
|
||||
self._current_operation)
|
||||
self._current_operation = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
||||
operation_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
member='data_items')
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
# Current Temp
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||
.values()):
|
||||
if value.label == 'Temperature':
|
||||
self._current_temperature = round((float(value.data)), 1)
|
||||
self._unit = value.units
|
||||
self._current_temperature = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='data')
|
||||
self._unit = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='units')
|
||||
|
||||
# Fan Mode
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||
.values()):
|
||||
self._current_fan_mode = value.data
|
||||
self._fan_list = list(value.data_items)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
self._current_fan_mode = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data')
|
||||
fan_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data_items')
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||
.values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
self._current_swing_mode = value.data
|
||||
self._swing_list = list(value.data_items)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
self._current_swing_mode = (
|
||||
self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data'))
|
||||
swing_list = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data_items')
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
temps = []
|
||||
for value in (
|
||||
|
@ -139,19 +146,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
break
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
|
||||
# Operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const
|
||||
.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()):
|
||||
self._operating_state = value.data
|
||||
self._operating_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
member='data')
|
||||
|
||||
# Fan operating state
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE)
|
||||
.values()):
|
||||
self._fan_state = value.data
|
||||
self._fan_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
member='data')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -215,50 +219,29 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
else:
|
||||
return
|
||||
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
if value.index == self._index:
|
||||
if self._zxt_120:
|
||||
# ZXT-120 responds only to whole int
|
||||
value.data = round(temperature, 0)
|
||||
self._target_temperature = temperature
|
||||
self.update_ha_state()
|
||||
else:
|
||||
value.data = temperature
|
||||
self.update_ha_state()
|
||||
break
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.update_ha_state()
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
for value in (self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE).
|
||||
values()):
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \
|
||||
value.index == 0:
|
||||
value.data = bytes(fan, 'utf-8')
|
||||
break
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
index=0, data=bytes(fan, 'utf-8'))
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0:
|
||||
value.data = bytes(operation_mode, 'utf-8')
|
||||
break
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||
value.index == 33:
|
||||
value.data = bytes(swing_mode, 'utf-8')
|
||||
break
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
|
|
@ -4,9 +4,11 @@ Support for Cover devices.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -53,17 +55,17 @@ COVER_SET_COVER_TILT_POSITION_SCHEMA = COVER_SERVICE_SCHEMA.extend({
|
|||
})
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_OPEN_COVER: {'method': 'open_cover'},
|
||||
SERVICE_CLOSE_COVER: {'method': 'close_cover'},
|
||||
SERVICE_OPEN_COVER: {'method': 'async_open_cover'},
|
||||
SERVICE_CLOSE_COVER: {'method': 'async_close_cover'},
|
||||
SERVICE_SET_COVER_POSITION: {
|
||||
'method': 'set_cover_position',
|
||||
'method': 'async_set_cover_position',
|
||||
'schema': COVER_SET_COVER_POSITION_SCHEMA},
|
||||
SERVICE_STOP_COVER: {'method': 'stop_cover'},
|
||||
SERVICE_OPEN_COVER_TILT: {'method': 'open_cover_tilt'},
|
||||
SERVICE_CLOSE_COVER_TILT: {'method': 'close_cover_tilt'},
|
||||
SERVICE_STOP_COVER_TILT: {'method': 'stop_cover_tilt'},
|
||||
SERVICE_STOP_COVER: {'method': 'async_stop_cover'},
|
||||
SERVICE_OPEN_COVER_TILT: {'method': 'async_open_cover_tilt'},
|
||||
SERVICE_CLOSE_COVER_TILT: {'method': 'async_close_cover_tilt'},
|
||||
SERVICE_STOP_COVER_TILT: {'method': 'async_stop_cover_tilt'},
|
||||
SERVICE_SET_COVER_TILT_POSITION: {
|
||||
'method': 'set_cover_tilt_position',
|
||||
'method': 'async_set_cover_tilt_position',
|
||||
'schema': COVER_SET_COVER_TILT_POSITION_SCHEMA},
|
||||
}
|
||||
|
||||
|
@ -124,40 +126,53 @@ def stop_cover_tilt(hass, entity_id=None):
|
|||
hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for covers."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_cover_service(service):
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_cover_service(service):
|
||||
"""Handle calls to the cover services."""
|
||||
covers = component.async_extract_from_service(service)
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
params = service.data.copy()
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
if not method:
|
||||
return
|
||||
|
||||
covers = component.extract_from_service(service)
|
||||
|
||||
# call method
|
||||
for cover in covers:
|
||||
getattr(cover, method['method'])(**params)
|
||||
yield from getattr(cover, method['method'])(**params)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
cover.update_ha_state(True)
|
||||
update_coro = hass.loop.create_task(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service_name].get(
|
||||
'schema', COVER_SERVICE_SCHEMA)
|
||||
hass.services.register(DOMAIN, service_name, handle_cover_service,
|
||||
descriptions.get(service_name), schema=schema)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, async_handle_cover_service,
|
||||
descriptions.get(service_name), schema=schema)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -215,30 +230,94 @@ class CoverDevice(Entity):
|
|||
"""Open the cover."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Open the cover.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.open_cover, **kwargs))
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Close cover.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.close_cover, **kwargs))
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
pass
|
||||
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_cover_position, **kwargs))
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
pass
|
||||
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.stop_cover, **kwargs))
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt."""
|
||||
pass
|
||||
|
||||
def async_open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.open_cover_tilt, **kwargs))
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt."""
|
||||
pass
|
||||
|
||||
def async_close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.close_cover_tilt, **kwargs))
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
pass
|
||||
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.set_cover_tilt_position, **kwargs))
|
||||
|
||||
def stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
pass
|
||||
|
||||
def async_stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the cover.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.stop_cover_tilt, **kwargs))
|
||||
|
|
|
@ -149,7 +149,7 @@ class DemoCover(CoverDevice):
|
|||
|
||||
if self._position in (100, 0, self._set_position):
|
||||
self.stop_cover()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _listen_cover_tilt(self):
|
||||
"""Listen for changes in cover tilt."""
|
||||
|
@ -167,4 +167,4 @@ class DemoCover(CoverDevice):
|
|||
if self._tilt_position in (100, 0, self._set_tilt_position):
|
||||
self.stop_cover_tilt()
|
||||
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -199,8 +199,7 @@ class GaradgetCover(CoverDevice):
|
|||
|
||||
def _check_state(self, now):
|
||||
"""Check the state of the service during an operation."""
|
||||
self.update()
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
|
|
|
@ -151,7 +151,7 @@ class MqttCover(CoverDevice):
|
|||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
|
@ -160,7 +160,7 @@ class MqttCover(CoverDevice):
|
|||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
|
|
|
@ -75,7 +75,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
|||
self._values[set_req.V_DIMMER] = 100
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
|
@ -88,7 +88,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
|||
self._values[set_req.V_DIMMER] = 0
|
||||
else:
|
||||
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
|
@ -99,7 +99,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
|||
if self.gateway.optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._values[set_req.V_DIMMER] = position
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the device."""
|
||||
|
|
|
@ -6,7 +6,7 @@ https://home-assistant.io/components/cover.wink/
|
|||
"""
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
|
@ -15,10 +15,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink cover platform."""
|
||||
import pywink
|
||||
|
||||
add_devices(WinkCoverDevice(shade, hass) for shade in
|
||||
pywink.get_shades())
|
||||
add_devices(WinkCoverDevice(door, hass) for door in
|
||||
pywink.get_garage_doors())
|
||||
for shade in pywink.get_shades():
|
||||
_id = shade.object_id() + shade.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCoverDevice(shade, hass)])
|
||||
for door in pywink.get_garage_doors():
|
||||
_id = door.object_id() + door.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCoverDevice(door, hass)])
|
||||
|
||||
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
|
@ -26,7 +30,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
|
|||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
super().__init__(wink, hass)
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the shade."""
|
||||
|
@ -36,13 +40,17 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
|
|||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self.wink.set_state(float(position)/100)
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of roller shutter."""
|
||||
return int(self.wink.state()*100)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
state = self.wink.state()
|
||||
if state == 0:
|
||||
return True
|
||||
elif state == 1:
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
return bool(state == 0)
|
||||
|
|
|
@ -52,12 +52,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
def __init__(self, value):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
import libopenzwave
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._lozwmgr = libopenzwave.PyManager()
|
||||
self._lozwmgr.create()
|
||||
self._node = value.node
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position = None
|
||||
self._workaround = None
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
|
@ -73,12 +72,15 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||
value.label == 'Level':
|
||||
self._current_position = value.data
|
||||
self._current_position = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='data')
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -104,27 +106,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Up':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
zwave.NETWORK.manager.pressButton(self._open_id)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Close':
|
||||
self._lozwmgr.pressButton(value.value_id)
|
||||
break
|
||||
zwave.NETWORK.manager.pressButton(self._close_id)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
|
@ -132,15 +118,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
for value in self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||
if value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Open' or value.command_class == \
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||
'Down':
|
||||
self._lozwmgr.releaseButton(value.value_id)
|
||||
break
|
||||
zwave.NETWORK.manager.releaseButton(self._open_id)
|
||||
|
||||
|
||||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
|
|
|
@ -4,6 +4,7 @@ Provides functionality to turn on lights based on the states.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_sun_light_trigger/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
|
@ -12,8 +13,8 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.helpers.event_decorators import track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_time, async_track_state_change)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -42,20 +43,20 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""The triggers to turn lights on or off based on device presence."""
|
||||
logger = logging.getLogger(__name__)
|
||||
device_tracker = get_component('device_tracker')
|
||||
group = get_component('group')
|
||||
light = get_component('light')
|
||||
sun = get_component('sun')
|
||||
|
||||
disable_turn_off = config[DOMAIN].get(CONF_DISABLE_TURN_OFF)
|
||||
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
|
||||
light.ENTITY_ID_ALL_LIGHTS)
|
||||
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE)
|
||||
device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
|
||||
device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
conf = config[DOMAIN]
|
||||
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
|
||||
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
|
||||
light_profile = conf.get(CONF_LIGHT_PROFILE)
|
||||
device_group = conf.get(CONF_DEVICE_GROUP,
|
||||
device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||
device_entity_ids = group.get_entity_ids(hass, device_group,
|
||||
device_tracker.DOMAIN)
|
||||
|
||||
|
@ -74,6 +75,8 @@ def setup(hass, config):
|
|||
"""Calculate the time when to start fading lights in when sun sets.
|
||||
|
||||
Returns None if no next_setting data available.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
next_setting = sun.next_setting(hass)
|
||||
if not next_setting:
|
||||
|
@ -81,22 +84,26 @@ def setup(hass, config):
|
|||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
|
||||
def async_turn_on_before_sunset(light_id):
|
||||
"""Helper function to turn on lights.
|
||||
|
||||
Speed is slow if there are devices home and the light is not on yet.
|
||||
"""
|
||||
"""Helper function to turn on lights."""
|
||||
if not device_tracker.is_on(hass) or light.is_on(hass, light_id):
|
||||
return
|
||||
light.async_turn_on(hass, light_id,
|
||||
transition=LIGHT_TRANSITION_TIME.seconds,
|
||||
profile=light_profile)
|
||||
|
||||
def async_turn_on_factory(light_id):
|
||||
"""Factory to generate turn on callbacks."""
|
||||
@callback
|
||||
def async_turn_on_light(now):
|
||||
"""Turn on specific light."""
|
||||
async_turn_on_before_sunset(light_id)
|
||||
|
||||
return async_turn_on_light
|
||||
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
@track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON,
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
@callback
|
||||
def schedule_lights_at_sun_set(hass, entity, old_state, new_state):
|
||||
def schedule_light_turn_on(entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
|
||||
We will schedule to have each light start after one another
|
||||
|
@ -106,35 +113,23 @@ def setup(hass, config):
|
|||
if not start_point:
|
||||
return
|
||||
|
||||
def async_turn_on_factory(light_id):
|
||||
"""Lambda can keep track of function parameters.
|
||||
|
||||
No local parameters. If we put the lambda directly in the below
|
||||
statement only the last light will be turned on.
|
||||
"""
|
||||
@callback
|
||||
def async_turn_on_light(now):
|
||||
"""Turn on specific light."""
|
||||
async_turn_on_before_sunset(light_id)
|
||||
|
||||
return async_turn_on_light
|
||||
|
||||
for index, light_id in enumerate(light_ids):
|
||||
track_point_in_time(hass, async_turn_on_factory(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
async_track_point_in_time(
|
||||
hass, async_turn_on_factory(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
|
||||
async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on,
|
||||
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
|
||||
|
||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||
# event.
|
||||
if sun.is_on(hass):
|
||||
schedule_lights_at_sun_set(hass, None, None, None)
|
||||
schedule_light_turn_on(None, None, None)
|
||||
|
||||
@track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME)
|
||||
@callback
|
||||
def check_light_on_dev_state_change(hass, entity, old_state, new_state):
|
||||
def check_light_on_dev_state_change(entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
# pylint: disable=unused-variable
|
||||
lights_are_on = group.is_on(hass, light_group)
|
||||
|
||||
light_needed = not (lights_are_on or sun.is_on(hass))
|
||||
|
||||
# These variables are needed for the elif check
|
||||
|
@ -164,17 +159,25 @@ def setup(hass, config):
|
|||
# will all the following then, break.
|
||||
break
|
||||
|
||||
if not disable_turn_off:
|
||||
@track_state_change(device_group, STATE_HOME, STATE_NOT_HOME)
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(hass, entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
# pylint: disable=unused-variable
|
||||
if not group.is_on(hass, light_group):
|
||||
return
|
||||
async_track_state_change(
|
||||
hass, device_entity_ids, check_light_on_dev_state_change,
|
||||
STATE_NOT_HOME, STATE_HOME)
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
light.async_turn_off(hass, light_ids)
|
||||
if disable_turn_off:
|
||||
return True
|
||||
|
||||
@callback
|
||||
def turn_off_lights_when_all_leave(entity, old_state, new_state):
|
||||
"""Handle device group state change."""
|
||||
if not group.is_on(hass, light_group):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Everyone has left but there are lights on. Turning them off")
|
||||
light.async_turn_off(hass, light_ids)
|
||||
|
||||
async_track_state_change(
|
||||
hass, device_group, turn_off_lights_when_all_leave,
|
||||
STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
return True
|
||||
|
|
|
@ -158,10 +158,11 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
None, platform.get_scanner, hass, {DOMAIN: p_config})
|
||||
elif hasattr(platform, 'async_setup_scanner'):
|
||||
setup = yield from platform.async_setup_scanner(
|
||||
hass, p_config, tracker.async_see)
|
||||
hass, p_config, tracker.async_see, disc_info)
|
||||
elif hasattr(platform, 'setup_scanner'):
|
||||
setup = yield from hass.loop.run_in_executor(
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see)
|
||||
None, platform.setup_scanner, hass, p_config, tracker.see,
|
||||
disc_info)
|
||||
else:
|
||||
raise HomeAssistantError("Invalid device_tracker platform.")
|
||||
|
||||
|
@ -193,6 +194,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
|||
discovery.async_listen(
|
||||
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_platform_discovered(platform, info):
|
||||
"""Callback to load a platform."""
|
||||
yield from async_setup_platform(platform, {}, disc_info=info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
|
||||
# Clean up stale devices
|
||||
async_track_utc_time_change(
|
||||
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||
|
|
|
@ -76,6 +76,7 @@ _IP_NEIGH_REGEX = re.compile(
|
|||
r'\w+\s'
|
||||
r'\w+\s'
|
||||
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s'
|
||||
r'\s?(router)?'
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
|
|
|
@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
"""Validate the configuration and return an Automatic scanner."""
|
||||
try:
|
||||
AutomaticDeviceScanner(hass, config, see)
|
||||
|
|
|
@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the Bluetooth LE Scanner."""
|
||||
# pylint: disable=import-error
|
||||
from gattlib import DiscoveryService
|
||||
|
|
|
@ -21,7 +21,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the Bluetooth Scanner."""
|
||||
# pylint: disable=import-error
|
||||
import bluetooth
|
||||
|
|
|
@ -4,7 +4,7 @@ import random
|
|||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the demo tracker."""
|
||||
def offset():
|
||||
"""Return random offset."""
|
||||
|
|
|
@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup an endpoint for the GPSLogger application."""
|
||||
hass.http.register_view(GPSLoggerView(see))
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
"""Set up the iCloud Scanner."""
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
|
|
@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
hass.http.register_view(LocativeView(see))
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the MQTT tracker."""
|
||||
devices = config[CONF_DEVICES]
|
||||
qos = config[CONF_QOS]
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
Support for tracking MySensors devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.mysensors/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import mysensors
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['mysensors']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the MySensors tracker."""
|
||||
def mysensors_callback(gateway, node_id):
|
||||
"""Callback for mysensors platform."""
|
||||
node = gateway.sensors[node_id]
|
||||
if node.sketch_name is None:
|
||||
_LOGGER.info('No sketch_name: node %s', node_id)
|
||||
return
|
||||
|
||||
pres = gateway.const.Presentation
|
||||
set_req = gateway.const.SetReq
|
||||
|
||||
for child in node.children.values():
|
||||
position = child.values.get(set_req.V_POSITION)
|
||||
if child.type != pres.S_GPS or position is None:
|
||||
continue
|
||||
try:
|
||||
latitude, longitude, _ = position.split(',')
|
||||
except ValueError:
|
||||
_LOGGER.error('Payload for V_POSITION %s is not of format '
|
||||
'latitude,longitude,altitude', position)
|
||||
continue
|
||||
name = '{} {} {}'.format(
|
||||
node.sketch_name, node_id, child.id)
|
||||
attr = {
|
||||
mysensors.ATTR_CHILD_ID: child.id,
|
||||
mysensors.ATTR_DESCRIPTION: child.description,
|
||||
mysensors.ATTR_DEVICE: gateway.device,
|
||||
mysensors.ATTR_NODE_ID: node_id,
|
||||
}
|
||||
see(
|
||||
dev_id=slugify(name),
|
||||
host_name=name,
|
||||
gps=(latitude, longitude),
|
||||
battery=node.battery_level,
|
||||
attributes=attr
|
||||
)
|
||||
|
||||
gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
|
||||
|
||||
for gateway in gateways:
|
||||
gateway.platform_callbacks.append(mysensors_callback)
|
||||
|
||||
return True
|
|
@ -71,7 +71,7 @@ def get_cipher():
|
|||
return (KEYLEN, decrypt)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up an OwnTracks tracker."""
|
||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||
|
@ -190,7 +190,7 @@ def setup_scanner(hass, config, see):
|
|||
return
|
||||
# OwnTracks uses - at the start of a beacon zone
|
||||
# to switch on 'hold mode' - ignore this
|
||||
location = slugify(data['desc'].lstrip("-"))
|
||||
location = data['desc'].lstrip("-")
|
||||
if location.lower() == 'home':
|
||||
location = STATE_HOME
|
||||
|
||||
|
@ -198,7 +198,7 @@ def setup_scanner(hass, config, see):
|
|||
|
||||
def enter_event():
|
||||
"""Execute enter event."""
|
||||
zone = hass.states.get("zone.{}".format(location))
|
||||
zone = hass.states.get("zone.{}".format(slugify(location)))
|
||||
with LOCK:
|
||||
if zone is None and data.get('t') == 'b':
|
||||
# Not a HA zone, and a beacon so assume mobile
|
||||
|
@ -227,7 +227,8 @@ def setup_scanner(hass, config, see):
|
|||
|
||||
if new_region:
|
||||
# Exit to previous region
|
||||
zone = hass.states.get("zone.{}".format(new_region))
|
||||
zone = hass.states.get(
|
||||
"zone.{}".format(slugify(new_region)))
|
||||
_set_gps_from_zone(kwargs, new_region, zone)
|
||||
_LOGGER.info("Exit to %s", new_region)
|
||||
see(**kwargs)
|
||||
|
|
|
@ -19,7 +19,7 @@ from datetime import timedelta
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL)
|
||||
PLATFORM_SCHEMA, DEFAULT_SCAN_INTERVAL, SOURCE_TYPE_ROUTER)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant import util
|
||||
from homeassistant import const
|
||||
|
@ -66,14 +66,14 @@ class Host:
|
|||
failed = 0
|
||||
while failed < self._count: # check more times if host in unreachable
|
||||
if self.ping():
|
||||
see(dev_id=self.dev_id)
|
||||
see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER)
|
||||
return True
|
||||
failed += 1
|
||||
|
||||
_LOGGER.debug("ping KO on ip=%s failed=%d", self.ip_address, failed)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Setup the Host objects and return the update function."""
|
||||
hosts = [Host(ip, dev_id, hass, config) for (dev_id, ip) in
|
||||
config[const.CONF_HOSTS].items()]
|
||||
|
|
|
@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
"""Return a Sky Hub 5 scanner if successful."""
|
||||
"""Return a Sky Hub scanner if successful."""
|
||||
scanner = SkyHubDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
@ -111,6 +111,9 @@ def _get_skyhub_data(url):
|
|||
def _parse_skyhub_response(data_str):
|
||||
"""Parse the Sky Hub data format."""
|
||||
pattmatch = re.search('attach_dev = \'(.*)\'', data_str)
|
||||
if pattmatch is None:
|
||||
raise IOError('Error: Impossible to fetch data from' +
|
||||
' Sky Hub. Try to reboot the router.')
|
||||
patt = pattmatch.group(1)
|
||||
|
||||
dev = [patt1.split(',') for patt1 in patt.split('<lf>')]
|
||||
|
|
|
@ -17,24 +17,24 @@ from homeassistant.components.device_tracker import (
|
|||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_AUTHKEY = "authkey"
|
||||
CONF_PRIVKEY = "privkey"
|
||||
CONF_BASEOID = "baseoid"
|
||||
REQUIREMENTS = ['pysnmp==4.3.3']
|
||||
|
||||
DEFAULT_COMMUNITY = "public"
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
CONF_PRIVKEY = 'privkey'
|
||||
CONF_BASEOID = 'baseoid'
|
||||
|
||||
DEFAULT_COMMUNITY = 'public'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||
vol.Inclusive(CONF_AUTHKEY, "keys"): cv.string,
|
||||
vol.Inclusive(CONF_PRIVKEY, "keys"): cv.string,
|
||||
vol.Inclusive(CONF_AUTHKEY, 'keys'): cv.string,
|
||||
vol.Inclusive(CONF_PRIVKEY, 'keys'): cv.string,
|
||||
vol.Required(CONF_BASEOID): cv.string
|
||||
})
|
||||
|
||||
|
@ -119,14 +119,14 @@ class SnmpScanner(DeviceScanner):
|
|||
return
|
||||
# pylint: disable=no-member
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
_LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(),
|
||||
errindex and restable[int(errindex) - 1][0] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
for _, val in resrow:
|
||||
mac = binascii.hexlify(val.asOctets()).decode('utf-8')
|
||||
_LOGGER.debug('Found mac %s', mac)
|
||||
_LOGGER.debug("Found MAC %s", mac)
|
||||
mac = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)])
|
||||
devices.append({'mac': mac})
|
||||
return devices
|
||||
|
|
|
@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
"""Validate the configuration and return a TrackR scanner."""
|
||||
TrackRDeviceScanner(hass, config, see)
|
||||
return True
|
||||
|
|
|
@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Validate the configuration and return a scanner."""
|
||||
from volvooncall import Connection
|
||||
connection = Connection(
|
||||
|
|
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
|||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.8.1']
|
||||
REQUIREMENTS = ['netdisco==0.8.2']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
|
@ -39,6 +39,8 @@ SERVICE_HANDLERS = {
|
|||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'flux_led': ('light', 'flux_led'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
|
|
@ -8,14 +8,13 @@ from homeassistant import core
|
|||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
|
||||
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, STATE_ON, STATE_OFF,
|
||||
HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ATTR_SUPPORTED_FEATURES,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||
SUPPORT_VOLUME_SET,
|
||||
ATTR_MEDIA_VOLUME_LEVEL, SUPPORT_VOLUME_SET,
|
||||
)
|
||||
from homeassistant.components.fan import (
|
||||
ATTR_SPEED, SUPPORT_SET_SPEED, SPEED_OFF, SPEED_LOW,
|
||||
|
@ -178,11 +177,10 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features &
|
||||
SUPPORT_BRIGHTNESS &
|
||||
(entity.domain == "light")) == SUPPORT_BRIGHTNESS:
|
||||
if brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
if entity.domain == "light":
|
||||
if entity_features & SUPPORT_BRIGHTNESS:
|
||||
if brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
# If the requested entity is a script add some variables
|
||||
elif entity.domain == "script":
|
||||
|
@ -195,9 +193,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||
|
||||
# If the requested entity is a media player, convert to volume
|
||||
elif entity.domain == "media_player":
|
||||
media_commands = entity.attributes.get(
|
||||
ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
|
||||
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
|
||||
if entity_features & SUPPORT_VOLUME_SET:
|
||||
if brightness is not None:
|
||||
turn_on_needed = True
|
||||
domain = entity.domain
|
||||
|
@ -215,9 +211,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||
|
||||
# If the requested entity is a fan, convert to speed
|
||||
elif entity.domain == "fan":
|
||||
functions = entity.attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (functions & SUPPORT_SET_SPEED) == SUPPORT_SET_SPEED:
|
||||
if entity_features & SUPPORT_SET_SPEED:
|
||||
if brightness is not None:
|
||||
domain = entity.domain
|
||||
# Convert 0-100 to a fan speed
|
||||
|
@ -288,9 +282,10 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
if entity.domain == "light":
|
||||
if entity_features & SUPPORT_BRIGHTNESS:
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
|
||||
elif (entity.domain == "script" or
|
||||
entity.domain == "media_player" or
|
||||
|
@ -316,8 +311,9 @@ def get_entity_state(config, entity):
|
|||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
pass
|
||||
if entity.domain == "light":
|
||||
if entity_features & SUPPORT_BRIGHTNESS:
|
||||
pass
|
||||
|
||||
elif entity.domain == "media_player":
|
||||
level = entity.attributes.get(
|
||||
|
|
|
@ -4,7 +4,9 @@ Provides functionality to interact with fans.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
@ -24,13 +26,14 @@ import homeassistant.helpers.config_validation as cv
|
|||
DOMAIN = 'fan'
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
GROUP_NAME_ALL_FANS = 'all fans'
|
||||
ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
# Bitfield of features supported by the fan entity
|
||||
ATTR_SUPPORTED_FEATURES = 'supported_features'
|
||||
SUPPORT_SET_SPEED = 1
|
||||
SUPPORT_OSCILLATE = 2
|
||||
SUPPORT_DIRECTION = 4
|
||||
|
@ -56,7 +59,6 @@ PROP_TO_ATTR = {
|
|||
'speed': ATTR_SPEED,
|
||||
'speed_list': ATTR_SPEED_LIST,
|
||||
'oscillating': ATTR_OSCILLATING,
|
||||
'supported_features': ATTR_SUPPORTED_FEATURES,
|
||||
'direction': ATTR_DIRECTION,
|
||||
} # type: dict
|
||||
|
||||
|
@ -88,7 +90,32 @@ FAN_SET_DIRECTION_SCHEMA = vol.Schema({
|
|||
vol.Optional(ATTR_DIRECTION): cv.string
|
||||
}) # type: dict
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_TURN_ON: {
|
||||
'method': 'async_turn_on',
|
||||
'schema': FAN_TURN_ON_SCHEMA,
|
||||
},
|
||||
SERVICE_TURN_OFF: {
|
||||
'method': 'async_turn_off',
|
||||
'schema': FAN_TURN_OFF_SCHEMA,
|
||||
},
|
||||
SERVICE_TOGGLE: {
|
||||
'method': 'async_toggle',
|
||||
'schema': FAN_TOGGLE_SCHEMA,
|
||||
},
|
||||
SERVICE_SET_SPEED: {
|
||||
'method': 'async_set_speed',
|
||||
'schema': FAN_SET_SPEED_SCHEMA,
|
||||
},
|
||||
SERVICE_OSCILLATE: {
|
||||
'method': 'async_oscillate',
|
||||
'schema': FAN_OSCILLATE_SCHEMA,
|
||||
},
|
||||
SERVICE_SET_DIRECTION: {
|
||||
'method': 'async_set_direction',
|
||||
'schema': FAN_SET_DIRECTION_SCHEMA,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def is_on(hass, entity_id: str=None) -> bool:
|
||||
|
@ -164,60 +191,53 @@ def set_direction(hass, entity_id: str=None, direction: str=None) -> None:
|
|||
hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data)
|
||||
|
||||
|
||||
def setup(hass, config: dict) -> None:
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config: dict):
|
||||
"""Expose fan control via statemachine and services."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS)
|
||||
component.setup(config)
|
||||
|
||||
def handle_fan_service(service: str) -> None:
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_fan_service(service):
|
||||
"""Hande service call for fans."""
|
||||
# Get the validated data
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
params = service.data.copy()
|
||||
|
||||
# Convert the entity ids to valid fan ids
|
||||
target_fans = component.extract_from_service(service)
|
||||
target_fans = component.async_extract_from_service(service)
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
service_fun = None
|
||||
for service_def in [SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_SET_SPEED, SERVICE_OSCILLATE,
|
||||
SERVICE_SET_DIRECTION]:
|
||||
if service_def == service.service:
|
||||
service_fun = service_def
|
||||
break
|
||||
for fan in target_fans:
|
||||
yield from getattr(fan, method['method'])(**params)
|
||||
|
||||
if service_fun:
|
||||
for fan in target_fans:
|
||||
getattr(fan, service_fun)(**params)
|
||||
update_tasks = []
|
||||
|
||||
for fan in target_fans:
|
||||
if fan.should_poll:
|
||||
fan.update_ha_state(True)
|
||||
return
|
||||
for fan in target_fans:
|
||||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
fan.async_update_ha_state(True))
|
||||
if hasattr(fan, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
# Listen for fan service calls.
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_fan_service,
|
||||
descriptions.get(SERVICE_TURN_ON),
|
||||
schema=FAN_TURN_ON_SCHEMA)
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_fan_service,
|
||||
descriptions.get(SERVICE_TURN_OFF),
|
||||
schema=FAN_TURN_OFF_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_SPEED, handle_fan_service,
|
||||
descriptions.get(SERVICE_SET_SPEED),
|
||||
schema=FAN_SET_SPEED_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_OSCILLATE, handle_fan_service,
|
||||
descriptions.get(SERVICE_OSCILLATE),
|
||||
schema=FAN_OSCILLATE_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_DIRECTION, handle_fan_service,
|
||||
descriptions.get(SERVICE_SET_DIRECTION),
|
||||
schema=FAN_SET_DIRECTION_SCHEMA)
|
||||
for service_name in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service_name].get('schema')
|
||||
hass.services.async_register(
|
||||
DOMAIN, service_name, async_handle_fan_service,
|
||||
descriptions.get(service_name), schema=schema)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -225,34 +245,57 @@ def setup(hass, config: dict) -> None:
|
|||
class FanEntity(ToggleEntity):
|
||||
"""Representation of a fan."""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
def set_speed(self: ToggleEntity, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_speed(self: ToggleEntity, speed: str):
|
||||
"""Set the speed of the fan.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
if speed is SPEED_OFF:
|
||||
return self.async_turn_off()
|
||||
return self.hass.loop.run_in_executor(None, self.set_speed, speed)
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_direction(self: ToggleEntity, direction: str):
|
||||
"""Set the direction of the fan.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_direction, direction)
|
||||
|
||||
def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
|
||||
"""Turn on the fan."""
|
||||
if speed is SPEED_OFF:
|
||||
self.turn_off()
|
||||
return
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self: ToggleEntity, **kwargs) -> None:
|
||||
"""Turn off the fan."""
|
||||
raise NotImplementedError()
|
||||
def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs):
|
||||
"""Turn on the fan.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
if speed is SPEED_OFF:
|
||||
return self.async_turn_off()
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.turn_on, speed, **kwargs))
|
||||
|
||||
def oscillate(self: ToggleEntity, oscillating: bool) -> None:
|
||||
"""Oscillate the fan."""
|
||||
pass
|
||||
|
||||
def async_oscillate(self: ToggleEntity, oscillating: bool):
|
||||
"""Oscillate the fan.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.oscillate, oscillating)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the entity is on."""
|
||||
|
|
|
@ -56,8 +56,10 @@ class DemoFan(FanEntity):
|
|||
"""Get the list of available speeds."""
|
||||
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def turn_on(self, speed: str=SPEED_MEDIUM) -> None:
|
||||
def turn_on(self, speed: str=None) -> None:
|
||||
"""Turn on the entity."""
|
||||
if speed is None:
|
||||
speed = SPEED_MEDIUM
|
||||
self.set_speed(speed)
|
||||
|
||||
def turn_off(self) -> None:
|
||||
|
@ -68,17 +70,17 @@ class DemoFan(FanEntity):
|
|||
def set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan."""
|
||||
self._speed = speed
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self.direction = direction
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation."""
|
||||
self.oscillating = oscillating
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_direction(self) -> str:
|
||||
|
|
|
@ -29,7 +29,7 @@ STATE_TO_VALUE = {}
|
|||
for key in VALUE_TO_STATE:
|
||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||
|
||||
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
STATES = [SPEED_OFF, SPEED_LOW, 'med', SPEED_HIGH]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
|
|
@ -150,7 +150,7 @@ class MqttFan(FanEntity):
|
|||
elif payload == self._payload[STATE_OFF]:
|
||||
self._state = False
|
||||
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_STATE_TOPIC] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic[CONF_STATE_TOPIC],
|
||||
|
@ -165,7 +165,7 @@ class MqttFan(FanEntity):
|
|||
self._speed = SPEED_MEDIUM
|
||||
elif payload == self._payload[SPEED_HIGH]:
|
||||
self._speed = SPEED_HIGH
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
|
||||
mqtt.subscribe(self._hass, self._topic[CONF_SPEED_STATE_TOPIC],
|
||||
|
@ -183,7 +183,7 @@ class MqttFan(FanEntity):
|
|||
self._oscillation = True
|
||||
elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]:
|
||||
self._oscillation = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
|
||||
mqtt.subscribe(self._hass,
|
||||
|
@ -262,7 +262,7 @@ class MqttFan(FanEntity):
|
|||
self._speed = speed
|
||||
mqtt.publish(self._hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
|
||||
mqtt_payload, self._qos, self._retain)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation."""
|
||||
|
@ -274,4 +274,4 @@ class MqttFan(FanEntity):
|
|||
mqtt.publish(self._hass,
|
||||
self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
payload, self._qos, self._retain)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -10,7 +10,7 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
|
|||
SPEED_LOW, SPEED_MEDIUM,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.wink import WinkDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,7 +22,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink platform."""
|
||||
import pywink
|
||||
|
||||
add_devices(WinkFanDevice(fan, hass) for fan in pywink.get_fans())
|
||||
for fan in pywink.get_fans():
|
||||
if fan.object_id() + fan.name() not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkFanDevice(fan, hass)])
|
||||
|
||||
|
||||
class WinkFanDevice(WinkDevice, FanEntity):
|
||||
|
@ -30,7 +32,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
|
|||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the fan."""
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
super().__init__(wink, hass)
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
|
|
|
@ -6,18 +6,29 @@ https://home-assistant.io/components/ffmpeg/
|
|||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
DOMAIN = 'ffmpeg'
|
||||
REQUIREMENTS = ["ha-ffmpeg==1.2"]
|
||||
REQUIREMENTS = ["ha-ffmpeg==1.4"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_START = 'start'
|
||||
SERVICE_STOP = 'stop'
|
||||
SERVICE_RESTART = 'restart'
|
||||
|
||||
DATA_FFMPEG = 'ffmpeg'
|
||||
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
CONF_INPUT = 'input'
|
||||
CONF_FFMPEG_BIN = 'ffmpeg_bin'
|
||||
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
|
||||
|
@ -34,18 +45,89 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def start(hass, entity_id=None):
|
||||
"""Start a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_START, data)
|
||||
|
||||
|
||||
def stop(hass, entity_id=None):
|
||||
"""Stop a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_STOP, data)
|
||||
|
||||
|
||||
def restart(hass, entity_id=None):
|
||||
"""Restart a ffmpeg process on entity."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
hass.services.call(DOMAIN, SERVICE_RESTART, data)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup the FFmpeg component."""
|
||||
conf = config.get(DOMAIN, {})
|
||||
|
||||
hass.data[DATA_FFMPEG] = FFmpegManager(
|
||||
manager = FFmpegManager(
|
||||
hass,
|
||||
conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY),
|
||||
conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST)
|
||||
)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file,
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
# register service
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in manager.entities
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = manager.entities
|
||||
|
||||
tasks = []
|
||||
for device in devices:
|
||||
if service.service == SERVICE_START:
|
||||
tasks.append(device.async_start_ffmpeg())
|
||||
elif service.service == SERVICE_STOP:
|
||||
tasks.append(device.async_stop_ffmpeg())
|
||||
else:
|
||||
tasks.append(device.async_restart_ffmpeg())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
tasks.clear()
|
||||
for device in devices:
|
||||
tasks.append(device.async_update_ha_state())
|
||||
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle,
|
||||
descriptions[DOMAIN].get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, async_service_handle,
|
||||
descriptions[DOMAIN].get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, async_service_handle,
|
||||
descriptions[DOMAIN].get(SERVICE_RESTART),
|
||||
schema=SERVICE_FFMPEG_SCHEMA)
|
||||
|
||||
hass.data[DATA_FFMPEG] = manager
|
||||
return True
|
||||
|
||||
|
||||
|
@ -58,12 +140,42 @@ class FFmpegManager(object):
|
|||
self._cache = {}
|
||||
self._bin = ffmpeg_bin
|
||||
self._run_test = run_test
|
||||
self._entities = []
|
||||
|
||||
@property
|
||||
def binary(self):
|
||||
"""Return ffmpeg binary from config."""
|
||||
return self._bin
|
||||
|
||||
@property
|
||||
def entities(self):
|
||||
"""Return ffmpeg entities for services."""
|
||||
return self._entities
|
||||
|
||||
@callback
|
||||
def async_register_device(self, device):
|
||||
"""Register a ffmpeg process/device."""
|
||||
self._entities.append(device)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_shutdown(event):
|
||||
"""Stop ffmpeg process."""
|
||||
yield from device.async_stop_ffmpeg()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_shutdown)
|
||||
|
||||
# start on startup
|
||||
if device.initial_state:
|
||||
@asyncio.coroutine
|
||||
def async_start(event):
|
||||
"""Start ffmpeg process."""
|
||||
yield from device.async_start_ffmpeg()
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_start)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_run_test(self, input_source):
|
||||
"""Run test on this input. TRUE is deactivate or run correct.
|
||||
|
@ -86,3 +198,42 @@ class FFmpegManager(object):
|
|||
return False
|
||||
self._cache[input_source] = True
|
||||
return True
|
||||
|
||||
|
||||
class FFmpegBase(Entity):
|
||||
"""Interface object for ffmpeg."""
|
||||
|
||||
def __init__(self, initial_state=True):
|
||||
"""Initialize ffmpeg base object."""
|
||||
self.ffmpeg = None
|
||||
self.initial_state = initial_state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.ffmpeg.is_running
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
def async_start_ffmpeg(self):
|
||||
"""Start a ffmpeg process.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_stop_ffmpeg(self):
|
||||
"""Stop a ffmpeg process.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.ffmpeg.close()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_restart_ffmpeg(self):
|
||||
"""Stop a ffmpeg process."""
|
||||
yield from self.async_stop_ffmpeg()
|
||||
yield from self.async_start_ffmpeg()
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<link rel="mask-icon" href="/static/icons/home-assistant-icon.svg" color="#3fbbf4">
|
||||
<link rel='preload' href='{{ core_url }}' as='script'/>
|
||||
{% for panel in panels.values() -%}
|
||||
<link rel='prefetch' href='{{ panel.url }}'>
|
||||
{% endfor -%}
|
||||
|
@ -24,35 +25,28 @@
|
|||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#ha-init-skeleton {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-webkit-justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 83px;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 0pt;
|
||||
#ha-init-skeleton::before {
|
||||
display: block;
|
||||
content: "";
|
||||
height: 48px;
|
||||
background-color: #03A9F4;
|
||||
}
|
||||
|
||||
#ha-init-skeleton .message {
|
||||
transition: font-size 2s;
|
||||
font-size: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton paper-spinner {
|
||||
height: 28px;
|
||||
margin-top: 16px;
|
||||
#ha-init-skeleton.error .message {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
|
@ -60,31 +54,27 @@
|
|||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error img,
|
||||
#ha-init-skeleton.error paper-spinner {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function initError() {
|
||||
document
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
document.getElementById('ha-init-skeleton').classList.add('error');
|
||||
};
|
||||
window.noAuth = {{ no_auth }};
|
||||
window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: 'shady',
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='ha-init-skeleton'>
|
||||
<img src='/static/icons/favicon-192x192.png' height='192'>
|
||||
<paper-spinner active></paper-spinner>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||
<div class='message'>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br>
|
||||
<a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "769f3fdd4e04b34bd66c7415743cf7b5",
|
||||
"frontend.html": "d48d9a13f7d677e59b1d22c6db051207",
|
||||
"mdi.html": "7a0f14bbf3822449f9060b9c53bd7376",
|
||||
"core.js": "adfeb513cf650acf763e284d76a48d6b",
|
||||
"frontend.html": "43340b2369646b779e04a9925c225ab4",
|
||||
"mdi.html": "c1dde43ccf5667f687c418fc8daf9668",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "f19840b9a6a46f57cb064b384e1353f5",
|
||||
"panels/ha-panel-dev-info.html": "3765a371478cc66d677cf6dcc35267c6",
|
||||
"panels/ha-panel-dev-service.html": "1d223225c1c75083738033895ea3e4b5",
|
||||
"panels/ha-panel-dev-state.html": "8257d99a38358a150eafdb23fa6727e0",
|
||||
"panels/ha-panel-dev-template.html": "cbb251acabd5e7431058ed507b70522b",
|
||||
"panels/ha-panel-history.html": "9f2c72574fb6135beb1b381a4b8b7703",
|
||||
"panels/ha-panel-dev-event.html": "5c82300b3cf543a92cf4297506e450e7",
|
||||
"panels/ha-panel-dev-info.html": "0469024d94d6270a8680df2be44ba916",
|
||||
"panels/ha-panel-dev-service.html": "9f749635e518a4ca7991975bdefdb10a",
|
||||
"panels/ha-panel-dev-state.html": "7d069ba8fd5379fa8f59858b8c0a7473",
|
||||
"panels/ha-panel-dev-template.html": "2b618508510afa5281c9ecae0c3a3dbd",
|
||||
"panels/ha-panel-history.html": "8955c1d093a2c417c89ed90dd627c7d3",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "313f2ac57aaa5ad55933c9bbf8d8a1e5",
|
||||
"panels/ha-panel-map.html": "13f120066c0b5faa2ce1db2c3f3cc486",
|
||||
"panels/ha-panel-logbook.html": "f36297a894524518fa70883f264492b0",
|
||||
"panels/ha-panel-map.html": "9c8c7924ba8f731560c9f4093835cc26",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 5159326a7b3d1ba29ae17a7861fa2eaa8c2c95f6
|
||||
Subproject commit f3808ff4d44733f5810e21131e0daa1425bf5f22
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1,2 +1,2 @@
|
|||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:16px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.4.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hass.config.core.version]]</p><p>Path to configuration.yaml: [[hass.config.core.config_dir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> —</p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},polymerVersion:{type:String,value:Polymer.version},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.callApi("GET","error_log").then(function(e){this.errorLog=e||"No errors have been reported."}.bind(this))}})</script></body></html>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -26,7 +26,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_UNKNOWN,
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyCEC==0.4.12']
|
||||
REQUIREMENTS = ['pyCEC==0.4.13']
|
||||
|
||||
DOMAIN = 'hdmi_cec'
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ def get_significant_states(start_time, end_time=None, entity_id=None,
|
|||
"""
|
||||
entity_ids = (entity_id.lower(), ) if entity_id is not None else None
|
||||
states = recorder.get_model('States')
|
||||
query = recorder.query('States').filter(
|
||||
query = recorder.query(states).filter(
|
||||
(states.domain.in_(SIGNIFICANT_DOMAINS) |
|
||||
(states.last_changed == states.last_updated)) &
|
||||
(states.last_updated > start_time))
|
||||
|
@ -221,12 +221,17 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
if datetime is None:
|
||||
return self.json_message('Invalid datetime', HTTP_BAD_REQUEST)
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
one_day = timedelta(days=1)
|
||||
|
||||
if datetime:
|
||||
start_time = dt_util.as_utc(datetime)
|
||||
else:
|
||||
start_time = dt_util.utcnow() - one_day
|
||||
start_time = now - one_day
|
||||
|
||||
if start_time > now:
|
||||
return self.json([])
|
||||
|
||||
end_time = start_time + one_day
|
||||
entity_id = request.GET.get('filter_entity_id')
|
||||
|
|
|
@ -4,53 +4,22 @@ Support for the demo image processing.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
|
||||
from homeassistant.components.image_processing import ImageProcessingEntity
|
||||
from homeassistant.components.image_processing import ATTR_CONFIDENCE
|
||||
from homeassistant.components.image_processing.openalpr_local import (
|
||||
ImageProcessingAlprEntity)
|
||||
from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
ImageProcessingFaceIdentifyEntity)
|
||||
ImageProcessingFaceEntity, ATTR_NAME, ATTR_AGE, ATTR_GENDER)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the demo image_processing platform."""
|
||||
add_devices([
|
||||
DemoImageProcessing('camera.demo_camera', "Demo"),
|
||||
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
|
||||
DemoImageProcessingFaceIdentify(
|
||||
'camera.demo_camera', "Demo Face Identify")
|
||||
DemoImageProcessingFace(
|
||||
'camera.demo_camera', "Demo Face")
|
||||
])
|
||||
|
||||
|
||||
class DemoImageProcessing(ImageProcessingEntity):
|
||||
"""Demo alpr image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
"""Initialize demo alpr."""
|
||||
self._name = name
|
||||
self._camera = camera_entity
|
||||
self._count = 0
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._count
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
self._count += 1
|
||||
|
||||
|
||||
class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
|
||||
"""Demo alpr image processing entity."""
|
||||
|
||||
|
@ -88,7 +57,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
|
|||
self.process_plates(demo_data, 1)
|
||||
|
||||
|
||||
class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity):
|
||||
class DemoImageProcessingFace(ImageProcessingFaceEntity):
|
||||
"""Demo face identify image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
|
@ -115,10 +84,22 @@ class DemoImageProcessingFaceIdentify(ImageProcessingFaceIdentifyEntity):
|
|||
|
||||
def process_image(self, image):
|
||||
"""Process image."""
|
||||
demo_data = {
|
||||
'Hans': 98.34,
|
||||
'Helena': 82.53,
|
||||
'Luna': 62.53,
|
||||
}
|
||||
demo_data = [
|
||||
{
|
||||
ATTR_CONFIDENCE: 98.34,
|
||||
ATTR_NAME: 'Hans',
|
||||
ATTR_AGE: 16.0,
|
||||
ATTR_GENDER: 'male',
|
||||
},
|
||||
{
|
||||
ATTR_NAME: 'Helena',
|
||||
ATTR_AGE: 28.0,
|
||||
ATTR_GENDER: 'female',
|
||||
},
|
||||
{
|
||||
ATTR_CONFIDENCE: 62.53,
|
||||
ATTR_NAME: 'Luna',
|
||||
},
|
||||
]
|
||||
|
||||
self.process_faces(demo_data, 4)
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
"""
|
||||
Component that will help set the microsoft face detect processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.microsoft_face_detect/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME)
|
||||
from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
ImageProcessingFaceEntity, ATTR_GENDER, ATTR_AGE, ATTR_GLASSES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['microsoft_face']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_IDENTIFY_FACE = 'detect_face'
|
||||
|
||||
SUPPORTED_ATTRIBUTES = [
|
||||
ATTR_AGE,
|
||||
ATTR_GENDER,
|
||||
ATTR_GLASSES
|
||||
]
|
||||
|
||||
CONF_ATTRIBUTES = 'attributes'
|
||||
DEFAULT_ATTRIBUTES = [ATTR_AGE, ATTR_GENDER]
|
||||
|
||||
|
||||
def validate_attributes(list_attributes):
|
||||
"""Validate face attributes."""
|
||||
for attr in list_attributes:
|
||||
if attr not in SUPPORTED_ATTRIBUTES:
|
||||
raise vol.Invalid("Invalid attribtue {0}".format(attr))
|
||||
return list_attributes
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ATTRIBUTES, default=DEFAULT_ATTRIBUTES):
|
||||
vol.All(cv.ensure_list, validate_attributes),
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the microsoft face detection platform."""
|
||||
api = hass.data[DATA_MICROSOFT_FACE]
|
||||
attributes = config[CONF_ATTRIBUTES]
|
||||
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(MicrosoftFaceDetectEntity(
|
||||
camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME)
|
||||
))
|
||||
|
||||
yield from async_add_devices(entities)
|
||||
|
||||
|
||||
class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity):
|
||||
"""Microsoft face api entity for identify."""
|
||||
|
||||
def __init__(self, camera_entity, api, attributes, name=None):
|
||||
"""Initialize openalpr local api."""
|
||||
super().__init__()
|
||||
|
||||
self._api = api
|
||||
self._camera = camera_entity
|
||||
self._attributes = attributes
|
||||
|
||||
if name:
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "MicrosoftFace {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_process_image(self, image):
|
||||
"""Process image.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
face_data = None
|
||||
try:
|
||||
face_data = yield from self._api.call_api(
|
||||
'post', 'detect', image, binary=True,
|
||||
params={'returnFaceAttributes': ",".join(self._attributes)})
|
||||
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Can't process image on microsoft face: %s", err)
|
||||
return
|
||||
|
||||
if face_data is None or len(face_data) < 1:
|
||||
return
|
||||
|
||||
faces = []
|
||||
for face in face_data:
|
||||
face_attr = {}
|
||||
for attr in self._attributes:
|
||||
if attr in face['faceAttributes']:
|
||||
face_attr[attr] = face['faceAttributes'][attr]
|
||||
|
||||
if face_attr:
|
||||
faces.append(face_attr)
|
||||
|
||||
self.async_process_faces(faces, len(face_data))
|
|
@ -23,11 +23,16 @@ DEPENDENCIES = ['microsoft_face']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_IDENTIFY_FACE = 'identify_face'
|
||||
EVENT_DETECT_FACE = 'image_processing.detect_face'
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_TOTAL_FACES = 'total_faces'
|
||||
ATTR_KNOWN_FACES = 'known_faces'
|
||||
ATTR_AGE = 'age'
|
||||
ATTR_GENDER = 'gender'
|
||||
ATTR_MOTION = 'motion'
|
||||
ATTR_GLASSES = 'glasses'
|
||||
ATTR_FACES = 'faces'
|
||||
|
||||
CONF_GROUP = 'group'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -52,71 +57,90 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
yield from async_add_devices(entities)
|
||||
|
||||
|
||||
class ImageProcessingFaceIdentifyEntity(ImageProcessingEntity):
|
||||
"""Base entity class for face identify/verify image processing."""
|
||||
class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
"""Base entity class for face image processing."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base face identify/verify entity."""
|
||||
self.known_faces = {} # last scan data
|
||||
self.faces = [] # last scan data
|
||||
self.total_faces = 0 # face count
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
confidence = 0
|
||||
face_name = STATE_UNKNOWN
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
# search high verify face
|
||||
for i_name, i_co in self.known_faces.items():
|
||||
if i_co > confidence:
|
||||
confidence = i_co
|
||||
face_name = i_name
|
||||
return face_name
|
||||
# no confidence support
|
||||
if not self.confidence:
|
||||
return self.total_faces
|
||||
|
||||
# search high confidence
|
||||
for face in self.faces:
|
||||
if ATTR_CONFIDENCE not in face:
|
||||
continue
|
||||
|
||||
f_co = face[ATTR_CONFIDENCE]
|
||||
if f_co > confidence:
|
||||
confidence = f_co
|
||||
for attr in [ATTR_NAME, ATTR_MOTION]:
|
||||
if attr in face:
|
||||
state = face[attr]
|
||||
break
|
||||
|
||||
return state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {
|
||||
ATTR_KNOWN_FACES: self.known_faces,
|
||||
ATTR_FACES: self.faces,
|
||||
ATTR_TOTAL_FACES: self.total_faces,
|
||||
}
|
||||
|
||||
return attr
|
||||
|
||||
def process_faces(self, known, total):
|
||||
def process_faces(self, faces, total):
|
||||
"""Send event with detected faces and store data."""
|
||||
run_callback_threadsafe(
|
||||
self.hass.loop, self.async_process_faces, known, total
|
||||
self.hass.loop, self.async_process_faces, faces, total
|
||||
).result()
|
||||
|
||||
@callback
|
||||
def async_process_faces(self, known, total):
|
||||
def async_process_faces(self, faces, total):
|
||||
"""Send event with detected faces and store data.
|
||||
|
||||
known are a dict in follow format:
|
||||
{ 'name': confidence }
|
||||
[
|
||||
{
|
||||
ATTR_CONFIDENCE: 80,
|
||||
ATTR_NAME: 'Name',
|
||||
ATTR_AGE: 12.0,
|
||||
ATTR_GENDER: 'man',
|
||||
ATTR_MOTION: 'smile',
|
||||
ATTR_GLASSES: 'sunglasses'
|
||||
},
|
||||
]
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
detect = {name: confidence for name, confidence in known.items()
|
||||
if confidence >= self.confidence}
|
||||
|
||||
# send events
|
||||
for name, confidence in detect.items():
|
||||
for face in faces:
|
||||
if ATTR_CONFIDENCE in face and self.confidence:
|
||||
if face[ATTR_CONFIDENCE] < self.confidence:
|
||||
continue
|
||||
|
||||
face.update({ATTR_ENTITY_ID: self.entity_id})
|
||||
self.hass.async_add_job(
|
||||
self.hass.bus.async_fire, EVENT_IDENTIFY_FACE, {
|
||||
ATTR_NAME: name,
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
ATTR_CONFIDENCE: confidence,
|
||||
}
|
||||
self.hass.bus.async_fire, EVENT_DETECT_FACE, face
|
||||
)
|
||||
|
||||
# update entity store
|
||||
self.known_faces = detect
|
||||
self.faces = faces
|
||||
self.total_faces = total
|
||||
|
||||
|
||||
class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity):
|
||||
class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
|
||||
"""Microsoft face api entity for identify."""
|
||||
|
||||
def __init__(self, camera_entity, api, face_group, confidence, name=None):
|
||||
|
@ -173,7 +197,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity):
|
|||
return
|
||||
|
||||
# parse data
|
||||
knwon_faces = {}
|
||||
knwon_faces = []
|
||||
total = 0
|
||||
for face in detect:
|
||||
total += 1
|
||||
|
@ -187,7 +211,10 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceIdentifyEntity):
|
|||
name = s_name
|
||||
break
|
||||
|
||||
knwon_faces[name] = data['confidence'] * 100
|
||||
knwon_faces.append({
|
||||
ATTR_NAME: name,
|
||||
ATTR_CONFIDENCE: data['confidence'] * 100,
|
||||
})
|
||||
|
||||
# process data
|
||||
self.async_process_faces(knwon_faces, total)
|
||||
|
|
|
@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
RE_ALPR_PLATE = re.compile(r"^plate\d*:")
|
||||
RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)")
|
||||
|
||||
EVENT_FOUND_PLATE = 'found_plate'
|
||||
EVENT_FOUND_PLATE = 'image_processing.found_plate'
|
||||
|
||||
ATTR_PLATE = 'plate'
|
||||
ATTR_PLATES = 'plates'
|
||||
|
|
|
@ -45,6 +45,15 @@ SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
SERVICE_SET_OPTIONS = 'set_options'
|
||||
|
||||
SERVICE_SET_OPTIONS_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_OPTIONS):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def _cv_input_select(cfg):
|
||||
"""Config validation helper for input select (Voluptuous)."""
|
||||
options = cfg[CONF_OPTIONS]
|
||||
|
@ -89,6 +98,14 @@ def select_previous(hass, entity_id):
|
|||
})
|
||||
|
||||
|
||||
def set_options(hass, entity_id, options):
|
||||
"""Set options of input_select."""
|
||||
hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTIONS: options,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Setup input select."""
|
||||
|
@ -148,6 +165,20 @@ def async_setup(hass, config):
|
|||
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
||||
schema=SERVICE_SELECT_PREVIOUS_SCHEMA)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_options_service(call):
|
||||
"""Handle a calls to the set options service."""
|
||||
target_inputs = component.async_extract_from_service(call)
|
||||
|
||||
tasks = [input_select.async_set_options(call.data[ATTR_OPTIONS])
|
||||
for input_select in target_inputs]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OPTIONS, async_set_options_service,
|
||||
schema=SERVICE_SET_OPTIONS_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
|
@ -207,3 +238,10 @@ class InputSelect(Entity):
|
|||
new_index = (current_index + offset) % len(self._options)
|
||||
self._current_option = self._options[new_index]
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_options(self, options):
|
||||
"""Set options."""
|
||||
self._current_option = options[0]
|
||||
self._options = options
|
||||
yield from self.async_update_ha_state()
|
||||
|
|
|
@ -35,7 +35,6 @@ ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights')
|
|||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
# Bitfield of features supported by the light entity
|
||||
ATTR_SUPPORTED_FEATURES = 'supported_features'
|
||||
SUPPORT_BRIGHTNESS = 1
|
||||
SUPPORT_COLOR_TEMP = 2
|
||||
SUPPORT_EFFECT = 4
|
||||
|
@ -85,7 +84,6 @@ PROP_TO_ATTR = {
|
|||
'white_value': ATTR_WHITE_VALUE,
|
||||
'effect_list': ATTR_EFFECT_LIST,
|
||||
'effect': ATTR_EFFECT,
|
||||
'supported_features': ATTR_SUPPORTED_FEATURES,
|
||||
}
|
||||
|
||||
# Service call validation schemas
|
||||
|
@ -364,8 +362,6 @@ class Light(ToggleEntity):
|
|||
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB(
|
||||
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
|
||||
data[ATTR_BRIGHTNESS])
|
||||
else:
|
||||
data[ATTR_SUPPORTED_FEATURES] = self.supported_features
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
@ -28,19 +28,21 @@ SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
|||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the demo light platform."""
|
||||
add_devices_callback([
|
||||
DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST,
|
||||
DemoLight("Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST,
|
||||
effect=LIGHT_EFFECT_LIST[0]),
|
||||
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
||||
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
||||
DemoLight("Ceiling Lights", True, True,
|
||||
LIGHT_COLORS[0], LIGHT_TEMPS[1]),
|
||||
DemoLight("Kitchen Lights", True, True,
|
||||
LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
||||
])
|
||||
|
||||
|
||||
class DemoLight(Light):
|
||||
"""Represenation of a demo light."""
|
||||
"""Representation of a demo light."""
|
||||
|
||||
def __init__(
|
||||
self, name, state, rgb=None, ct=None, brightness=180,
|
||||
xy_color=(.5, .5), white=200, effect_list=None, effect=None):
|
||||
def __init__(self, name, state, available=False, rgb=None, ct=None,
|
||||
brightness=180, xy_color=(.5, .5), white=200,
|
||||
effect_list=None, effect=None):
|
||||
"""Initialize the light."""
|
||||
self._name = name
|
||||
self._state = state
|
||||
|
@ -53,61 +55,68 @@ class DemoLight(Light):
|
|||
self._effect = effect
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed for a demo light."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the light if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
def available(self) -> bool:
|
||||
"""Return availability."""
|
||||
# This demo light is always available, but well-behaving components
|
||||
# should implement this to inform Home Assistant accordingly.
|
||||
return True
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def xy_color(self):
|
||||
def xy_color(self) -> tuple:
|
||||
"""Return the XY color value [float, float]."""
|
||||
return self._xy_color
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
def rgb_color(self) -> tuple:
|
||||
"""Return the RBG color value."""
|
||||
return self._rgb
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
def color_temp(self) -> int:
|
||||
"""Return the CT color temperature."""
|
||||
return self._ct
|
||||
|
||||
@property
|
||||
def white_value(self):
|
||||
def white_value(self) -> int:
|
||||
"""Return the white value of this light between 0..255."""
|
||||
return self._white
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
def effect_list(self) -> list:
|
||||
"""Return the list of supported effects."""
|
||||
return self._effect_list
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
def effect(self) -> str:
|
||||
"""Return the current effect."""
|
||||
return self._effect
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_DEMO
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the light on."""
|
||||
self._state = True
|
||||
|
||||
|
@ -129,9 +138,14 @@ class DemoLight(Light):
|
|||
if ATTR_EFFECT in kwargs:
|
||||
self._effect = kwargs[ATTR_EFFECT]
|
||||
|
||||
# As we have disabled polling, we need to inform
|
||||
# Home Assistant about updates in our state ourselves.
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the light off."""
|
||||
self._state = False
|
||||
|
||||
# As we have disabled polling, we need to inform
|
||||
# Home Assistant about updates in our state ourselves.
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.components.light import (
|
|||
PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['flux_led==0.12']
|
||||
REQUIREMENTS = ['flux_led==0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,10 +29,13 @@ DOMAIN = 'flux_led'
|
|||
SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
||||
SUPPORT_RGB_COLOR)
|
||||
|
||||
MODE_RGB = 'rgb'
|
||||
MODE_RGBW = 'rgbw'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(ATTR_MODE, default='rgbw'):
|
||||
vol.All(cv.string, vol.In(['rgbw', 'rgb'])),
|
||||
vol.Optional(ATTR_MODE, default=MODE_RGBW):
|
||||
vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])),
|
||||
vol.Optional(CONF_PROTOCOL, default=None):
|
||||
vol.All(cv.string, vol.In(['ledenet'])),
|
||||
})
|
||||
|
@ -48,7 +51,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
import flux_led
|
||||
lights = []
|
||||
light_ips = []
|
||||
for ipaddr, device_config in config[CONF_DEVICES].items():
|
||||
|
||||
for ipaddr, device_config in config.get(CONF_DEVICES, {}).items():
|
||||
device = {}
|
||||
device['name'] = device_config[CONF_NAME]
|
||||
device['ipaddr'] = ipaddr
|
||||
|
@ -59,7 +63,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
lights.append(light)
|
||||
light_ips.append(ipaddr)
|
||||
|
||||
if not config[CONF_AUTOMATIC_ADD]:
|
||||
if discovery_info:
|
||||
device = {}
|
||||
# discovery_info: ip address,device id,device type
|
||||
device['ipaddr'] = discovery_info[0]
|
||||
device['name'] = discovery_info[1]
|
||||
# As we don't know protocol and mode set to none to autodetect.
|
||||
device[CONF_PROTOCOL] = None
|
||||
device[ATTR_MODE] = None
|
||||
|
||||
light = FluxLight(device)
|
||||
if light.is_valid:
|
||||
lights.append(light)
|
||||
light_ips.append(device['ipaddr'])
|
||||
|
||||
if not config.get(CONF_AUTOMATIC_ADD, False):
|
||||
add_devices(lights)
|
||||
return
|
||||
|
||||
|
@ -94,10 +112,20 @@ class FluxLight(Light):
|
|||
self._mode = device[ATTR_MODE]
|
||||
self.is_valid = True
|
||||
self._bulb = None
|
||||
|
||||
try:
|
||||
self._bulb = flux_led.WifiLedBulb(self._ipaddr)
|
||||
if self._protocol:
|
||||
self._bulb.setProtocol(self._protocol)
|
||||
|
||||
# After bulb object is created the status is updated. We can
|
||||
# now set the correct mode if it was not explicitly defined.
|
||||
if not self._mode:
|
||||
if self._bulb.rgbwcapable:
|
||||
self._mode = MODE_RGBW
|
||||
else:
|
||||
self._mode = MODE_RGB
|
||||
|
||||
except socket.error:
|
||||
self.is_valid = False
|
||||
_LOGGER.error(
|
||||
|
@ -121,7 +149,7 @@ class FluxLight(Light):
|
|||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._bulb.getWarmWhite255()
|
||||
return self._bulb.brightness
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
|
|
|
@ -200,8 +200,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
|
|||
|
||||
for light_id, info in api_lights.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(hass,
|
||||
int(light_id), info,
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights,
|
||||
bridge_type, allow_unreachable,
|
||||
allow_in_emulated_hue)
|
||||
|
@ -219,7 +218,6 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
|
|||
|
||||
if lightgroup_id not in lightgroups:
|
||||
lightgroups[lightgroup_id] = HueLight(
|
||||
hass,
|
||||
int(lightgroup_id), info, bridge, update_lights,
|
||||
bridge_type, allow_unreachable, allow_in_emulated_hue,
|
||||
True)
|
||||
|
@ -282,11 +280,10 @@ def request_configuration(host, hass, add_devices, filename,
|
|||
class HueLight(Light):
|
||||
"""Representation of a Hue light."""
|
||||
|
||||
def __init__(self, hass, light_id, info, bridge, update_lights,
|
||||
def __init__(self, light_id, info, bridge, update_lights,
|
||||
bridge_type, allow_unreachable, allow_in_emulated_hue,
|
||||
is_group=False):
|
||||
"""Initialize the light."""
|
||||
self.hass = hass
|
||||
self.light_id = light_id
|
||||
self.info = info
|
||||
self.bridge = bridge
|
||||
|
@ -304,8 +301,14 @@ class HueLight(Light):
|
|||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this Hue light."""
|
||||
return "{}.{}".format(
|
||||
self.__class__, self.info.get('uniqueid', self.name))
|
||||
lid = self.info.get('uniqueid')
|
||||
|
||||
if lid is None:
|
||||
default_type = 'Group' if self.is_group else 'Light'
|
||||
ltype = self.info.get('type', default_type)
|
||||
lid = '{}.{}.{}'.format(self.name, ltype, self.light_id)
|
||||
|
||||
return '{}.{}'.format(self.__class__, lid)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -28,7 +28,9 @@ SUPPORT_HYPERION = SUPPORT_RGB_COLOR
|
|||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): cv.string,
|
||||
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR):
|
||||
vol.All(list, vol.Length(min=3, max=3),
|
||||
[vol.All(vol.Coerce(int), vol.Range(min=0, max=255))]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue