mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
commit
dc08852fc2
23
.coveragerc
23
.coveragerc
@ -11,9 +11,15 @@ omit =
|
||||
homeassistant/components/alarmdecoder.py
|
||||
homeassistant/components/*/alarmdecoder.py
|
||||
|
||||
homeassistant/components/amcrest.py
|
||||
homeassistant/components/*/amcrest.py
|
||||
|
||||
homeassistant/components/apcupsd.py
|
||||
homeassistant/components/*/apcupsd.py
|
||||
|
||||
homeassistant/components/apple_tv.py
|
||||
homeassistant/components/*/apple_tv.py
|
||||
|
||||
homeassistant/components/arduino.py
|
||||
homeassistant/components/*/arduino.py
|
||||
|
||||
@ -89,6 +95,9 @@ omit =
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
|
||||
homeassistant/components/lametric.py
|
||||
homeassistant/components/*/lametric.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@ -164,6 +173,9 @@ omit =
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twilio_call.py
|
||||
|
||||
homeassistant/components/velux.py
|
||||
homeassistant/components/*/velux.py
|
||||
|
||||
homeassistant/components/vera.py
|
||||
homeassistant/components/*/vera.py
|
||||
|
||||
@ -211,7 +223,6 @@ omit =
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/binary_sensor/tapsaff.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/ffmpeg.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
@ -301,8 +312,8 @@ omit =
|
||||
homeassistant/components/lock/nuki.py
|
||||
homeassistant/components/lock/lockitron.py
|
||||
homeassistant/components/lock/sesame.py
|
||||
homeassistant/components/media_extractor.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
|
||||
@ -339,6 +350,7 @@ omit =
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vizio.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
@ -375,11 +387,11 @@ omit =
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/prometheus.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/amcrest.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
@ -390,7 +402,7 @@ omit =
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/broadlink.py
|
||||
homeassistant/components/sensor/buienradar.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/citybikes.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
homeassistant/components/sensor/cert_expiry.py
|
||||
homeassistant/components/sensor/comed_hourly_pricing.py
|
||||
@ -404,6 +416,7 @@ omit =
|
||||
homeassistant/components/sensor/dnsip.py
|
||||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
@ -450,6 +463,7 @@ omit =
|
||||
homeassistant/components/sensor/openexchangerates.py
|
||||
homeassistant/components/sensor/opensky.py
|
||||
homeassistant/components/sensor/openweathermap.py
|
||||
homeassistant/components/sensor/otp.py
|
||||
homeassistant/components/sensor/pi_hole.py
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
@ -511,6 +525,7 @@ omit =
|
||||
homeassistant/components/switch/tplink.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_vacuum.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
|
@ -16,8 +16,8 @@ matrix:
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
# - python: "3.6-dev"
|
||||
# env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
|
41
CODEOWNERS
Normal file
41
CODEOWNERS
Normal file
@ -0,0 +1,41 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
setup.py @home-assistant/core
|
||||
homeassistant/*.py @home-assistant/core
|
||||
homeassistant/helpers/* @home-assistant/core
|
||||
homeassistant/util/* @home-assistant/core
|
||||
homeassistant/components/api.py @home-assistant/core
|
||||
homeassistant/components/automation/* @home-assistant/core
|
||||
homeassistant/components/configurator.py @home-assistant/core
|
||||
homeassistant/components/group.py @home-assistant/core
|
||||
homeassistant/components/history.py @home-assistant/core
|
||||
homeassistant/components/http/* @home-assistant/core
|
||||
homeassistant/components/input_*.py @home-assistant/core
|
||||
homeassistant/components/introduction.py @home-assistant/core
|
||||
homeassistant/components/logger.py @home-assistant/core
|
||||
homeassistant/components/mqtt/* @home-assistant/core
|
||||
homeassistant/components/panel_custom.py @home-assistant/core
|
||||
homeassistant/components/panel_iframe.py @home-assistant/core
|
||||
homeassistant/components/persistent_notification.py @home-assistant/core
|
||||
homeassistant/components/scene/__init__.py @home-assistant/core
|
||||
homeassistant/components/scene/hass.py @home-assistant/core
|
||||
homeassistant/components/script.py @home-assistant/core
|
||||
homeassistant/components/shell_command.py @home-assistant/core
|
||||
homeassistant/components/sun.py @home-assistant/core
|
||||
homeassistant/components/updater.py @home-assistant/core
|
||||
homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.py @home-assistant/core
|
||||
homeassistant/components/zone.py @home-assistant/core
|
||||
|
||||
Dockerfile @home-assistant/docker
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# Indiviudal components
|
||||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/media_player/kodi.py @armills
|
@ -229,8 +229,8 @@ def cmdline() -> List[str]:
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
@ -39,19 +39,19 @@ def is_on(hass, entity_id=None):
|
||||
else:
|
||||
entity_ids = hass.states.entity_ids()
|
||||
|
||||
for entity_id in entity_ids:
|
||||
domain = ha.split_entity_id(entity_id)[0]
|
||||
for ent_id in entity_ids:
|
||||
domain = ha.split_entity_id(ent_id)[0]
|
||||
|
||||
module = get_component(domain)
|
||||
|
||||
try:
|
||||
if module.is_on(hass, entity_id):
|
||||
if module.is_on(hass, ent_id):
|
||||
return True
|
||||
|
||||
except AttributeError:
|
||||
# module is None or method is_on does not exist
|
||||
_LOGGER.exception("Failed to call %s.is_on for %s",
|
||||
module, entity_id)
|
||||
module, ent_id)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -92,8 +92,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
@ -113,8 +113,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
else:
|
||||
return '^\\d{4,6}$'
|
||||
return '^\\d{4,6}$'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -99,8 +99,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return self._pre_trigger_state
|
||||
return self._pre_trigger_state
|
||||
|
||||
return self._state
|
||||
|
||||
|
@ -80,8 +80,7 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
else:
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN,
|
||||
CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.7']
|
||||
REQUIREMENTS = ['total_connect_client==0.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -39,10 +39,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation a Wink camera alarm."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink alarm."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
|
@ -271,14 +271,14 @@ class Alert(ToggleEntity):
|
||||
'notify', target, {'message': self._done_message})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self):
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""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):
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Async Acknowledge alert."""
|
||||
_LOGGER.debug("Acknowledged Alert: %s", self._name)
|
||||
self._ack = True
|
||||
|
149
homeassistant/components/amcrest.py
Normal file
149
homeassistant/components/amcrest.py
Normal file
@ -0,0 +1,149 @@
|
||||
"""
|
||||
This component provides basic support for Amcrest IP cameras.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/amcrest/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Amcrest IP Camera component."""
|
||||
from amcrest import AmcrestCamera
|
||||
|
||||
amcrest_cams = config[DOMAIN]
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
for device in amcrest_cams:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
try:
|
||||
camera.current_time
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
name = device.get(CONF_NAME)
|
||||
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
|
||||
sensors = device.get(CONF_SENSORS)
|
||||
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
|
||||
|
||||
username = device.get(CONF_USERNAME)
|
||||
password = device.get(CONF_PASSWORD)
|
||||
|
||||
# currently aiohttp only works with basic authentication
|
||||
# only valid for mjpeg streaming
|
||||
if username is not None and password is not None:
|
||||
if device.get(CONF_AUTHENTICATION) == HTTP_BASIC_AUTHENTICATION:
|
||||
authentication = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
authentication = None
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_AUTHENTICATION: authentication,
|
||||
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
|
||||
CONF_NAME: name,
|
||||
CONF_RESOLUTION: resolution,
|
||||
CONF_STREAM_SOURCE: stream_source,
|
||||
}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
'device': camera,
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
|
||||
return True
|
@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['apcaccess==0.0.10']
|
||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -198,8 +198,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
@ -237,8 +236,7 @@ class APIEntityStateView(HomeAssistantView):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
|
259
homeassistant/components/apple_tv.py
Normal file
259
homeassistant/components/apple_tv.py
Normal file
@ -0,0 +1,259 @@
|
||||
"""
|
||||
Support for Apple TV.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/apple_tv/
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'apple_tv'
|
||||
|
||||
SERVICE_SCAN = 'apple_tv_scan'
|
||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
||||
|
||||
ATTR_ATV = 'atv'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
CONF_LOGIN_ID = 'login_id'
|
||||
CONF_START_OFF = 'start_off'
|
||||
CONF_CREDENTIALS = 'credentials'
|
||||
|
||||
DEFAULT_NAME = 'Apple TV'
|
||||
|
||||
DATA_APPLE_TV = 'data_apple_tv'
|
||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
||||
|
||||
KEY_CONFIG = 'apple_tv_configuring'
|
||||
|
||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@asyncio.coroutine
|
||||
def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
notification = get_component('persistent_notification')
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
notification.async_create(
|
||||
hass,
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{0}'.format(credentials),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
except exceptions.DeviceAuthenticationError as ex:
|
||||
notification.async_create(
|
||||
hass,
|
||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
||||
'Details: {0}'.format(ex),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
hass, 'Apple TV Authentication', configuration_callback,
|
||||
description='Please enter PIN code shown on screen.',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = 'Home Sharing disabled'
|
||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
||||
atv.name, atv.address, login_id))
|
||||
|
||||
if not devices:
|
||||
devices = ['No device(s) found']
|
||||
|
||||
notification = get_component('persistent_notification')
|
||||
notification.async_create(
|
||||
hass,
|
||||
'The following devices were found:<br /><br />' +
|
||||
'<br /><br />'.join(devices),
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the Apple TV component."""
|
||||
if DATA_APPLE_TV not in hass.data:
|
||||
hass.data[DATA_APPLE_TV] = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Handler for service calls."""
|
||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
for device in devices:
|
||||
atv = device.atv
|
||||
if service.service == SERVICE_AUTHENTICATE:
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
elif service.service == SERVICE_SCAN:
|
||||
hass.async_add_job(scan_for_apple_tvs, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Setup an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
CONF_START_OFF: False
|
||||
})
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
descriptions.get(SERVICE_SCAN),
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
descriptions.get(SERVICE_AUTHENTICATE),
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _setup_atv(hass, atv_config):
|
||||
"""Setup an Apple TV."""
|
||||
import pyatv
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
start_off = atv_config.get(CONF_START_OFF)
|
||||
credentials = atv_config.get(CONF_CREDENTIALS)
|
||||
|
||||
if host in hass.data[DATA_APPLE_TV]:
|
||||
return
|
||||
|
||||
details = pyatv.AppleTVDevice(name, host, login_id)
|
||||
session = async_get_clientsession(hass)
|
||||
atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
|
||||
if credentials:
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
ATTR_ATV: atv,
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
"""Manager for global power management of an Apple TV.
|
||||
|
||||
An instance is used per device to share the same power state between
|
||||
several platforms.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, atv, is_off):
|
||||
"""Initialize power manager."""
|
||||
self.hass = hass
|
||||
self.atv = atv
|
||||
self.listeners = []
|
||||
self._is_on = not is_off
|
||||
|
||||
def init(self):
|
||||
"""Initialize power management."""
|
||||
if self._is_on:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
@property
|
||||
def turned_on(self):
|
||||
"""If device is on or off."""
|
||||
return self._is_on
|
||||
|
||||
def set_power_on(self, value):
|
||||
"""Change if a device is on or off."""
|
||||
if value != self._is_on:
|
||||
self._is_on = value
|
||||
if not self._is_on:
|
||||
self.atv.push_updater.stop()
|
||||
else:
|
||||
self.atv.push_updater.start()
|
||||
|
||||
for listener in self.listeners:
|
||||
self.hass.async_add_job(listener.async_update_ha_state())
|
@ -42,8 +42,6 @@ def async_trigger(hass, config, action):
|
||||
},
|
||||
})
|
||||
|
||||
# Do something to call action
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
else:
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
return async_track_sunset(hass, call_action, offset)
|
||||
|
@ -68,7 +68,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
if request.status_code is not 200:
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
@property
|
||||
|
@ -199,11 +199,10 @@ class FlicButton(BinarySensorDevice):
|
||||
"Queued %s dropped for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return True
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
_LOGGER.info(
|
||||
"Queued %s allowed for %s. Time in queue was %s",
|
||||
click_type, self.address, time_string)
|
||||
return False
|
||||
|
||||
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||
"""Update device state, if event was not queued."""
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.2']
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
@ -8,7 +8,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
@ -18,7 +18,6 @@ DEPENDENCIES = ['modbus']
|
||||
|
||||
CONF_COIL = 'coil'
|
||||
CONF_COILS = 'coils'
|
||||
CONF_SLAVE = 'slave'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COILS): [{
|
||||
@ -50,6 +49,7 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
self._coil = int(coil)
|
||||
self._value = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
@ -62,4 +62,10 @@ class ModbusCoilSensor(BinarySensorDevice):
|
||||
def update(self):
|
||||
"""Update the state of the sensor."""
|
||||
result = modbus.HUB.read_coils(self._slave, self._coil, 1)
|
||||
self._value = result.bits[0]
|
||||
try:
|
||||
self._value = result.bits[0]
|
||||
except AttributeError:
|
||||
_LOGGER.error(
|
||||
'No response from modbus slave %s coil %s',
|
||||
self._slave,
|
||||
self._coil)
|
||||
|
@ -156,8 +156,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
else:
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -12,13 +12,12 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['octoprint']
|
||||
|
||||
DOMAIN = "octoprint"
|
||||
DEFAULT_NAME = 'OctoPrint'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
@ -37,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
octoprint = get_component('octoprint')
|
||||
octoprint_api = hass.data[DOMAIN]["api"]
|
||||
name = config.get(CONF_NAME)
|
||||
monitored_conditions = config.get(
|
||||
CONF_MONITORED_CONDITIONS, SENSOR_TYPES.keys())
|
||||
@ -45,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for octo_type in monitored_conditions:
|
||||
new_sensor = OctoPrintBinarySensor(
|
||||
octoprint.OCTOPRINT, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
octoprint_api, octo_type, SENSOR_TYPES[octo_type][2],
|
||||
name, SENSOR_TYPES[octo_type][3], SENSOR_TYPES[octo_type][0],
|
||||
SENSOR_TYPES[octo_type][1], 'flags')
|
||||
devices.append(new_sensor)
|
||||
@ -98,6 +97,3 @@ class OctoPrintBinarySensor(BinarySensorDevice):
|
||||
except requests.exceptions.ConnectionError:
|
||||
# Error calling the api, already logged in api.update()
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||
|
@ -28,6 +28,7 @@ from homeassistant.util import dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_VARIABLE = 'variable'
|
||||
CONF_RESET_DELAY_SEC = 'reset_delay_sec'
|
||||
|
||||
DEFAULT_NAME = 'Pilight Binary Sensor'
|
||||
DEPENDENCIES = ['pilight']
|
||||
@ -38,7 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default='on'): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default='off'): cv.string,
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESET_DELAY_SEC, default=30): cv.positive_int
|
||||
})
|
||||
|
||||
|
||||
@ -54,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload=config.get(CONF_PAYLOAD),
|
||||
on_value=config.get(CONF_PAYLOAD_ON),
|
||||
off_value=config.get(CONF_PAYLOAD_OFF),
|
||||
rst_dly_sec=config.get(CONF_RESET_DELAY_SEC),
|
||||
)])
|
||||
else:
|
||||
add_devices([PilightBinarySensor(
|
||||
|
@ -126,14 +126,14 @@ class PingData(object):
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': ''}
|
||||
else:
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
|
||||
match = PING_MATCHER.search(str(out).split('\n')[-1])
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
return {
|
||||
'min': rtt_min,
|
||||
'avg': rtt_avg,
|
||||
'max': rtt_max,
|
||||
'mdev': rtt_mdev}
|
||||
except (subprocess.CalledProcessError, AttributeError):
|
||||
return False
|
||||
|
||||
|
@ -30,8 +30,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
return bool(val)
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
else:
|
||||
return val != 'Normal'
|
||||
return val != 'Normal'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
@ -121,10 +121,6 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@ -136,10 +132,6 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
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):
|
||||
"""Return the state attributes."""
|
||||
@ -152,10 +144,6 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Lutron Connected bulb remote."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@ -175,10 +163,6 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
class WinkButton(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay button."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@ -191,10 +175,6 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Relay gang."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the gang is connected."""
|
||||
|
@ -34,10 +34,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
|
||||
clusters = discovery_info['clusters']
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
|
||||
device_class = None
|
||||
cluster = [c for c in clusters if isinstance(c, IasZone)][0]
|
||||
cluster = in_clusters[IasZone.cluster_id]
|
||||
if discovery_info['new_join']:
|
||||
yield from cluster.bind()
|
||||
ieee = cluster.endpoint.device.application.ieee
|
||||
@ -64,7 +64,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
super().__init__(**kwargs)
|
||||
self._device_class = device_class
|
||||
from bellows.zigbee.zcl.clusters.security import IasZone
|
||||
self._ias_zone_cluster = self._clusters[IasZone.cluster_id]
|
||||
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
@ -148,8 +148,7 @@ class CalendarEventDevice(Entity):
|
||||
if 'date' in date:
|
||||
return dt.start_of_local_day(dt.dt.datetime.combine(
|
||||
dt.parse_date(date['date']), dt.dt.time.min))
|
||||
else:
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
return dt.as_local(dt.parse_datetime(date['dateTime']))
|
||||
|
||||
start = _get_date(self.data.event['start'])
|
||||
end = _get_date(self.data.event['end'])
|
||||
|
@ -266,8 +266,7 @@ class Camera(Entity):
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
return STATE_STREAMING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
|
@ -7,108 +7,59 @@ https://home-assistant.io/components/camera.amcrest/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.amcrest import (
|
||||
STREAM_SOURCE_LIST, TIMEOUT)
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEPENDENCIES = ['amcrest', 'ffmpeg']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'mjpeg'
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up an Amcrest IP Camera."""
|
||||
from amcrest import AmcrestCamera
|
||||
camera = AmcrestCamera(
|
||||
config.get(CONF_HOST), config.get(CONF_PORT),
|
||||
config.get(CONF_USERNAME), config.get(CONF_PASSWORD)).camera
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
persistent_notification = loader.get_component('persistent_notification')
|
||||
try:
|
||||
camera.current_time
|
||||
# pylint: disable=broad-except
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
persistent_notification.create(
|
||||
hass, 'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
device = discovery_info['device']
|
||||
authentication = discovery_info['authentication']
|
||||
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
|
||||
name = discovery_info['name']
|
||||
resolution = discovery_info['resolution']
|
||||
stream_source = discovery_info['stream_source']
|
||||
|
||||
async_add_devices([
|
||||
AmcrestCam(hass,
|
||||
name,
|
||||
device,
|
||||
authentication,
|
||||
ffmpeg_arguments,
|
||||
stream_source,
|
||||
resolution)], True)
|
||||
|
||||
add_devices([AmcrestCam(hass, config, camera)])
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestCam(Camera):
|
||||
"""An implementation of an Amcrest IP camera."""
|
||||
|
||||
def __init__(self, hass, device_info, camera):
|
||||
def __init__(self, hass, name, camera, authentication,
|
||||
ffmpeg_arguments, stream_source, resolution):
|
||||
"""Initialize an Amcrest camera."""
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._name = name
|
||||
self._camera = camera
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._name = device_info.get(CONF_NAME)
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||
self._stream_source = STREAM_SOURCE_LIST[
|
||||
device_info.get(CONF_STREAM_SOURCE)
|
||||
]
|
||||
self._token = self._auth = aiohttp.BasicAuth(
|
||||
device_info.get(CONF_USERNAME),
|
||||
password=device_info.get(CONF_PASSWORD)
|
||||
)
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
self._stream_source = stream_source
|
||||
self._resolution = resolution
|
||||
self._token = self._auth = authentication
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image reponse from the camera."""
|
||||
|
@ -49,7 +49,6 @@ class ArloCam(Camera):
|
||||
"""Initialize an Arlo camera."""
|
||||
super().__init__()
|
||||
self._camera = camera
|
||||
self._base_stn = hass.data[DATA_ARLO].base_stations[0]
|
||||
self._name = self._camera.name
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
@ -103,7 +102,16 @@ class ArloCam(Camera):
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
"""Set the mode in the base station."""
|
||||
self._base_stn.mode = mode
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
if base_stations:
|
||||
primary_base_station = base_stations[0]
|
||||
primary_base_station.mode = mode
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
|
@ -113,8 +113,7 @@ class NetatmoCamera(Camera):
|
||||
return "Presence"
|
||||
elif self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [
|
||||
|
@ -398,16 +398,14 @@ class ClimateDevice(Entity):
|
||||
"""Return the current state."""
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
"""Return the precision of the system."""
|
||||
if self.unit_of_measurement == TEMP_CELSIUS:
|
||||
return PRECISION_TENTHS
|
||||
else:
|
||||
return PRECISION_WHOLE
|
||||
return PRECISION_WHOLE
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@ -709,6 +707,5 @@ class ClimateDevice(Entity):
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
else:
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
@ -151,16 +151,14 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.current_operation == STATE_AUTO:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
@ -171,8 +169,7 @@ class Thermostat(ClimateDevice):
|
||||
return int(self.thermostat['runtime']['desiredHeat'] / 10)
|
||||
elif self.current_operation == STATE_COOL:
|
||||
return int(self.thermostat['runtime']['desiredCool'] / 10)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def desired_fan_mode(self):
|
||||
@ -184,8 +181,7 @@ class Thermostat(ClimateDevice):
|
||||
"""Return the current fan state."""
|
||||
if 'fan' in self.thermostat['equipmentStatus']:
|
||||
return STATE_ON
|
||||
else:
|
||||
return STATE_OFF
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def current_hold_mode(self):
|
||||
@ -199,15 +195,13 @@ class Thermostat(ClimateDevice):
|
||||
int(event['startDate'][0:4]) <= 1:
|
||||
# A temporary hold from away climate is a hold
|
||||
return 'away'
|
||||
else:
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
# A permanent hold from away climate is away_mode
|
||||
return None
|
||||
elif event['holdClimateRef'] != "":
|
||||
# Any other hold based on climate
|
||||
return event['holdClimateRef']
|
||||
else:
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
# Any hold not based on a climate is a temp hold
|
||||
return TEMPERATURE_HOLD
|
||||
elif event['type'].startswith('auto'):
|
||||
# All auto modes are treated as holds
|
||||
return event['type'][4:].lower()
|
||||
@ -222,8 +216,7 @@ class Thermostat(ClimateDevice):
|
||||
if self.operation_mode == 'auxHeatOnly' or \
|
||||
self.operation_mode == 'heatPump':
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return self.operation_mode
|
||||
return self.operation_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@ -384,8 +377,7 @@ class Thermostat(ClimateDevice):
|
||||
# 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'
|
||||
return 'nextTransition'
|
||||
|
||||
@property
|
||||
def climate_list(self):
|
||||
|
@ -145,4 +145,4 @@ class Flexit(ClimateDevice):
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new fan mode."""
|
||||
self.unit.set_fan_speed(fan)
|
||||
self.unit.set_fan_speed(self._fan_list.index(fan))
|
||||
|
@ -12,7 +12,8 @@ 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)
|
||||
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
|
||||
STATE_AUTO)
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
|
||||
CONF_NAME)
|
||||
@ -87,6 +88,7 @@ class GenericThermostat(ClimateDevice):
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
self._keep_alive = keep_alive
|
||||
self._enabled = True
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
@ -131,18 +133,39 @@ class GenericThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if not self._enabled:
|
||||
return STATE_OFF
|
||||
if self.ac_mode:
|
||||
cooling = self._active and self._is_device_active
|
||||
return STATE_COOL if cooling else STATE_IDLE
|
||||
else:
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
heating = self._active and self._is_device_active
|
||||
return STATE_HEAT if heating else STATE_IDLE
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return [STATE_AUTO, STATE_OFF]
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
switch.async_turn_off(self.hass, self.heater_entity_id)
|
||||
else:
|
||||
_LOGGER.error('Unrecognized operation mode: %s', operation_mode)
|
||||
return
|
||||
# Ensure we updae the current operation after changing the mode
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@ -159,9 +182,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
# get default temp from super class
|
||||
return ClimateDevice.min_temp.fget(self)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
@ -169,9 +192,9 @@ class GenericThermostat(ClimateDevice):
|
||||
# pylint: disable=no-member
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
# Get default temp from super class
|
||||
return ClimateDevice.max_temp.fget(self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_sensor_changed(self, entity_id, old_state, new_state):
|
||||
@ -221,6 +244,9 @@ class GenericThermostat(ClimateDevice):
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if self.min_cycle_duration:
|
||||
if self._is_device_active:
|
||||
current_state = STATE_ON
|
||||
|
@ -46,8 +46,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
|
@ -60,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if region == 'us':
|
||||
return _setup_us(username, password, config, add_devices)
|
||||
else:
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
return _setup_round(username, password, config, add_devices)
|
||||
|
||||
|
||||
def _setup_round(username, password, config, add_devices):
|
||||
@ -251,8 +251,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._device.system_mode == 'cool':
|
||||
return self._device.setpoint_cool
|
||||
else:
|
||||
return self._device.setpoint_heat
|
||||
return self._device.setpoint_heat
|
||||
|
||||
@property
|
||||
def current_operation(self: ClimateDevice) -> str:
|
||||
|
@ -109,16 +109,14 @@ class NestThermostat(ClimateDevice):
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
|
||||
return self._target_temperature
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@ -129,8 +127,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_high(self):
|
||||
@ -141,8 +138,7 @@ class NestThermostat(ClimateDevice):
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
@ -188,9 +184,8 @@ class NestThermostat(ClimateDevice):
|
||||
if self._has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self._fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
@ -119,14 +119,14 @@ class NetatmoThermostat(ClimateDevice):
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
mode, temperature, DEFAULT_TIME_OFFSET)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
|
||||
|
@ -92,8 +92,7 @@ class ThermostatDevice(ClimateDevice):
|
||||
"""Return current operation i.e. heat, cool, idle."""
|
||||
if self._state:
|
||||
return STATE_HEAT
|
||||
else:
|
||||
return STATE_IDLE
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
@ -136,24 +136,53 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._away
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._current_temperature = self.device.temp['raw']
|
||||
"""Update and validate the data from the thermostat."""
|
||||
current_temp = self.device.temp['raw']
|
||||
if current_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid temperature reading")
|
||||
return
|
||||
self._current_temperature = current_temp
|
||||
self._name = self.device.name['raw']
|
||||
self._fmode = self.device.fmode['human']
|
||||
self._tmode = self.device.tmode['human']
|
||||
self._tstate = self.device.tstate['human']
|
||||
try:
|
||||
self._fmode = self.device.fmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid fan mode reading")
|
||||
try:
|
||||
self._tmode = self.device.tmode['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat mode reading")
|
||||
try:
|
||||
self._tstate = self.device.tstate['human']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Couldn't get valid thermostat state reading")
|
||||
|
||||
if self._tmode == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_COOL
|
||||
elif self._tmode == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_HEAT
|
||||
elif self._tmode == 'Auto':
|
||||
if self._tstate == 'Cool':
|
||||
self._target_temperature = self.device.t_cool['raw']
|
||||
target_temp = self.device.t_cool['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
elif self._tstate == 'Heat':
|
||||
self._target_temperature = self.device.t_heat['raw']
|
||||
target_temp = self.device.t_heat['raw']
|
||||
if target_temp == -1:
|
||||
_LOGGER.error("Couldn't get valid target reading")
|
||||
return
|
||||
self._target_temperature = target_temp
|
||||
self._current_operation = STATE_AUTO
|
||||
else:
|
||||
self._current_operation = STATE_IDLE
|
||||
|
@ -117,9 +117,8 @@ class SensiboClimate(ClimateDevice):
|
||||
# We are working in same units as the a/c unit. Use whole degrees
|
||||
# like the API supports.
|
||||
return 1
|
||||
else:
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
# Unit conversion is going on. No point to stick to specific steps.
|
||||
return None
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
return
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
@ -61,9 +61,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if climate_devices:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
@ -150,8 +147,7 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return current readable operation mode."""
|
||||
if self._cooling:
|
||||
return "Cooling"
|
||||
else:
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
@ -163,16 +159,14 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the fan setting."""
|
||||
if self.ac_mode:
|
||||
return FAN_MODES_LIST.get(self._current_fan)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
if self.ac_mode:
|
||||
return list(FAN_MODES_LIST.values())
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
@ -218,18 +212,16 @@ class TadoClimate(ClimateDevice):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
|
@ -111,8 +111,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
# This will address both possibilities
|
||||
if self.wink.current_humidity() < 1:
|
||||
return self.wink.current_humidity() * 100
|
||||
else:
|
||||
return self.wink.current_humidity()
|
||||
return self.wink.current_humidity()
|
||||
return None
|
||||
|
||||
@property
|
||||
def external_temperature(self):
|
||||
@ -175,10 +175,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return self.wink.current_max_set_point()
|
||||
elif self.current_operation == STATE_HEAT:
|
||||
return self.wink.current_min_set_point()
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@ -206,8 +203,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return True
|
||||
elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on():
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
@ -270,9 +266,8 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
return STATE_ON
|
||||
elif self.wink.current_fan_mode() == 'auto':
|
||||
return STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@ -451,8 +446,7 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
return SPEED_HIGH
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
@ -154,8 +154,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return TEMP_CELSIUS
|
||||
elif self._unit == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
else:
|
||||
return self._unit
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
@ -112,10 +112,7 @@ class CommandCover(CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
@ -79,8 +79,7 @@ class DemoCover(CoverDevice):
|
||||
"""Flag supported features."""
|
||||
if self._supported_features is not None:
|
||||
return self._supported_features
|
||||
else:
|
||||
return super().supported_features
|
||||
return super().supported_features
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
|
@ -159,8 +159,7 @@ class GaradgetCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state == STATE_CLOSED
|
||||
return self._state == STATE_CLOSED
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
@ -20,8 +20,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, config)
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
@ -52,10 +52,7 @@ class HMCover(HMDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
@ -361,8 +361,7 @@ class MqttCover(CoverDevice):
|
||||
position_percentage = float(offset_position) / tilt_range * 100.0
|
||||
if self._tilt_invert:
|
||||
return 100 - position_percentage
|
||||
else:
|
||||
return position_percentage
|
||||
return position_percentage
|
||||
|
||||
def find_in_range_from_percent(self, percentage):
|
||||
"""
|
||||
|
@ -53,8 +53,7 @@ class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||
set_req = self.gateway.const.SetReq
|
||||
if set_req.V_DIMMER in self._values:
|
||||
return self._values.get(set_req.V_DIMMER) == 0
|
||||
else:
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
|
@ -117,8 +117,7 @@ class OpenGarageCover(CoverDevice):
|
||||
"""Return if the cover is closed."""
|
||||
if self._state == STATE_UNKNOWN:
|
||||
return None
|
||||
else:
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
return self._state in [STATE_CLOSED, STATE_OPENING]
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
|
@ -40,14 +40,15 @@ STOP_ACTION = 'stop_cover'
|
||||
POSITION_ACTION = 'set_cover_position'
|
||||
TILT_ACTION = 'set_cover_tilt_position'
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
|
||||
CONF_OPEN_OR_CLOSE = 'open_or_close'
|
||||
|
||||
TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
|
||||
SUPPORT_SET_TILT_POSITION)
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(OPEN_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CLOSE_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Inclusive(CLOSE_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(STOP_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Exclusive(CONF_POSITION_TEMPLATE,
|
||||
CONF_VALUE_OR_POSITION_TEMPLATE): cv.template,
|
||||
vol.Exclusive(CONF_VALUE_TEMPLATE,
|
||||
@ -77,9 +78,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
position_template = device_config.get(CONF_POSITION_TEMPLATE)
|
||||
tilt_template = device_config.get(CONF_TILT_TEMPLATE)
|
||||
icon_template = device_config.get(CONF_ICON_TEMPLATE)
|
||||
open_action = device_config[OPEN_ACTION]
|
||||
close_action = device_config[CLOSE_ACTION]
|
||||
stop_action = device_config[STOP_ACTION]
|
||||
open_action = device_config.get(OPEN_ACTION)
|
||||
close_action = device_config.get(CLOSE_ACTION)
|
||||
stop_action = device_config.get(STOP_ACTION)
|
||||
position_action = device_config.get(POSITION_ACTION)
|
||||
tilt_action = device_config.get(TILT_ACTION)
|
||||
|
||||
@ -88,6 +89,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
|
||||
continue
|
||||
|
||||
if position_action is None and open_action is None:
|
||||
_LOGGER.error('Must specify at least one of %s' or '%s',
|
||||
OPEN_ACTION, POSITION_ACTION)
|
||||
continue
|
||||
template_entity_ids = set()
|
||||
if state_template is not None:
|
||||
temp_ids = state_template.extract_entities()
|
||||
@ -147,9 +152,15 @@ class CoverTemplate(CoverDevice):
|
||||
self._position_template = position_template
|
||||
self._tilt_template = tilt_template
|
||||
self._icon_template = icon_template
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._open_script = None
|
||||
if open_action is not None:
|
||||
self._open_script = Script(hass, open_action)
|
||||
self._close_script = None
|
||||
if close_action is not None:
|
||||
self._close_script = Script(hass, close_action)
|
||||
self._stop_script = None
|
||||
if stop_action is not None:
|
||||
self._stop_script = Script(hass, stop_action)
|
||||
self._position_script = None
|
||||
if position_action is not None:
|
||||
self._position_script = Script(hass, position_action)
|
||||
@ -227,9 +238,12 @@ class CoverTemplate(CoverDevice):
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
if self.current_cover_position is not None:
|
||||
if self._stop_script is not None:
|
||||
supported_features |= SUPPORT_STOP
|
||||
|
||||
if self._position_script is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self.current_cover_tilt_position is not None:
|
||||
@ -245,23 +259,30 @@ class CoverTemplate(CoverDevice):
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Move the cover up."""
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
if self._open_script:
|
||||
self.hass.async_add_job(self._open_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 100}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Move the cover down."""
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
if self._close_script:
|
||||
self.hass.async_add_job(self._close_script.async_run())
|
||||
elif self._position_script:
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": 0}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Fire the stop action."""
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
if self._stop_script:
|
||||
self.hass.async_add_job(self._stop_script.async_run())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Set cover position."""
|
||||
if ATTR_POSITION not in kwargs:
|
||||
return
|
||||
self._position = kwargs[ATTR_POSITION]
|
||||
self.hass.async_add_job(self._position_script.async_run(
|
||||
{"position": self._position}))
|
||||
@ -283,8 +304,6 @@ class CoverTemplate(CoverDevice):
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION not in kwargs:
|
||||
return
|
||||
self._tilt_value = kwargs[ATTR_TILT_POSITION]
|
||||
self.hass.async_add_job(self._tilt_script.async_run(
|
||||
{"tilt": self._tilt_value}))
|
||||
|
@ -53,10 +53,7 @@ class VeraCover(VeraDevice, CoverDevice):
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.current_cover_position == 0
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
|
@ -29,20 +29,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['cover'].append(self)
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the shade."""
|
||||
self.wink.set_state(1)
|
||||
|
||||
|
@ -73,8 +73,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return None
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
@ -86,8 +85,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
return 0
|
||||
elif self._current_position >= 95:
|
||||
return 100
|
||||
else:
|
||||
return self._current_position
|
||||
return self._current_position
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
|
@ -60,20 +60,11 @@ _LEASES_REGEX = re.compile(
|
||||
r'(?P<host>([^\s]+))')
|
||||
|
||||
# Command to get both 5GHz and 2.4GHz clients
|
||||
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
|
||||
_WL_CMD = 'for dev in `nvram get wl_ifnames`; do wl -i $dev assoclist; done'
|
||||
_WL_REGEX = re.compile(
|
||||
r'\w+\s' +
|
||||
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
|
||||
|
||||
_ARP_CMD = 'arp -n'
|
||||
_ARP_REGEX = re.compile(
|
||||
r'.+\s' +
|
||||
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
|
||||
r'.+\s' +
|
||||
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
|
||||
r'\s' +
|
||||
r'.*')
|
||||
|
||||
_IP_NEIGH_CMD = 'ip neigh'
|
||||
_IP_NEIGH_REGEX = re.compile(
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
|
||||
@ -84,15 +75,6 @@ _IP_NEIGH_REGEX = re.compile(
|
||||
r'\s?(router)?'
|
||||
r'(?P<status>(\w+))')
|
||||
|
||||
_NVRAM_CMD = 'nvram get client_info_tmp'
|
||||
_NVRAM_REGEX = re.compile(
|
||||
r'.*>.*>' +
|
||||
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
|
||||
r'>' +
|
||||
r'(?P<mac>(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' +
|
||||
r'>' +
|
||||
r'.*')
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
@ -102,7 +84,7 @@ def get_scanner(hass, config):
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram')
|
||||
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases')
|
||||
|
||||
|
||||
class AsusWrtDeviceScanner(DeviceScanner):
|
||||
@ -173,7 +155,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
return False
|
||||
|
||||
with self.lock:
|
||||
_LOGGER.info('Checking ARP')
|
||||
_LOGGER.info('Checking Devices')
|
||||
data = self.get_asuswrt_data()
|
||||
if not data:
|
||||
return False
|
||||
@ -182,7 +164,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
client['status'] == 'REACHABLE' or
|
||||
client['status'] == 'DELAY' or
|
||||
client['status'] == 'STALE' or
|
||||
client['status'] == 'IN_NVRAM']
|
||||
client['status'] == 'IN_ASSOCLIST']
|
||||
self.last_results = active_clients
|
||||
return True
|
||||
|
||||
@ -204,41 +186,12 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
|
||||
host = ''
|
||||
|
||||
# match mac addresses to IP addresses in ARP table
|
||||
for arp in result.arp:
|
||||
if match.group('mac').lower() in \
|
||||
arp.decode('utf-8').lower():
|
||||
arp_match = _ARP_REGEX.search(
|
||||
arp.decode('utf-8').lower())
|
||||
if not arp_match:
|
||||
_LOGGER.warning("Could not parse arp row: %s", arp)
|
||||
continue
|
||||
|
||||
devices[arp_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': arp_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
# match mac addresses to IP addresses in NVRAM table
|
||||
for nvr in result.nvram:
|
||||
if match.group('mac').upper() in nvr.decode('utf-8'):
|
||||
nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8'))
|
||||
if not nvram_match:
|
||||
_LOGGER.warning("Could not parse nvr row: %s", nvr)
|
||||
continue
|
||||
|
||||
# skip current check if already in ARP table
|
||||
if nvram_match.group('ip') in devices.keys():
|
||||
continue
|
||||
|
||||
devices[nvram_match.group('ip')] = {
|
||||
'host': host,
|
||||
'status': 'IN_NVRAM',
|
||||
'ip': nvram_match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
devices[match.group('mac').upper()] = {
|
||||
'host': host,
|
||||
'status': 'IN_ASSOCLIST',
|
||||
'ip': '',
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
else:
|
||||
for lease in result.leases:
|
||||
@ -256,20 +209,23 @@ class AsusWrtDeviceScanner(DeviceScanner):
|
||||
if host == '*':
|
||||
host = ''
|
||||
|
||||
devices[match.group('ip')] = {
|
||||
devices[match.group('mac')] = {
|
||||
'host': host,
|
||||
'status': '',
|
||||
'ip': match.group('ip'),
|
||||
'mac': match.group('mac').upper(),
|
||||
}
|
||||
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
|
||||
continue
|
||||
if match.group('ip') in devices:
|
||||
devices[match.group('ip')]['status'] = match.group('status')
|
||||
for neighbor in result.neighbors:
|
||||
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
|
||||
if not match:
|
||||
_LOGGER.warning("Could not parse neighbor row: %s",
|
||||
neighbor)
|
||||
continue
|
||||
if match.group('mac') in devices:
|
||||
devices[match.group('mac')]['status'] = (
|
||||
match.group('status'))
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
@ -317,27 +273,19 @@ class SshConnection(_Connection):
|
||||
try:
|
||||
if not self.connected:
|
||||
self.connect()
|
||||
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
if self._ap:
|
||||
self._ssh.sendline(_ARP_CMD)
|
||||
self._ssh.prompt()
|
||||
arp_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
neighbors = ['']
|
||||
self._ssh.sendline(_WL_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_NVRAM_CMD)
|
||||
self._ssh.prompt()
|
||||
nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:]
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._ssh.sendline(_IP_NEIGH_CMD)
|
||||
self._ssh.prompt()
|
||||
neighbors = self._ssh.before.split(b'\n')[1:-1]
|
||||
self._ssh.sendline(_LEASES_CMD)
|
||||
self._ssh.prompt()
|
||||
leases_result = self._ssh.before.split(b'\n')[1:-1]
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except exceptions.EOF as err:
|
||||
_LOGGER.error("Connection refused. SSH enabled?")
|
||||
self.disconnect()
|
||||
@ -407,23 +355,14 @@ class TelnetConnection(_Connection):
|
||||
neighbors = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
if self._ap:
|
||||
self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
|
||||
arp_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii'))
|
||||
nvram_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1].split(b'<')[1:])
|
||||
else:
|
||||
arp_result = ['']
|
||||
nvram_result = ['']
|
||||
self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
|
||||
leases_result = (self._telnet.read_until(self._prompt_string).
|
||||
split(b'\n')[1:-1])
|
||||
return AsusWrtResult(neighbors, leases_result, arp_result,
|
||||
nvram_result)
|
||||
return AsusWrtResult(neighbors, leases_result)
|
||||
except EOFError:
|
||||
_LOGGER.error("Unexpected response from router")
|
||||
self.disconnect()
|
||||
|
@ -52,8 +52,7 @@ class BboxDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -87,21 +87,20 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
lines_result = lines_result[2:]
|
||||
|
||||
for line in lines_result:
|
||||
if len(line.split()) is 6:
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) != 6:
|
||||
continue
|
||||
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
# ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA',
|
||||
# 'GigabitEthernet0']
|
||||
age = parts[2]
|
||||
hw_addr = parts[3]
|
||||
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
if age != "-":
|
||||
mac = _parse_cisco_mac_address(hw_addr)
|
||||
age = int(age)
|
||||
if age < 1:
|
||||
last_results.append(mac)
|
||||
|
||||
self.last_results = last_results
|
||||
return True
|
||||
|
@ -83,11 +83,14 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
|
||||
if not connections:
|
||||
_LOGGER.debug("Device %s is not connected", mac)
|
||||
continue
|
||||
name = device["friendlyName"]
|
||||
properties = device["properties"]
|
||||
for prop in properties:
|
||||
|
||||
name = None
|
||||
for prop in device["properties"]:
|
||||
if prop["name"] == "userDeviceName":
|
||||
name = prop["value"]
|
||||
if not name:
|
||||
name = device.get("friendlyName", device["deviceID"])
|
||||
|
||||
_LOGGER.debug("Device %s is connected", mac)
|
||||
self.last_results[mac] = name
|
||||
except (KeyError, IndexError):
|
||||
|
@ -94,21 +94,20 @@ class LocativeView(HomeAssistantView):
|
||||
partial(self.see, dev_id=device,
|
||||
location_name=location_name, gps=gps_location))
|
||||
return 'Setting location to not home'
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return 'Received test message.'
|
||||
|
||||
else:
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error('Received unidentified message from Locative: %s',
|
||||
direction)
|
||||
return ('Received unidentified message: {}'.format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
@ -95,8 +95,7 @@ class NmapDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -131,8 +131,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
plaintext_payload = decrypt_payload(topic, data['data'])
|
||||
if plaintext_payload is None:
|
||||
return None
|
||||
else:
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
return validate_payload(topic, plaintext_payload, data_type)
|
||||
|
||||
if not isinstance(data, dict) or data.get('_type') != data_type:
|
||||
_LOGGER.debug("Skipping %s update for following data "
|
||||
|
@ -90,8 +90,7 @@ class TadoDeviceScanner(DeviceScanner):
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -73,8 +73,8 @@ class TomatoDeviceScanner(DeviceScanner):
|
||||
|
||||
if not filter_named or not filter_named[0]:
|
||||
return None
|
||||
else:
|
||||
return filter_named[0]
|
||||
|
||||
return filter_named[0]
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_tomato_info(self):
|
||||
|
@ -9,7 +9,7 @@ import hashlib
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@ -33,8 +33,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a TP-Link scanner."""
|
||||
for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner,
|
||||
Tplink2DeviceScanner, TplinkDeviceScanner]:
|
||||
for cls in [Tplink5DeviceScanner, Tplink4DeviceScanner,
|
||||
Tplink3DeviceScanner, Tplink2DeviceScanner,
|
||||
TplinkDeviceScanner]:
|
||||
scanner = cls(config[DOMAIN])
|
||||
if scanner.success_init:
|
||||
return scanner
|
||||
@ -234,10 +235,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
return False
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"An unknown error happened while fetching data")
|
||||
return False
|
||||
_LOGGER.error(
|
||||
"An unknown error happened while fetching data")
|
||||
return False
|
||||
except ValueError:
|
||||
_LOGGER.error("Router didn't respond with JSON. "
|
||||
"Check if credentials are correct")
|
||||
@ -350,3 +350,83 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
self.last_results = [mac.replace("-", ":") for mac in mac_results]
|
||||
return True
|
||||
|
||||
|
||||
class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||
"""This class queries a TP-Link EAP-225 AP with newer TP-Link FW."""
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found MAC IDs."""
|
||||
self._update_info()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def get_device_name(self, device):
|
||||
"""Get firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the TP-Link AP is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
with self.lock:
|
||||
_LOGGER.info("Loading wireless clients...")
|
||||
|
||||
base_url = 'http://{}'.format(self.host)
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
||||
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Content-Type": "application/x-www-form-urlencoded; "
|
||||
"charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Referer": "http://" + self.host + "/",
|
||||
"Connection": "keep-alive",
|
||||
"Pragma": "no-cache",
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
|
||||
password_md5 = hashlib.md5(self.password).hexdigest().upper()
|
||||
|
||||
# create a session to handle cookie easier
|
||||
session = requests.session()
|
||||
session.get(base_url, headers=header)
|
||||
|
||||
login_data = {"username": self.username, "password": password_md5}
|
||||
session.post(base_url, login_data, headers=header)
|
||||
|
||||
# a timestamp is required to be sent as get parameter
|
||||
timestamp = int(datetime.now().timestamp() * 1e3)
|
||||
|
||||
client_list_url = '{}/data/monitor.client.client.json'.format(
|
||||
base_url)
|
||||
|
||||
get_params = {
|
||||
'operation': 'load',
|
||||
'_': timestamp
|
||||
}
|
||||
|
||||
response = session.get(client_list_url,
|
||||
headers=header,
|
||||
params=get_params)
|
||||
session.close()
|
||||
try:
|
||||
list_of_devices = response.json()
|
||||
except ValueError:
|
||||
_LOGGER.error("AP didn't respond with JSON. "
|
||||
"Check if credentials are correct.")
|
||||
return False
|
||||
|
||||
if list_of_devices:
|
||||
self.last_results = {
|
||||
device['MAC'].replace('-', ':'): device['DeviceName']
|
||||
for device in list_of_devices['data']
|
||||
}
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -12,11 +12,10 @@ import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
@ -25,12 +24,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
|
||||
})
|
||||
|
||||
CMD_LOGIN = 15
|
||||
CMD_LOGOUT = 16
|
||||
CMD_DEVICES = 123
|
||||
|
||||
|
||||
@ -38,7 +34,7 @@ CMD_DEVICES = 123
|
||||
def async_get_scanner(hass, config):
|
||||
"""Return the UPC device scanner."""
|
||||
scanner = UPCDeviceScanner(hass, config[DOMAIN])
|
||||
success_init = yield from scanner.async_login()
|
||||
success_init = yield from scanner.async_initialize_token()
|
||||
|
||||
return scanner if success_init else None
|
||||
|
||||
@ -50,7 +46,6 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
"""Initialize the scanner."""
|
||||
self.hass = hass
|
||||
self.host = config[CONF_HOST]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.data = {}
|
||||
self.token = None
|
||||
@ -65,21 +60,12 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
|
||||
self.websession = async_get_clientsession(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_logout(event):
|
||||
"""Logout from upc connect box."""
|
||||
yield from self._async_ws_function(CMD_LOGOUT)
|
||||
self.token = None
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, async_logout)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
if self.token is None:
|
||||
reconnect = yield from self.async_login()
|
||||
if not reconnect:
|
||||
token_initialized = yield from self.async_initialize_token()
|
||||
if not token_initialized:
|
||||
_LOGGER.error("Not connected to %s", self.host)
|
||||
return []
|
||||
|
||||
@ -95,55 +81,42 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_device_name(self, device):
|
||||
"""Ge the firmware doesn't save the name of the wireless device."""
|
||||
"""The firmware doesn't save the name of the wireless device."""
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
"""Login into firmware and get first token."""
|
||||
def async_initialize_token(self):
|
||||
"""Get first token."""
|
||||
try:
|
||||
# get first token
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host)
|
||||
"http://{}/common_page/login.html".format(self.host),
|
||||
headers=self.headers
|
||||
)
|
||||
|
||||
yield from response.text()
|
||||
|
||||
self.token = response.cookies['sessionToken'].value
|
||||
|
||||
# login
|
||||
data = yield from self._async_ws_function(CMD_LOGIN, {
|
||||
'Username': 'NULL',
|
||||
'Password': self.password,
|
||||
})
|
||||
|
||||
# Successful?
|
||||
return data is not None
|
||||
return True
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Can not load login page from %s", self.host)
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_ws_function(self, function, additional_form=None):
|
||||
def _async_ws_function(self, function):
|
||||
"""Execute a command on UPC firmware webservice."""
|
||||
form_data = {
|
||||
'token': self.token,
|
||||
'fun': function
|
||||
}
|
||||
|
||||
if additional_form:
|
||||
form_data.update(additional_form)
|
||||
|
||||
redirects = function != CMD_DEVICES
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
# The 'token' parameter has to be first, and 'fun' second
|
||||
# or the UPC firmware will return an error
|
||||
response = yield from self.websession.post(
|
||||
"http://{}/xml/getter.xml".format(self.host),
|
||||
data=form_data,
|
||||
data="token={}&fun={}".format(self.token, function),
|
||||
headers=self.headers,
|
||||
allow_redirects=redirects
|
||||
allow_redirects=False
|
||||
)
|
||||
|
||||
# error?
|
||||
|
@ -101,10 +101,10 @@ class XiaomiDeviceScanner(DeviceScanner):
|
||||
result = _retrieve_list(self.host, self.token)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
_LOGGER.info("Refreshing token and retrying device list refresh")
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
return _retrieve_list(self.host, self.token)
|
||||
|
||||
_LOGGER.info("Refreshing token and retrying device list refresh")
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
return _retrieve_list(self.host, self.token)
|
||||
|
||||
def _store_result(self, result):
|
||||
"""Extract and store the device list in self.last_results."""
|
||||
|
@ -32,6 +32,7 @@ SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
|
||||
SERVICE_HASSIO = 'hassio'
|
||||
SERVICE_AXIS = 'axis'
|
||||
SERVICE_APPLE_TV = 'apple_tv'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@ -40,6 +41,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
||||
SERVICE_HASSIO: ('hassio', None),
|
||||
SERVICE_AXIS: ('axis', None),
|
||||
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
@ -52,7 +54,6 @@ SERVICE_HANDLERS = {
|
||||
'denonavr': ('media_player', 'denonavr'),
|
||||
'samsung_tv': ('media_player', 'samsungtv'),
|
||||
'yeelight': ('light', 'yeelight'),
|
||||
'apple_tv': ('media_player', 'apple_tv'),
|
||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||
'openhome': ('media_player', 'openhome'),
|
||||
'harmony': ('remote', 'harmony'),
|
||||
|
@ -1,4 +1,8 @@
|
||||
"""Parent component for Dyson Pure Cool Link devices."""
|
||||
"""Parent component for Dyson Pure Cool Link devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/dyson/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@ -9,7 +13,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \
|
||||
CONF_DEVICES
|
||||
|
||||
REQUIREMENTS = ['libpurecoollink==0.1.5']
|
||||
REQUIREMENTS = ['libpurecoollink==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,4 +1,8 @@
|
||||
"""Support for Dyson Pure Cool link fan."""
|
||||
"""Support for Dyson Pure Cool link fan.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.dyson/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
from os import path
|
||||
@ -79,9 +83,11 @@ class DysonPureCoolLinkDevice(FanEntity):
|
||||
|
||||
def on_message(self, message):
|
||||
"""Called when new messages received from the fan."""
|
||||
_LOGGER.debug(
|
||||
"Message received for fan device %s : %s", self.name, message)
|
||||
self.schedule_update_ha_state()
|
||||
from libpurecoollink.dyson import DysonState
|
||||
if isinstance(message, DysonState):
|
||||
_LOGGER.debug("Message received for fan device %s : %s", self.name,
|
||||
message)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -157,8 +163,7 @@ class DysonPureCoolLinkDevice(FanEntity):
|
||||
from libpurecoollink.const import FanSpeed
|
||||
if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value:
|
||||
return self._device.state.speed
|
||||
else:
|
||||
return int(self._device.state.speed)
|
||||
return int(self._device.state.speed)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
insteonhub = hass.data['insteon_local']
|
||||
|
||||
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
|
||||
if len(conf_fans):
|
||||
if conf_fans:
|
||||
for device_id in conf_fans:
|
||||
setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
|
||||
add_devices)
|
||||
|
@ -33,10 +33,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class WinkFanDevice(WinkDevice, FanEntity):
|
||||
"""Representation of a Wink fan."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the fan."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
|
@ -6,10 +6,13 @@ import logging
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.config import find_config_file, load_yaml_config_file
|
||||
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_NOT_FOUND
|
||||
from homeassistant.components import api, group
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http.auth import is_trusted_ip
|
||||
from homeassistant.components.http.const import KEY_DEVELOPMENT
|
||||
@ -23,6 +26,8 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
|
||||
|
||||
ATTR_THEMES = 'themes'
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
MANIFEST_JSON = {
|
||||
'background_color': '#FFFFFF',
|
||||
'description': 'Open-source home automation platform running on Python 3.',
|
||||
@ -33,7 +38,7 @@ MANIFEST_JSON = {
|
||||
'name': 'Home Assistant',
|
||||
'short_name': 'Assistant',
|
||||
'start_url': '/',
|
||||
'theme_color': '#03A9F4'
|
||||
'theme_color': DEFAULT_THEME_COLOR
|
||||
}
|
||||
|
||||
for size in (192, 384, 512, 1024):
|
||||
@ -45,11 +50,30 @@ for size in (192, 384, 512, 1024):
|
||||
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_INDEX_VIEW = 'frontend_index_view'
|
||||
DATA_THEMES = 'frontend_themes'
|
||||
DATA_DEFAULT_THEME = 'frontend_default_theme'
|
||||
DEFAULT_THEME = 'default'
|
||||
|
||||
PRIMARY_COLOR = 'primary-color'
|
||||
|
||||
# To keep track we don't register a component twice (gives a warning)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(ATTR_THEMES): vol.Schema({
|
||||
cv.string: {cv.string: cv.string}
|
||||
}),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_SET_THEME = 'set_theme'
|
||||
SERVICE_RELOAD_THEMES = 'reload_themes'
|
||||
SERVICE_SET_THEME_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def register_built_in_panel(hass, component_name, sidebar_title=None,
|
||||
sidebar_icon=None, url_path=None, config=None):
|
||||
@ -138,6 +162,8 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
||||
if index_view:
|
||||
hass.http.app.router.add_route(
|
||||
'get', '/{}'.format(url_path), index_view.get)
|
||||
hass.http.app.router.add_route(
|
||||
'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get)
|
||||
|
||||
|
||||
def add_manifest_json_key(key, val):
|
||||
@ -172,20 +198,81 @@ def setup(hass, config):
|
||||
# Now register their urls.
|
||||
if DATA_PANELS in hass.data:
|
||||
for url_path in hass.data[DATA_PANELS]:
|
||||
hass.http.app.router.add_route('get', '/{}'.format(url_path),
|
||||
index_view.get)
|
||||
hass.http.app.router.add_route(
|
||||
'get', '/{}'.format(url_path), index_view.get)
|
||||
hass.http.app.router.add_route(
|
||||
'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get)
|
||||
else:
|
||||
hass.data[DATA_PANELS] = {}
|
||||
|
||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template'):
|
||||
'dev-template', 'kiosk'):
|
||||
register_built_in_panel(hass, panel)
|
||||
|
||||
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
|
||||
setup_themes(hass, themes)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_themes(hass, themes):
|
||||
"""Set up themes data and services."""
|
||||
hass.http.register_view(ThemesView)
|
||||
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
|
||||
if themes is None:
|
||||
hass.data[DATA_THEMES] = {}
|
||||
return
|
||||
|
||||
hass.data[DATA_THEMES] = themes
|
||||
|
||||
@callback
|
||||
def update_theme_and_fire_event():
|
||||
"""Update theme_color in manifest."""
|
||||
name = hass.data[DATA_DEFAULT_THEME]
|
||||
themes = hass.data[DATA_THEMES]
|
||||
if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]:
|
||||
MANIFEST_JSON['theme_color'] = themes[name][PRIMARY_COLOR]
|
||||
else:
|
||||
MANIFEST_JSON['theme_color'] = DEFAULT_THEME_COLOR
|
||||
hass.bus.async_fire(EVENT_THEMES_UPDATED, {
|
||||
'themes': themes,
|
||||
'default_theme': name,
|
||||
})
|
||||
|
||||
@callback
|
||||
def set_theme(call):
|
||||
"""Set backend-prefered theme."""
|
||||
data = call.data
|
||||
name = data[CONF_NAME]
|
||||
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
|
||||
_LOGGER.info("Theme %s set as default", name)
|
||||
hass.data[DATA_DEFAULT_THEME] = name
|
||||
update_theme_and_fire_event()
|
||||
else:
|
||||
_LOGGER.warning("Theme %s is not defined.", name)
|
||||
|
||||
@callback
|
||||
def reload_themes(_):
|
||||
"""Reload themes."""
|
||||
path = find_config_file(hass.config.config_dir)
|
||||
new_themes = load_yaml_config_file(path)[DOMAIN].get(ATTR_THEMES, {})
|
||||
hass.data[DATA_THEMES] = new_themes
|
||||
if hass.data[DATA_DEFAULT_THEME] not in new_themes:
|
||||
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
|
||||
update_theme_and_fire_event()
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
hass.services.register(DOMAIN, SERVICE_SET_THEME,
|
||||
set_theme,
|
||||
descriptions[SERVICE_SET_THEME],
|
||||
SERVICE_SET_THEME_SCHEMA)
|
||||
hass.services.register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes,
|
||||
descriptions[SERVICE_RELOAD_THEMES])
|
||||
|
||||
|
||||
class BootstrapView(HomeAssistantView):
|
||||
"""View to bootstrap frontend with all needed data."""
|
||||
|
||||
@ -212,7 +299,7 @@ class IndexView(HomeAssistantView):
|
||||
url = '/'
|
||||
name = 'frontend:index'
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{entity_id}']
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the frontend view."""
|
||||
@ -225,17 +312,10 @@ class IndexView(HomeAssistantView):
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id=None):
|
||||
def get(self, request, extra=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if entity_id is not None:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if (not state or state.domain != 'group' or
|
||||
not state.attributes.get(group.ATTR_VIEW)):
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
if request.app[KEY_DEVELOPMENT]:
|
||||
core_url = '/static/home-assistant-polymer/build/core.js'
|
||||
compatibility_url = \
|
||||
@ -295,3 +375,21 @@ class ManifestJSONView(HomeAssistantView):
|
||||
"""Return the manifest.json."""
|
||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
||||
return web.Response(body=msg, content_type="application/manifest+json")
|
||||
|
||||
|
||||
class ThemesView(HomeAssistantView):
|
||||
"""View to return defined themes."""
|
||||
|
||||
requires_auth = False
|
||||
url = '/api/themes'
|
||||
name = 'api:themes'
|
||||
|
||||
@callback
|
||||
def get(self, request):
|
||||
"""Return themes."""
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json({
|
||||
'themes': hass.data[DATA_THEMES],
|
||||
'default_theme': hass.data[DATA_DEFAULT_THEME],
|
||||
})
|
||||
|
11
homeassistant/components/frontend/services.yaml
Normal file
11
homeassistant/components/frontend/services.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
# Describes the format for available frontend services
|
||||
|
||||
set_theme:
|
||||
description: Set a theme unless the client selected per-device theme.
|
||||
fields:
|
||||
name:
|
||||
description: Name of a predefined theme or 'default'.
|
||||
example: 'light'
|
||||
|
||||
reload_themes:
|
||||
description: Reload themes from yaml config.
|
@ -3,21 +3,21 @@
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||
"frontend.html": "bdcde4695ce32595a9e1d813b9d7c5f9",
|
||||
"mdi.html": "c92bd28c434865d6cabb34cd3c0a3e4c",
|
||||
"frontend.html": "a7d4cb8260e8094342b5bd8c36c4bf5b",
|
||||
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-automation.html": "4f98839bb082885657bbcd0ac04fc680",
|
||||
"panels/ha-panel-automation.html": "72a5c1856cece8d9246328e84185ab0b",
|
||||
"panels/ha-panel-config.html": "76853de505d173e82249bf605eb73505",
|
||||
"panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61",
|
||||
"panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc",
|
||||
"panels/ha-panel-dev-service.html": "92c6be30b1af95791d5a6429df505852",
|
||||
"panels/ha-panel-dev-service.html": "ac2c50e486927dc4443e93d79f08c06e",
|
||||
"panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869",
|
||||
"panels/ha-panel-dev-template.html": "d33a55b937b50cdfe8b6fae81f70a139",
|
||||
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
|
||||
"panels/ha-panel-dev-template.html": "82cd543177c417e5c6612e07df851e6b",
|
||||
"panels/ha-panel-hassio.html": "262d31efd9add719e0325da5cf79a096",
|
||||
"panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094",
|
||||
"panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d",
|
||||
"panels/ha-panel-map.html": "0ba605729197c4724ecc7310b08f7050",
|
||||
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
"panels/ha-panel-map.html": "d3dae1400ec4e4cd7681d2aa79131d55",
|
||||
"panels/ha-panel-zwave.html": "2ea2223339d1d2faff478751c2927d11"
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 6e30534e2dc906d29ce497bf0a2eacb18e36f9c5
|
||||
Subproject commit 7c079dc01f03d7599762df1ac8c6eab8e4b90004
|
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.
@ -1 +1 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-hassio"><template><style>[hidden]{display:none!important}</style><hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]"></hassio-main></template></dom-module><script>Polymer({is:"ha-panel-hassio",properties:{hass:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},loaded:{type:Boolean,value:!1}},attached:function(){window.HASS_DEV||this.importHref("/api/hassio/panel",null,function(){alert("Failed to load the Hass.io panel from supervisor.")})}})</script></body></html>
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-hassio"><template><style>[hidden]{display:none!important}</style><hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main></template></dom-module><script>Polymer({is:"ha-panel-hassio",properties:{hass:Object,narrow:Boolean,showMenu:Boolean,route:Object,loaded:{type:Boolean,value:!1}},attached:function(){window.HASS_DEV||this.importHref("/api/hassio/panel",null,function(){alert("Failed to load the Hass.io panel from supervisor.")})}})</script></body></html>
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-kiosk"><template><partial-cards id="kiosk-states" hass="[[hass]]" show-menu="" route="[[route]]" panel-visible=""></partial-cards></template></dom-module><script>Polymer({is:"ha-panel-kiosk",properties:{hass:Object,route:Object}})</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.
@ -1,125 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebSocket debug</title>
|
||||
<style>
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.controls textarea {
|
||||
height: 160px;
|
||||
min-width: 400px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='controls'>
|
||||
<textarea id="messageinput">
|
||||
{
|
||||
"id": 1, "type": "subscribe_events", "event_type": "state_changed"
|
||||
}
|
||||
</textarea>
|
||||
<pre>
|
||||
Examples:
|
||||
{
|
||||
"id": 2, "type": "subscribe_events", "event_type": "state_changed"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 4, "type": "unsubscribe_events", "subscription": 2
|
||||
}
|
||||
|
||||
{
|
||||
"id": 5, "type": "get_states"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 6, "type": "get_config"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 7, "type": "get_services"
|
||||
}
|
||||
|
||||
{
|
||||
"id": 8, "type": "get_panels"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onclick="openSocket();" >Open</button>
|
||||
<button type="button" onclick="send();" >Send</button>
|
||||
<button type="button" onclick="closeSocket();" >Close</button>
|
||||
</div>
|
||||
<!-- Server responses get written here -->
|
||||
<pre id="messages"></pre>
|
||||
|
||||
<!-- Script to utilise the WebSocket -->
|
||||
<script type="text/javascript">
|
||||
var webSocket;
|
||||
var messages = document.getElementById("messages");
|
||||
|
||||
function openSocket(){
|
||||
var isOpen = false;
|
||||
// Ensures only one connection is open at a time
|
||||
if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
|
||||
writeResponse("WebSocket is already opened.");
|
||||
return;
|
||||
}
|
||||
// Create a new instance of the websocket
|
||||
webSocket = new WebSocket("ws://localhost:8123/api/websocket");
|
||||
|
||||
/**
|
||||
* Binds functions to the listeners for the websocket.
|
||||
*/
|
||||
webSocket.onopen = function(event){
|
||||
if (!isOpen) {
|
||||
isOpen = true;
|
||||
writeResponse('Connection opened');
|
||||
}
|
||||
// For reasons I can't determine, onopen gets called twice
|
||||
// and the first time event.data is undefined.
|
||||
// Leave a comment if you know the answer.
|
||||
if(event.data === undefined)
|
||||
return;
|
||||
|
||||
writeResponse(event.data);
|
||||
};
|
||||
|
||||
webSocket.onmessage = function(event){
|
||||
writeResponse(event.data);
|
||||
};
|
||||
|
||||
webSocket.onclose = function(event){
|
||||
writeResponse("Connection closed");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the value of the text input to the server
|
||||
*/
|
||||
function send(){
|
||||
var text = document.getElementById("messageinput").value;
|
||||
webSocket.send(text);
|
||||
}
|
||||
|
||||
function closeSocket(){
|
||||
webSocket.close();
|
||||
}
|
||||
|
||||
function writeResponse(text){
|
||||
messages.innerHTML += "\n" + text;
|
||||
}
|
||||
|
||||
openSocket();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user