commit
70ea16bdc0
200 files changed
+7412
-2099
No files matched your search
+14
@@ -20,6 +20,9 @@ omit =
|
||||
homeassistant/components/android_ip_webcam.py
|
||||
homeassistant/components/*/android_ip_webcam.py
|
||||
|
||||
homeassistant/components/axis.py
|
||||
homeassistant/components/*/axis.py
|
||||
|
||||
homeassistant/components/bbb_gpio.py
|
||||
homeassistant/components/*/bbb_gpio.py
|
||||
|
||||
@@ -59,6 +62,9 @@ omit =
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/kira.py
|
||||
homeassistant/components/*/kira.py
|
||||
|
||||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
@@ -83,12 +89,18 @@ omit =
|
||||
homeassistant/components/qwikswitch.py
|
||||
homeassistant/components/*/qwikswitch.py
|
||||
|
||||
homeassistant/components/raspihats.py
|
||||
homeassistant/components/*/raspihats.py
|
||||
|
||||
homeassistant/components/rfxtrx.py
|
||||
homeassistant/components/*/rfxtrx.py
|
||||
|
||||
homeassistant/components/rpi_gpio.py
|
||||
homeassistant/components/*/rpi_gpio.py
|
||||
|
||||
homeassistant/components/rpi_pfio.py
|
||||
homeassistant/components/*/rpi_pfio.py
|
||||
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
@@ -175,6 +187,7 @@ omit =
|
||||
homeassistant/components/binary_sensor/flic.py
|
||||
homeassistant/components/binary_sensor/hikvision.py
|
||||
homeassistant/components/binary_sensor/iss.py
|
||||
homeassistant/components/binary_sensor/mystrom.py
|
||||
homeassistant/components/binary_sensor/pilight.py
|
||||
homeassistant/components/binary_sensor/ping.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
@@ -239,6 +252,7 @@ omit =
|
||||
homeassistant/components/ifttt.py
|
||||
homeassistant/components/image_processing/dlib_face_detect.py
|
||||
homeassistant/components/image_processing/dlib_face_identify.py
|
||||
homeassistant/components/image_processing/seven_segments.py
|
||||
homeassistant/components/joaoapps_join.py
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/keyboard_remote.py
|
||||
|
||||
+9
-5
@@ -1,13 +1,15 @@
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libudev-dev
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=lint
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=py34
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
- python: "3.5"
|
||||
@@ -16,6 +18,8 @@ matrix:
|
||||
env: TOXENV=py36
|
||||
- python: "3.6-dev"
|
||||
env: TOXENV=py36
|
||||
- python: "3.4.2"
|
||||
env: TOXENV=requirements
|
||||
# allow_failures:
|
||||
# - python: "3.5"
|
||||
# env: TOXENV=typing
|
||||
@@ -25,5 +29,5 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: tox
|
||||
script: travis_wait tox
|
||||
after_success: coveralls
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:3.5
|
||||
FROM python:3.6
|
||||
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
# Uncomment any of the following lines to disable the installation.
|
||||
|
||||
@@ -4,6 +4,7 @@ Interfaces with Wink Cameras.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
@@ -42,6 +43,11 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
"""Initialize the Wink alarm."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.core import callback
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +35,6 @@ CONF_TEXT = 'text'
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_UID = 'uid'
|
||||
CONF_DATE = 'date'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
@@ -88,7 +86,6 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
@@ -331,10 +328,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
if isinstance(item[CONF_DATE], str):
|
||||
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
|
||||
|
||||
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
|
||||
output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.core import CoreState
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START)
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import get_platform
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
|
||||
DOMAIN = 'automation'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
@@ -81,6 +82,8 @@ _TRIGGER_SCHEMA = vol.All(
|
||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
# str on purpose
|
||||
CONF_ID: str,
|
||||
CONF_ALIAS: cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||
@@ -139,16 +142,21 @@ def reload(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
def async_reload(hass):
|
||||
"""Reload the automation from config.
|
||||
|
||||
Returns a coroutine object.
|
||||
"""
|
||||
return hass.services.async_call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
|
||||
success = yield from _async_process_config(hass, config, component)
|
||||
|
||||
if not success:
|
||||
return False
|
||||
yield from _async_process_config(hass, config, component)
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, conf_util.load_yaml_config_file, os.path.join(
|
||||
@@ -215,15 +223,20 @@ def async_setup(hass, config):
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
descriptions.get(service), schema=SERVICE_SCHEMA)
|
||||
|
||||
if 'frontend' in hass.config.components:
|
||||
register_built_in_panel(hass, 'automation', 'Automations',
|
||||
'mdi:playlist-play')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||
hidden, initial_state):
|
||||
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
|
||||
async_action, hidden, initial_state):
|
||||
"""Initialize an automation entity."""
|
||||
self._id = automation_id
|
||||
self._name = name
|
||||
self._async_attach_triggers = async_attach_triggers
|
||||
self._async_detach_triggers = None
|
||||
@@ -346,6 +359,16 @@ class AutomationEntity(ToggleEntity):
|
||||
self.async_trigger)
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return automation attributes."""
|
||||
if self._id is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
CONF_ID: self._id
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_process_config(hass, config, component):
|
||||
@@ -359,6 +382,7 @@ def _async_process_config(hass, config, component):
|
||||
conf = config[config_key]
|
||||
|
||||
for list_no, config_block in enumerate(conf):
|
||||
automation_id = config_block.get(CONF_ID)
|
||||
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
|
||||
list_no)
|
||||
|
||||
@@ -383,16 +407,14 @@ def _async_process_config(hass, config, component):
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
name, async_attach_triggers, cond_func, action, hidden,
|
||||
initial_state)
|
||||
automation_id, name, async_attach_triggers, cond_func, action,
|
||||
hidden, initial_state)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
if entities:
|
||||
yield from component.async_add_entities(entities)
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
|
||||
def _async_get_action(hass, config, name):
|
||||
"""Return an action based on a configuration."""
|
||||
|
||||
@@ -9,8 +9,8 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback, CoreState
|
||||
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_EVENT_TYPE = 'event_type'
|
||||
@@ -31,19 +31,6 @@ def async_trigger(hass, config, action):
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data = config.get(CONF_EVENT_DATA)
|
||||
|
||||
if (event_type == EVENT_HOMEASSISTANT_START and
|
||||
hass.state == CoreState.starting):
|
||||
_LOGGER.warning('Deprecation: Automations should not listen to event '
|
||||
"'homeassistant_start'. Use platform 'homeassistant' "
|
||||
'instead. Feature will be removed in 0.45')
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': None,
|
||||
},
|
||||
})
|
||||
return lambda: None
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
|
||||
@@ -79,6 +79,10 @@ def async_trigger(hass, config, action):
|
||||
call_action()
|
||||
return
|
||||
|
||||
# If only state attributes changed, ignore this event
|
||||
if from_s.last_changed == to_s.last_changed:
|
||||
return
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
|
||||
@@ -16,8 +16,6 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['sun']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
Support for Axis devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/axis/
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED,
|
||||
CONF_HOST, CONF_INCLUDE, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
|
||||
REQUIREMENTS = ['axis==7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
CONFIG_FILE = 'axis.conf'
|
||||
|
||||
AXIS_DEVICES = {}
|
||||
|
||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||
'daynight', 'tampering', 'input']
|
||||
|
||||
PLATFORMS = ['camera']
|
||||
|
||||
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_INCLUDE):
|
||||
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: DEVICE_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def request_configuration(hass, name, host, serialnumber):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
def configuration_callback(callback_data):
|
||||
"""Called when config is submitted."""
|
||||
if CONF_INCLUDE not in callback_data:
|
||||
configurator.notify_errors(request_id,
|
||||
"Functionality mandatory.")
|
||||
return False
|
||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||
callback_data[CONF_HOST] = host
|
||||
if CONF_NAME not in callback_data:
|
||||
callback_data[CONF_NAME] = name
|
||||
try:
|
||||
config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
configurator.notify_errors(request_id,
|
||||
"Bad input, please check spelling.")
|
||||
return False
|
||||
|
||||
if setup_device(hass, config):
|
||||
config_file = _read_config(hass)
|
||||
config_file[serialnumber] = dict(config)
|
||||
del config_file[serialnumber]['hass']
|
||||
_write_config(hass, config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
configurator.notify_errors(request_id,
|
||||
"Failed to register, please try again.")
|
||||
return False
|
||||
|
||||
title = '{} ({})'.format(name, host)
|
||||
request_id = configurator.request_config(
|
||||
hass, title, configuration_callback,
|
||||
description='Functionality: ' + str(AXIS_INCLUDE),
|
||||
entity_picture="/static/images/logo_axis.png",
|
||||
link_name='Axis platform documentation',
|
||||
link_url='https://home-assistant.io/components/axis/',
|
||||
submit_caption="Confirm",
|
||||
fields=[
|
||||
{'id': CONF_NAME,
|
||||
'name': "Device name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_USERNAME,
|
||||
'name': "User name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PASSWORD,
|
||||
'name': 'Password',
|
||||
'type': 'password'},
|
||||
{'id': CONF_INCLUDE,
|
||||
'name': "Device functionality (space separated list)",
|
||||
'type': 'text'},
|
||||
{'id': ATTR_LOCATION,
|
||||
'name': "Physical location of device (optional)",
|
||||
'type': 'text'},
|
||||
{'id': CONF_TRIGGER_TIME,
|
||||
'name': "Sensor update interval (optional)",
|
||||
'type': 'number'},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, base_config):
|
||||
"""Common setup for Axis devices."""
|
||||
def _shutdown(call): # pylint: disable=unused-argument
|
||||
"""Stop the metadatastream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
_LOGGER.info("Stopping metadatastream for %s.", serialnumber)
|
||||
device.stop_metadatastream()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown)
|
||||
|
||||
def axis_device_discovered(service, discovery_info):
|
||||
"""Called when axis devices has been found."""
|
||||
host = discovery_info['host']
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = _read_config(hass)
|
||||
if serialnumber in config_file:
|
||||
try:
|
||||
config = DEVICE_SCHEMA(config_file[serialnumber])
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
else:
|
||||
request_configuration(hass, name, host, serialnumber)
|
||||
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
||||
if DOMAIN in base_config:
|
||||
for device in base_config[DOMAIN]:
|
||||
config = base_config[DOMAIN][device]
|
||||
if CONF_NAME not in config:
|
||||
config[CONF_NAME] = device
|
||||
if not setup_device(hass, config):
|
||||
_LOGGER.error("Couldn\'t set up %s", config['name'])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_device(hass, config):
|
||||
"""Set up device."""
|
||||
from axis import AxisDevice
|
||||
|
||||
config['hass'] = hass
|
||||
device = AxisDevice(config) # Initialize device
|
||||
enable_metadatastream = False
|
||||
|
||||
if device.serial_number is None:
|
||||
# If there is no serial number a connection could not be made
|
||||
_LOGGER.error("Couldn\'t connect to %s", config[CONF_HOST])
|
||||
return False
|
||||
|
||||
for component in config[CONF_INCLUDE]:
|
||||
if component in EVENT_TYPES:
|
||||
# Sensors are created by device calling event_initialized
|
||||
# when receiving initialize messages on metadatastream
|
||||
device.add_event_topic(convert(component, 'type', 'subscribe'))
|
||||
if not enable_metadatastream:
|
||||
enable_metadatastream = True
|
||||
else:
|
||||
discovery.load_platform(hass, component, DOMAIN, config)
|
||||
|
||||
if enable_metadatastream:
|
||||
device.initialize_new_event = event_initialized
|
||||
device.initiate_metadatastream()
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
return True
|
||||
|
||||
|
||||
def _read_config(hass):
|
||||
"""Read Axis config."""
|
||||
path = hass.config.path(CONFIG_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return {}
|
||||
|
||||
with open(path) as f_handle:
|
||||
# Guard against empty file
|
||||
return json.loads(f_handle.read() or '{}')
|
||||
|
||||
|
||||
def _write_config(hass, config):
|
||||
"""Write Axis config."""
|
||||
data = json.dumps(config)
|
||||
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(data)
|
||||
|
||||
|
||||
def event_initialized(event):
|
||||
"""Register event initialized on metadatastream here."""
|
||||
hass = event.device_config('hass')
|
||||
discovery.load_platform(hass,
|
||||
convert(event.topic, 'topic', 'platform'),
|
||||
DOMAIN, {'axis_event': event})
|
||||
|
||||
|
||||
class AxisDeviceEvent(Entity):
|
||||
"""Representation of a Axis device event."""
|
||||
|
||||
def __init__(self, axis_event):
|
||||
"""Initialize the event."""
|
||||
self.axis_event = axis_event
|
||||
self._event_class = convert(self.axis_event.topic, 'topic', 'class')
|
||||
self._name = '{}_{}_{}'.format(self.axis_event.device_name,
|
||||
convert(self.axis_event.topic,
|
||||
'topic', 'type'),
|
||||
self.axis_event.id)
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.update()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the event."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the event."""
|
||||
return self._event_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the event."""
|
||||
attr = {}
|
||||
|
||||
tripped = self.axis_event.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
|
||||
location = self.axis_event.device_config(ATTR_LOCATION)
|
||||
if location:
|
||||
attr[ATTR_LOCATION] = location
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
def convert(item, from_key, to_key):
|
||||
"""Translate between Axis and HASS syntax."""
|
||||
for entry in REMAP:
|
||||
if entry[from_key] == item:
|
||||
return entry[to_key]
|
||||
|
||||
|
||||
REMAP = [{'type': 'motion',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:VideoAnalytics/tnsaxis:MotionDetection',
|
||||
'subscribe': 'onvif:VideoAnalytics/axis:MotionDetection',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'vmd3',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1',
|
||||
'subscribe': 'onvif:RuleEngine/axis:VMD3/vmd3_video_1',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'pir',
|
||||
'class': 'motion',
|
||||
'topic': 'tns1:Device/tnsaxis:Sensor/PIR',
|
||||
'subscribe': 'onvif:Device/axis:Sensor/axis:PIR',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'sound',
|
||||
'class': 'sound',
|
||||
'topic': 'tns1:AudioSource/tnsaxis:TriggerLevel',
|
||||
'subscribe': 'onvif:AudioSource/axis:TriggerLevel',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'daynight',
|
||||
'class': 'light',
|
||||
'topic': 'tns1:VideoSource/tnsaxis:DayNightVision',
|
||||
'subscribe': 'onvif:VideoSource/axis:DayNightVision',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'tampering',
|
||||
'class': 'safety',
|
||||
'topic': 'tns1:VideoSource/tnsaxis:Tampering',
|
||||
'subscribe': 'onvif:VideoSource/axis:Tampering',
|
||||
'platform': 'binary_sensor'},
|
||||
{'type': 'input',
|
||||
'class': 'input',
|
||||
'topic': 'tns1:Device/tnsaxis:IO/Port',
|
||||
'subscribe': 'onvif:Device/axis:IO/Port',
|
||||
'platform': 'sensor'}, ]
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Support for Axis binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.axis/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.axis import (AxisDeviceEvent)
|
||||
from homeassistant.const import (CONF_TRIGGER_TIME)
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis device event."""
|
||||
add_devices([AxisBinarySensor(discovery_info['axis_event'], hass)], True)
|
||||
|
||||
|
||||
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
"""Representation of a binary Axis event."""
|
||||
|
||||
def __init__(self, axis_event, hass):
|
||||
"""Initialize the binary sensor."""
|
||||
self.hass = hass
|
||||
self._state = False
|
||||
self._delay = axis_event.device_config(CONF_TRIGGER_TIME)
|
||||
self._timer = None
|
||||
AxisDeviceEvent.__init__(self, axis_event)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if event is active."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
self._state = self.axis_event.is_tripped
|
||||
|
||||
def _update_callback(self):
|
||||
"""Update the sensor's state, if needed."""
|
||||
self.update()
|
||||
|
||||
if self._timer is not None:
|
||||
self._timer()
|
||||
self._timer = None
|
||||
|
||||
if self._delay > 0 and not self.is_on:
|
||||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s Called delayed (%s sec) update.",
|
||||
self._name, self._delay)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
|
||||
self._timer = track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self._delay))
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Support for the myStrom buttons.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.mystrom/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up myStrom Binary Sensor."""
|
||||
hass.http.register_view(MyStromView(async_add_devices))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MyStromView(HomeAssistantView):
|
||||
"""View to handle requests from myStrom buttons."""
|
||||
|
||||
url = '/api/mystrom'
|
||||
name = 'api:mystrom'
|
||||
|
||||
def __init__(self, add_devices):
|
||||
"""Initialize the myStrom URL endpoint."""
|
||||
self.buttons = {}
|
||||
self.add_devices = add_devices
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""The GET request received from a myStrom button."""
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle(self, hass, data):
|
||||
"""Handle requests to the myStrom endpoint."""
|
||||
button_action = list(data.keys())[0]
|
||||
button_id = data[button_action]
|
||||
entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
|
||||
|
||||
if button_action not in ['single', 'double', 'long', 'touch']:
|
||||
_LOGGER.error(
|
||||
"Received unidentified message from myStrom button: %s", data)
|
||||
return ("Received unidentified message: {}".format(data),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if entity_id not in self.buttons:
|
||||
_LOGGER.info("New myStrom button/action detected: %s/%s",
|
||||
button_id, button_action)
|
||||
self.buttons[entity_id] = MyStromBinarySensor(
|
||||
'{}_{}'.format(button_id, button_action))
|
||||
hass.async_add_job(self.add_devices, [self.buttons[entity_id]])
|
||||
else:
|
||||
new_state = True if self.buttons[entity_id].state == 'off' \
|
||||
else False
|
||||
self.buttons[entity_id].async_on_update(new_state)
|
||||
|
||||
|
||||
class MyStromBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a myStrom button."""
|
||||
|
||||
def __init__(self, button_id):
|
||||
"""Initialize the myStrom Binary sensor."""
|
||||
self._button_id = button_id
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._button_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def async_on_update(self, value):
|
||||
"""Receive an update."""
|
||||
self._state = value
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Configure a binary_sensor using a digital input from a raspihats board.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.raspihats/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice
|
||||
)
|
||||
from homeassistant.components.raspihats import (
|
||||
CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX,
|
||||
CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['raspihats']
|
||||
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
DEFAULT_DEVICE_CLASS = None
|
||||
|
||||
_CHANNELS_SCHEMA = vol.Schema([{
|
||||
vol.Required(CONF_INDEX): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string,
|
||||
}])
|
||||
|
||||
_I2C_HATS_SCHEMA = vol.Schema([{
|
||||
vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES),
|
||||
vol.Required(CONF_ADDRESS): vol.Coerce(int),
|
||||
vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA
|
||||
}])
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the raspihats binary_sensor devices."""
|
||||
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
|
||||
binary_sensors = []
|
||||
i2c_hat_configs = config.get(CONF_I2C_HATS)
|
||||
for i2c_hat_config in i2c_hat_configs:
|
||||
address = i2c_hat_config[CONF_ADDRESS]
|
||||
board = i2c_hat_config[CONF_BOARD]
|
||||
try:
|
||||
I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address)
|
||||
for channel_config in i2c_hat_config[CONF_CHANNELS]:
|
||||
binary_sensors.append(
|
||||
I2CHatBinarySensor(
|
||||
address,
|
||||
channel_config[CONF_INDEX],
|
||||
channel_config[CONF_NAME],
|
||||
channel_config[CONF_INVERT_LOGIC],
|
||||
channel_config[CONF_DEVICE_CLASS]
|
||||
)
|
||||
)
|
||||
except I2CHatsException as ex:
|
||||
_LOGGER.error(
|
||||
"Failed to register " + board + "I2CHat@" + hex(address) + " "
|
||||
+ str(ex)
|
||||
)
|
||||
add_devices(binary_sensors)
|
||||
|
||||
|
||||
class I2CHatBinarySensor(BinarySensorDevice):
|
||||
"""Represents a binary sensor that uses a I2C-HAT digital input."""
|
||||
|
||||
I2C_HATS_MANAGER = None
|
||||
|
||||
def __init__(self, address, channel, name, invert_logic, device_class):
|
||||
"""Initialize sensor."""
|
||||
self._address = address
|
||||
self._channel = channel
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._invert_logic = invert_logic
|
||||
self._device_class = device_class
|
||||
self._state = self.I2C_HATS_MANAGER.read_di(
|
||||
self._address,
|
||||
self._channel
|
||||
)
|
||||
|
||||
def online_callback():
|
||||
"""Callback fired when board is online."""
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.I2C_HATS_MANAGER.register_online_callback(
|
||||
self._address,
|
||||
self._channel,
|
||||
online_callback
|
||||
)
|
||||
|
||||
def edge_callback(state):
|
||||
"""Read digital input state."""
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.I2C_HATS_MANAGER.register_di_callback(
|
||||
self._address,
|
||||
self._channel,
|
||||
edge_callback
|
||||
)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling not needed for this sensor."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of this sensor."""
|
||||
return self._state != self._invert_logic
|
||||
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Support for binary sensor using the PiFace Digital I/O module on a RPi.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rpi_pfio/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.rpi_pfio as rpi_pfio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_INVERT_LOGIC = 'invert_logic'
|
||||
ATTR_SETTLE_TIME = 'settle_time'
|
||||
CONF_PORTS = 'ports'
|
||||
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
DEFAULT_SETTLE_TIME = 20
|
||||
|
||||
DEPENDENCIES = ['rpi_pfio']
|
||||
|
||||
PORT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_NAME, default=None): cv.string,
|
||||
vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORTS, default={}): vol.Schema({
|
||||
cv.positive_int: PORT_SCHEMA
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the PiFace Digital Input devices."""
|
||||
binary_sensors = []
|
||||
ports = config.get('ports')
|
||||
for port, port_entity in ports.items():
|
||||
name = port_entity[ATTR_NAME]
|
||||
settle_time = port_entity[ATTR_SETTLE_TIME] / 1000
|
||||
invert_logic = port_entity[ATTR_INVERT_LOGIC]
|
||||
|
||||
binary_sensors.append(RPiPFIOBinarySensor(
|
||||
hass, port, name, settle_time, invert_logic))
|
||||
add_devices(binary_sensors, True)
|
||||
|
||||
rpi_pfio.activate_listener(hass)
|
||||
|
||||
|
||||
class RPiPFIOBinarySensor(BinarySensorDevice):
|
||||
"""Represent a binary sensor that a PiFace Digital Input."""
|
||||
|
||||
def __init__(self, hass, port, name, settle_time, invert_logic):
|
||||
"""Initialize the RPi binary sensor."""
|
||||
self._port = port
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._invert_logic = invert_logic
|
||||
self._state = None
|
||||
|
||||
def read_pfio(port):
|
||||
"""Read state from PFIO."""
|
||||
self._state = rpi_pfio.read_input(self._port)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state != self._invert_logic
|
||||
|
||||
def update(self):
|
||||
"""Update the PFIO state."""
|
||||
self._state = rpi_pfio.read_input(self._port)
|
||||
@@ -4,6 +4,7 @@ Support for Wink binary sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
@@ -101,6 +102,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
else:
|
||||
self.capability = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED)
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
REQUIREMENTS = ['blinkpy==0.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ class CameraImageView(CameraView):
|
||||
image = yield from camera.async_camera_image()
|
||||
|
||||
if image:
|
||||
return web.Response(body=image)
|
||||
return web.Response(body=image, content_type='image/jpeg')
|
||||
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
@@ -12,18 +12,22 @@ import voluptuous as vol
|
||||
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
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_get_clientsession, async_aiohttp_proxy_web,
|
||||
async_aiohttp_proxy_stream)
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.1.9']
|
||||
REQUIREMENTS = ['amcrest==1.2.0']
|
||||
DEPENDENCIES = ['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
|
||||
@@ -40,7 +44,8 @@ RESOLUTION_LIST = {
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
@@ -56,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@@ -92,8 +98,9 @@ class AmcrestCam(Camera):
|
||||
super(AmcrestCam, self).__init__()
|
||||
self._camera = camera
|
||||
self._base_url = self._camera.get_base_url()
|
||||
self._hass = hass
|
||||
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)
|
||||
@@ -117,15 +124,28 @@ class AmcrestCam(Camera):
|
||||
yield from super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
# Otherwise, stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = '{0}mjpg/video.cgi?channel=0&subtype={1}'.format(
|
||||
self._base_url, self._resolution)
|
||||
elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
|
||||
stream_coro = websession.get(
|
||||
streaming_url, auth=self._token, timeout=TIMEOUT)
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
else:
|
||||
# streaming via fmpeg
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
streaming_url = self._camera.rtsp_url(typeno=self._resolution)
|
||||
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
streaming_url, extra_cmd=self._ffmpeg_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Support for Axis camera streaming.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.axis/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
DOMAIN = 'axis'
|
||||
|
||||
|
||||
def _get_image_url(host, mode):
|
||||
if mode == 'mjpeg':
|
||||
return 'http://{}/axis-cgi/mjpg/video.cgi'.format(host)
|
||||
elif mode == 'single':
|
||||
return 'http://{}/axis-cgi/jpg/image.cgi'.format(host)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Axis camera."""
|
||||
device_info = {
|
||||
CONF_NAME: discovery_info['name'],
|
||||
CONF_USERNAME: discovery_info['username'],
|
||||
CONF_PASSWORD: discovery_info['password'],
|
||||
CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'),
|
||||
CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION,
|
||||
}
|
||||
add_devices([MjpegCamera(hass, device_info)])
|
||||
@@ -20,12 +20,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NVR = 'nvr'
|
||||
CONF_KEY = 'key'
|
||||
CONF_PASSWORD = 'password'
|
||||
|
||||
DEFAULT_PASSWORD = 'ubnt'
|
||||
DEFAULT_PORT = 7080
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NVR): cv.string,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
@@ -34,6 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Discover cameras on a Unifi NVR."""
|
||||
addr = config[CONF_NVR]
|
||||
key = config[CONF_KEY]
|
||||
password = config[CONF_PASSWORD]
|
||||
port = config[CONF_PORT]
|
||||
|
||||
from uvcclient import nvr
|
||||
@@ -59,7 +63,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera[identifier],
|
||||
camera['name'])
|
||||
camera['name'],
|
||||
password)
|
||||
for camera in cameras])
|
||||
return True
|
||||
|
||||
@@ -67,12 +72,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class UnifiVideoCamera(Camera):
|
||||
"""A Ubiquiti Unifi Video Camera."""
|
||||
|
||||
def __init__(self, nvr, uuid, name):
|
||||
def __init__(self, nvr, uuid, name, password):
|
||||
"""Initialize an Unifi camera."""
|
||||
super(UnifiVideoCamera, self).__init__()
|
||||
self._nvr = nvr
|
||||
self._uuid = uuid
|
||||
self._name = name
|
||||
self._password = password
|
||||
self.is_streaming = False
|
||||
self._connect_addr = None
|
||||
self._camera = None
|
||||
@@ -102,7 +108,6 @@ class UnifiVideoCamera(Camera):
|
||||
def _login(self):
|
||||
"""Login to the camera."""
|
||||
from uvcclient import camera as uvc_camera
|
||||
from uvcclient import store as uvc_store
|
||||
|
||||
caminfo = self._nvr.get_camera(self._uuid)
|
||||
if self._connect_addr:
|
||||
@@ -110,13 +115,6 @@ class UnifiVideoCamera(Camera):
|
||||
else:
|
||||
addrs = [caminfo['host'], caminfo['internalHost']]
|
||||
|
||||
store = uvc_store.get_info_store()
|
||||
password = store.get_camera_password(self._uuid)
|
||||
if password is None:
|
||||
_LOGGER.debug("Logging into camera %(name)s with default password",
|
||||
dict(name=self._name))
|
||||
password = 'ubnt'
|
||||
|
||||
if self._nvr.server_version >= (3, 2, 0):
|
||||
client_cls = uvc_camera.UVCCameraClientV320
|
||||
else:
|
||||
@@ -126,7 +124,7 @@ class UnifiVideoCamera(Camera):
|
||||
for addr in addrs:
|
||||
try:
|
||||
camera = client_cls(
|
||||
addr, caminfo['username'], password)
|
||||
addr, caminfo['username'], self._password)
|
||||
camera.login()
|
||||
_LOGGER.debug("Logged into UVC camera %(name)s via %(addr)s",
|
||||
dict(name=self._name, addr=addr))
|
||||
|
||||
@@ -107,12 +107,7 @@ class ZoneMinderCamera(MjpegCamera):
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
if not status_response.get("success", False):
|
||||
_LOGGER.warning("Alarm status API call failed for monitor %i",
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
self._is_recording = status_response.get('status') == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
|
||||
@@ -149,22 +149,22 @@ class SensiboClimate(ClimateDevice):
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._ac_states['fanLevel']
|
||||
return self._ac_states.get('fanLevel')
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
"""List of available fan modes."""
|
||||
return self._current_capabilities['fanLevels']
|
||||
return self._current_capabilities.get('fanLevels')
|
||||
|
||||
@property
|
||||
def current_swing_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._ac_states['swing']
|
||||
return self._ac_states.get('swing')
|
||||
|
||||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._current_capabilities['swing']
|
||||
return self._current_capabilities.get('swing')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -4,6 +4,8 @@ Support for Wink thermostats.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
@@ -52,6 +54,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
super().__init__(wink, hass)
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['climate'].append(self)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID
|
||||
from homeassistant.setup import (
|
||||
async_prepare_setup_platform, ATTR_COMPONENT)
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
@@ -14,8 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
|
||||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'group', 'hassbian')
|
||||
ON_DEMAND = ('zwave', )
|
||||
SECTIONS = ('core', 'group', 'hassbian', 'automation')
|
||||
ON_DEMAND = ('zwave')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -60,7 +60,7 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class EditKeyBasedConfigView(HomeAssistantView):
|
||||
class BaseEditConfigView(HomeAssistantView):
|
||||
"""Configure a Group endpoint."""
|
||||
|
||||
def __init__(self, component, config_type, path, key_schema, data_schema,
|
||||
@@ -73,13 +73,29 @@ class EditKeyBasedConfigView(HomeAssistantView):
|
||||
self.data_schema = data_schema
|
||||
self.post_write_hook = post_write_hook
|
||||
|
||||
def _empty_config(self):
|
||||
"""Empty config if file not found."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
"""Get value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
raise NotImplementedError
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, config_key):
|
||||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from hass.loop.run_in_executor(
|
||||
None, _read, hass.config.path(self.path))
|
||||
return self.json(current.get(config_key, {}))
|
||||
current = yield from self.read_config(hass)
|
||||
value = self._get_value(current, config_key)
|
||||
|
||||
if value is None:
|
||||
return self.json_message('Resource not found', 404)
|
||||
|
||||
return self.json(value)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request, config_key):
|
||||
@@ -104,10 +120,10 @@ class EditKeyBasedConfigView(HomeAssistantView):
|
||||
hass = request.app['hass']
|
||||
path = hass.config.path(self.path)
|
||||
|
||||
current = yield from hass.loop.run_in_executor(None, _read, path)
|
||||
current.setdefault(config_key, {}).update(data)
|
||||
current = yield from self.read_config(hass)
|
||||
self._write_value(current, config_key, data)
|
||||
|
||||
yield from hass.loop.run_in_executor(None, _write, path, current)
|
||||
yield from hass.async_add_job(_write, path, current)
|
||||
|
||||
if self.post_write_hook is not None:
|
||||
hass.async_add_job(self.post_write_hook(hass))
|
||||
@@ -116,13 +132,59 @@ class EditKeyBasedConfigView(HomeAssistantView):
|
||||
'result': 'ok',
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def read_config(self, hass):
|
||||
"""Read the config."""
|
||||
current = yield from hass.async_add_job(
|
||||
_read, hass.config.path(self.path))
|
||||
if not current:
|
||||
current = self._empty_config()
|
||||
return current
|
||||
|
||||
|
||||
class EditKeyBasedConfigView(BaseEditConfigView):
|
||||
"""Configure a list of entries."""
|
||||
|
||||
def _empty_config(self):
|
||||
"""Return an empty config."""
|
||||
return {}
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
"""Get value."""
|
||||
return data.get(config_key, {})
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
data.setdefault(config_key, {}).update(new_value)
|
||||
|
||||
|
||||
class EditIdBasedConfigView(BaseEditConfigView):
|
||||
"""Configure key based config entries."""
|
||||
|
||||
def _empty_config(self):
|
||||
"""Return an empty config."""
|
||||
return []
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
"""Get value."""
|
||||
return next(
|
||||
(val for val in data if val.get(CONF_ID) == config_key), None)
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
value = self._get_value(data, config_key)
|
||||
|
||||
if value is None:
|
||||
value = {CONF_ID: config_key}
|
||||
data.append(value)
|
||||
|
||||
value.update(new_value)
|
||||
|
||||
|
||||
def _read(path):
|
||||
"""Read YAML helper."""
|
||||
if not os.path.isfile(path):
|
||||
with open(path, 'w'):
|
||||
pass
|
||||
return {}
|
||||
return None
|
||||
|
||||
return load_yaml(path)
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditIdBasedConfigView
|
||||
from homeassistant.components.automation import (
|
||||
PLATFORM_SCHEMA, DOMAIN, async_reload)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
CONFIG_PATH = 'automations.yaml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Automation config API."""
|
||||
hass.http.register_view(EditIdBasedConfigView(
|
||||
DOMAIN, 'config', CONFIG_PATH, cv.string,
|
||||
PLATFORM_SCHEMA, post_write_hook=async_reload
|
||||
))
|
||||
return True
|
||||
@@ -9,9 +9,11 @@ the user has submitted configuration information.
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback as async_callback
|
||||
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
|
||||
ATTR_ENTITY_PICTURE
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.util.async import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_REQUESTS = {}
|
||||
@@ -43,7 +45,9 @@ def request_config(
|
||||
|
||||
Will return an ID to be used for sequent calls.
|
||||
"""
|
||||
instance = _get_instance(hass)
|
||||
instance = run_callback_threadsafe(hass.loop,
|
||||
_async_get_instance,
|
||||
hass).result()
|
||||
|
||||
request_id = instance.request_config(
|
||||
name, callback,
|
||||
@@ -79,7 +83,8 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _get_instance(hass):
|
||||
@async_callback
|
||||
def _async_get_instance(hass):
|
||||
"""Get an instance per hass object."""
|
||||
instance = hass.data.get(_KEY_INSTANCE)
|
||||
|
||||
@@ -97,7 +102,7 @@ class Configurator(object):
|
||||
self.hass = hass
|
||||
self._cur_id = 0
|
||||
self._requests = {}
|
||||
hass.services.register(
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
||||
|
||||
def request_config(
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Support for Lutron Caseta SerenaRollerShade.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
|
||||
from homeassistant.components.lutron_caseta import (
|
||||
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta Serena shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"])
|
||||
for cover_device in cover_devices:
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
|
||||
|
||||
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Representation of a Lutron Serena shade."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._state["current_state"] < 1
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
def update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
||||
@@ -4,6 +4,8 @@ Support for Wink Covers.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.wink/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
@@ -31,6 +33,11 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""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):
|
||||
"""Close the shade."""
|
||||
self.wink.set_state(0)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
A component which allows you to send data to Datadog.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/datadog/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX,
|
||||
EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED,
|
||||
STATE_UNKNOWN)
|
||||
from homeassistant.helpers import state as state_helper
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['datadog==0.15.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RATE = 'rate'
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8125
|
||||
DEFAULT_PREFIX = 'hass'
|
||||
DEFAULT_RATE = 1
|
||||
DOMAIN = 'datadog'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
|
||||
vol.Optional(CONF_RATE, default=DEFAULT_RATE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the Datadog component."""
|
||||
from datadog import initialize, statsd
|
||||
|
||||
conf = config[DOMAIN]
|
||||
host = conf.get(CONF_HOST)
|
||||
port = conf.get(CONF_PORT)
|
||||
sample_rate = conf.get(CONF_RATE)
|
||||
prefix = conf.get(CONF_PREFIX)
|
||||
|
||||
initialize(statsd_host=host, statsd_port=port)
|
||||
|
||||
def logbook_entry_listener(event):
|
||||
"""Listen for logbook entries and send them as events."""
|
||||
name = event.data.get('name')
|
||||
message = event.data.get('message')
|
||||
|
||||
statsd.event(
|
||||
title="Home Assistant",
|
||||
text="%%% \n **{}** {} \n %%%".format(name, message),
|
||||
tags=[
|
||||
"entity:{}".format(event.data.get('entity_id')),
|
||||
"domain:{}".format(event.data.get('domain'))
|
||||
]
|
||||
)
|
||||
|
||||
_LOGGER.debug('Sent event %s', event.data.get('entity_id'))
|
||||
|
||||
def state_changed_listener(event):
|
||||
"""Listen for new messages on the bus and sends them to Datadog."""
|
||||
state = event.data.get('new_state')
|
||||
|
||||
if state is None or state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
if state.attributes.get('hidden') is True:
|
||||
return
|
||||
|
||||
states = dict(state.attributes)
|
||||
metric = "{}.{}".format(prefix, state.domain)
|
||||
tags = ["entity:{}".format(state.entity_id)]
|
||||
|
||||
for key, value in states.items():
|
||||
if isinstance(value, (float, int)):
|
||||
attribute = "{}.{}".format(metric, key.replace(' ', '_'))
|
||||
statsd.gauge(
|
||||
attribute,
|
||||
value,
|
||||
sample_rate=sample_rate,
|
||||
tags=tags
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
'Sent metric %s: %s (tags: %s)',
|
||||
attribute,
|
||||
value,
|
||||
tags
|
||||
)
|
||||
|
||||
try:
|
||||
value = state_helper.state_as_number(state)
|
||||
except ValueError:
|
||||
_LOGGER.debug(
|
||||
'Error sending %s: %s (tags: %s)',
|
||||
metric,
|
||||
state.state,
|
||||
tags
|
||||
)
|
||||
return
|
||||
|
||||
statsd.gauge(
|
||||
metric,
|
||||
value,
|
||||
sample_rate=sample_rate,
|
||||
tags=tags
|
||||
)
|
||||
|
||||
_LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags)
|
||||
|
||||
hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener)
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener)
|
||||
|
||||
return True
|
||||
@@ -157,28 +157,28 @@ def async_setup(hass, config):
|
||||
}},
|
||||
]}))
|
||||
|
||||
tasks2.append(group.Group.async_create_group(hass, 'living room', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Living Room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'cover.living_room_window', media_players[1],
|
||||
'scene.romantic_lights']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'bedroom', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'kitchen', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [
|
||||
lights[2], 'cover.kitchen_window', 'lock.kitchen_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'doors', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'automations', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ]))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'people', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'People', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus']))
|
||||
tasks2.append(group.Group.async_create_group(hass, 'downstairs', [
|
||||
tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'cover.kitchen_window',
|
||||
'cover.living_room_window', 'group.doors',
|
||||
'thermostat.ecobee',
|
||||
'climate.ecobee',
|
||||
], view=True))
|
||||
|
||||
results = yield from asyncio.gather(*tasks2, loop=hass.loop)
|
||||
|
||||
@@ -14,12 +14,13 @@ from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_point_in_time, async_track_state_change)
|
||||
async_track_point_in_utc_time, async_track_state_change)
|
||||
from homeassistant.helpers.sun import is_up, get_astral_event_next
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'device_sun_light_trigger'
|
||||
DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun']
|
||||
DEPENDENCIES = ['light', 'device_tracker', 'group']
|
||||
|
||||
CONF_DEVICE_GROUP = 'device_group'
|
||||
CONF_DISABLE_TURN_OFF = 'disable_turn_off'
|
||||
@@ -50,7 +51,6 @@ def async_setup(hass, config):
|
||||
device_tracker = get_component('device_tracker')
|
||||
group = get_component('group')
|
||||
light = get_component('light')
|
||||
sun = get_component('sun')
|
||||
conf = config[DOMAIN]
|
||||
disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF)
|
||||
light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS)
|
||||
@@ -78,7 +78,7 @@ def async_setup(hass, config):
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
next_setting = sun.next_setting(hass)
|
||||
next_setting = get_astral_event_next(hass, 'sunset')
|
||||
if not next_setting:
|
||||
return None
|
||||
return next_setting - LIGHT_TRANSITION_TIME * len(light_ids)
|
||||
@@ -103,7 +103,7 @@ def async_setup(hass, config):
|
||||
# Track every time sun rises so we can schedule a time-based
|
||||
# pre-sun set event
|
||||
@callback
|
||||
def schedule_light_turn_on(entity, old_state, new_state):
|
||||
def schedule_light_turn_on(now):
|
||||
"""Turn on all the lights at the moment sun sets.
|
||||
|
||||
We will schedule to have each light start after one another
|
||||
@@ -114,26 +114,26 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
for index, light_id in enumerate(light_ids):
|
||||
async_track_point_in_time(
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_turn_on_factory(light_id),
|
||||
start_point + index * LIGHT_TRANSITION_TIME)
|
||||
|
||||
async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on,
|
||||
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
|
||||
async_track_point_in_utc_time(hass, schedule_light_turn_on,
|
||||
get_astral_event_next(hass, 'sunrise'))
|
||||
|
||||
# If the sun is already above horizon schedule the time-based pre-sun set
|
||||
# event.
|
||||
if sun.is_on(hass):
|
||||
schedule_light_turn_on(None, None, None)
|
||||
if is_up(hass):
|
||||
schedule_light_turn_on(None)
|
||||
|
||||
@callback
|
||||
def check_light_on_dev_state_change(entity, old_state, new_state):
|
||||
"""Handle tracked device state changes."""
|
||||
lights_are_on = group.is_on(hass, light_group)
|
||||
light_needed = not (lights_are_on or sun.is_on(hass))
|
||||
light_needed = not (lights_are_on or is_up(hass))
|
||||
|
||||
# These variables are needed for the elif check
|
||||
now = dt_util.now()
|
||||
now = dt_util.utcnow()
|
||||
start_point = calc_time_for_light_when_sunset()
|
||||
|
||||
# Do we need lights?
|
||||
@@ -146,7 +146,7 @@ def async_setup(hass, config):
|
||||
# Check this by seeing if current time is later then the point
|
||||
# in time when we would start putting the lights on.
|
||||
elif (start_point and
|
||||
start_point < now < sun.next_setting(hass)):
|
||||
start_point < now < get_astral_event_next(hass, 'sunset')):
|
||||
|
||||
# Check for every light if it would be on if someone was home
|
||||
# when the fading in started and turn it on if so
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
REQUIREMENTS = ['aioautomatic==0.3.1']
|
||||
REQUIREMENTS = ['aioautomatic==0.4.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,7 +31,8 @@ CONF_DEVICES = 'devices'
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
SCOPE = ['location', 'vehicle:profile', 'trip']
|
||||
DEFAULT_SCOPE = ['location', 'vehicle:profile', 'trip']
|
||||
FULL_SCOPE = DEFAULT_SCOPE + ['current_location']
|
||||
|
||||
ATTR_FUEL_LEVEL = 'fuel_level'
|
||||
|
||||
@@ -58,8 +59,17 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
client_session=async_get_clientsession(hass),
|
||||
request_kwargs={'timeout': DEFAULT_TIMEOUT})
|
||||
try:
|
||||
session = yield from client.create_session_from_password(
|
||||
SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
try:
|
||||
session = yield from client.create_session_from_password(
|
||||
FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
except aioautomatic.exceptions.ForbiddenError as exc:
|
||||
if not str(exc).startswith("invalid_scope"):
|
||||
raise exc
|
||||
_LOGGER.info("Client not authorized for current_location scope. "
|
||||
"location:updated events will not be received.")
|
||||
session = yield from client.create_session_from_password(
|
||||
DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
|
||||
data = AutomaticData(
|
||||
hass, client, session, config[CONF_DEVICES], async_see)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
INTERFACES = 2
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
REQUIREMENTS = ['beautifulsoup4==4.5.3']
|
||||
REQUIREMENTS = ['beautifulsoup4==4.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
self._log_out()
|
||||
return self.last_results.keys()
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@@ -195,8 +196,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
self.sysauth = regex_result.group(1)
|
||||
_LOGGER.info(self.sysauth)
|
||||
return True
|
||||
except ValueError:
|
||||
_LOGGER.error("Couldn't fetch auth tokens!")
|
||||
except (ValueError, KeyError) as _:
|
||||
_LOGGER.error("Couldn't fetch auth tokens! Response was: %s",
|
||||
response.text)
|
||||
return False
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
@@ -250,6 +252,21 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
return False
|
||||
|
||||
def _log_out(self):
|
||||
with self.lock:
|
||||
_LOGGER.info("Logging out of router admin interface...")
|
||||
|
||||
url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?'
|
||||
'form=logout').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
requests.post(url,
|
||||
params={'operation': 'write'},
|
||||
headers={'referer': referer},
|
||||
cookies={'sysauth': self.sysauth})
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
|
||||
|
||||
class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
"""This class queries an Archer C7 router with TP-Link firmware 150427."""
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.discovery import async_load_platform, async_discover
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['netdisco==1.0.0rc3']
|
||||
REQUIREMENTS = ['netdisco==1.0.0']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
@@ -31,6 +31,7 @@ SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
|
||||
SERVICE_HASSIO = 'hassio'
|
||||
SERVICE_AXIS = 'axis'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
@@ -38,6 +39,7 @@ SERVICE_HANDLERS = {
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
||||
SERVICE_HASSIO: ('hassio', None),
|
||||
SERVICE_AXIS: ('axis', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
'google_cast': ('media_player', 'cast'),
|
||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||
|
||||
@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['dweepy==0.2.0']
|
||||
REQUIREMENTS = ['dweepy==0.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,4 +67,4 @@ def send_data(name, msg):
|
||||
try:
|
||||
dweepy.dweet_for(name, msg)
|
||||
except dweepy.DweepyError:
|
||||
_LOGGER.error("Error saving data '%s' to Dweet.io", msg)
|
||||
_LOGGER.error("Error saving data to Dweet.io: %s", msg)
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['pyeight==0.0.4']
|
||||
REQUIREMENTS = ['pyeight==0.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -145,6 +145,9 @@ def async_setup(hass, config):
|
||||
sensors.append('{}_{}'.format(obj.side, sensor))
|
||||
binary_sensors.append('{}_presence'.format(obj.side))
|
||||
sensors.append('room_temp')
|
||||
else:
|
||||
# No users, cannot continue
|
||||
return False
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for Wink fans.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/fan.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
|
||||
@@ -12,6 +13,8 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH,
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_LOWEST = 'lowest'
|
||||
@@ -34,6 +37,11 @@ class WinkFanDevice(WinkDevice, FanEntity):
|
||||
"""Initialize the fan."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['fan'].append(self)
|
||||
|
||||
def set_direction(self: ToggleEntity, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
self.wink.set_fan_direction(direction)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||
"frontend.html": "5999c8fac69c503b846672cae75a12b0",
|
||||
"compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812",
|
||||
"core.js": "d4a7cb8c80c62b536764e0e81385f6aa",
|
||||
"frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1",
|
||||
"mdi.html": "f407a5a57addbe93817ee1b244d33fbe",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8",
|
||||
"panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1",
|
||||
"panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed",
|
||||
"panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750",
|
||||
"panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b",
|
||||
"panels/ha-panel-hassio.html": "23d175b6744c20e2fdf475b6efdaa1d3",
|
||||
"panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229",
|
||||
"panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163",
|
||||
"panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4",
|
||||
"panels/ha-panel-zwave.html": "84fb45638d2a69bac343246a687f647c",
|
||||
"panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o<arguments.length;o++){var i=t[o];if(void 0!==i&&null!==i)for(var l=Object.keys(Object(i)),a=0,c=l.length;a<c;a++){var b=l[a],f=Object.getOwnPropertyDescriptor(i,b);void 0!==f&&f.enumerable&&(n[b]=i[b])}}return n}function r(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}var t={assign:e,polyfill:r};t.polyfill()})();
|
||||
!function(){"use strict";function e(e,t){if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var r=Object(e),n=1;n<arguments.length;n++){var o=arguments[n];if(void 0!==o&&null!==o)for(var i=Object.keys(Object(o)),l=0,c=i.length;l<c;l++){var a=i[l],b=Object.getOwnPropertyDescriptor(o,a);void 0!==b&&b.enumerable&&(r[a]=o[a])}}return r}function t(){Object.assign||Object.defineProperty(Object,"assign",{enumerable:!1,configurable:!0,writable:!0,value:e})}({assign:e,polyfill:t}).polyfill()}();
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
!(function(){"use strict";function e(e){return{type:"auth",api_password:e}}function t(){return{type:"get_states"}}function n(){return{type:"get_config"}}function r(){return{type:"get_services"}}function i(){return{type:"get_panels"}}function s(e,t,n){var r={type:"call_service",domain:e,service:t};return n&&(r.service_data=n),r}function o(e){var t={type:"subscribe_events"};return e&&(t.event_type=e),t}function c(e){return{type:"unsubscribe_events",subscription:e}}function u(){return{type:"ping"}}function a(e,t){return{type:"result",success:!1,error:{code:e,message:t}}}function f(t,n){function r(i,s,o){var c=new WebSocket(t),u=!1,a=function(){if(u)return void o(C);if(0===i)return void o(O);var e=i===-1?-1:i-1;setTimeout((function(){return r(e,s,o)}),1e3)},f=function(t){var r=JSON.parse(t.data);switch(r.type){case"auth_required":"authToken"in n?c.send(JSON.stringify(e(n.authToken))):(u=!0,c.close());break;case"auth_invalid":u=!0,c.close();break;case"auth_ok":c.removeEventListener("message",f),c.removeEventListener("close",a),s(c)}};c.addEventListener("message",f),c.addEventListener("close",a)}return new Promise(function(e,t){return r(n.setupRetry||0,e,t)})}function d(e){return e.result}function v(e,t){return void 0===t&&(t={}),f(e,t).then((function(n){var r=new L(e,t);return r.setSocket(n),r}))}function h(e,t){return e._subscribeConfig?e._subscribeConfig(t):new Promise(function(n,r){var i=null,s=null,o=[],c=null;t&&o.push(t);var u=function(e){i=Object.assign({},i,e);for(var t=0;t<o.length;t++)o[t](i)},a=function(e,t){return u({services:Object.assign({},i.services,(n={},n[e]=t,n))});var n},f=function(e){if(null!==i){var t=Object.assign({},i.core,{components:i.core.components.concat(e.data.component)});u({core:t})}},d=function(e){if(null!==i){var t,n=e.data,r=n.domain,s=n.service,o=Object.assign({},i.services[r]||{},(t={},t[s]={description:"",fields:{}},t));a(r,o)}},v=function(e){if(null!==i){var t=e.data,n=t.domain,r=t.service,s=i.services[n];if(s&&r in s){var o={};Object.keys(s).forEach((function(e){e!==r&&(o[e]=s[e])})),a(n,o)}}},h=function(){return Promise.all([e.getConfig(),e.getPanels(),e.getServices()]).then((function(e){var t=e[0],n=e[1],r=e[2];u({core:t,panels:n,services:r})}))},l=function(e){e&&o.splice(o.indexOf(e),1),0===o.length&&s()};e._subscribeConfig=function(e){return e&&(o.push(e),null!==i&&e(i)),c.then((function(){return function(){return l(e)}}))},c=Promise.all([e.subscribeEvents(f,"component_loaded"),e.subscribeEvents(d,"service_registered"),e.subscribeEvents(v,"service_removed"),h()]),c.then((function(r){var i=r[0],o=r[1],c=r[2];s=function(){removeEventListener("ready",h),i(),o(),c()},e.addEventListener("ready",h),n((function(){return l(t)}))}),(function(){return r()}))})}function l(e){for(var t={},n=0;n<e.length;n++){var r=e[n];t[r.entity_id]=r}return t}function p(e,t){var n=Object.assign({},e);return n[t.entity_id]=t,n}function b(e,t){var n=Object.assign({},e);return delete n[t],n}function g(e,t){return e._subscribeEntities?e._subscribeEntities(t):new Promise(function(n,r){function i(e){if(null!==c){var t=e.data,n=t.entity_id,r=t.new_state;c=r?p(c,r):b(c,n);for(var i=0;i<a.length;i++)a[i](c)}}function s(){return e.getStates().then((function(e){c=l(e);for(var t=0;t<a.length;t++)a[t](c)}))}function o(t){t&&a.splice(a.indexOf(t),1),0===a.length&&(u(),e.removeEventListener("ready",s),e._subscribeEntities=null)}var c=null,u=null,a=[],f=null;t&&a.push(t),e._subscribeEntities=function(e){return e&&(a.push(e),null!==c&&e(c)),f.then((function(){return function(){return o(e)}}))},f=Promise.all([e.subscribeEvents(i,"state_changed"),s()]),f.then((function(r){var i=r[0];u=i,e.addEventListener("ready",s),n((function(){return o(t)}))}),(function(){return r()}))})}function m(e){return e.substr(0,e.indexOf("."))}function y(e){return e.substr(e.indexOf(".")+1)}function _(e,t){var n={};return t.attributes.entity_id.forEach((function(t){var r=e[t];r&&(n[r.entity_id]=r)})),n}function E(e){var t=[],n={};return Object.keys(e).forEach((function(r){var i=e[r];"group"===m(r)?t.push(i):n[r]=i})),t.sort((function(e,t){return e.attributes.order-t.attributes.order})),t.forEach((function(e){return e.attributes.entity_id.forEach((function(e){delete n[e]}))})),{groups:t,ungrouped:n}}function w(e,t){var n={};return t.attributes.entity_id.forEach((function(t){var r=e[t];if(r&&!r.attributes.hidden&&(n[r.entity_id]=r,"group"===m(r.entity_id))){var i=_(e,r);Object.keys(i).forEach((function(e){var t=i[e];t.attributes.hidden||(n[e]=t)}))}})),n}function k(e){var t=[];return Object.keys(e).forEach((function(n){var r=e[n];r.attributes.view&&t.push(r)})),t.sort((function(e,t){return e.entity_id===P?-1:t.entity_id===P?1:e.attributes.order-t.attributes.order})),t}var O=1,C=2,j=3,L=function(e,t){this.url=e,this.options=t||{},this.commandId=1,this.commands={},this.eventListeners={},this.closeRequested=!1,this._handleMessage=this._handleMessage.bind(this),this._handleClose=this._handleClose.bind(this)};L.prototype.setSocket=function(e)Line truncated
|
||||
!function(){"use strict";function e(e){return{type:"auth",api_password:e}}function t(){return{type:"get_states"}}function n(){return{type:"get_config"}}function i(){return{type:"get_services"}}function r(){return{type:"get_panels"}}function s(e,t,n){var i={type:"call_service",domain:e,service:t};return n&&(i.service_data=n),i}function o(e){var t={type:"subscribe_events"};return e&&(t.event_type=e),t}function c(e){return{type:"unsubscribe_events",subscription:e}}function u(){return{type:"ping"}}function a(e,t){return{type:"result",success:!1,error:{code:e,message:t}}}function d(t,n){function i(r,s,o){var c=new WebSocket(t),u=!1,a=function(){if(u)return void o(C);if(0===r)return void o(O);var e=-1===r?-1:r-1;setTimeout(function(){return i(e,s,o)},1e3)},d=function t(i){switch(JSON.parse(i.data).type){case"auth_required":"authToken"in n?c.send(JSON.stringify(e(n.authToken))):(u=!0,c.close());break;case"auth_invalid":u=!0,c.close();break;case"auth_ok":c.removeEventListener("message",t),c.removeEventListener("close",a),s(c)}};c.addEventListener("message",d),c.addEventListener("close",a)}return new Promise(function(e,t){return i(n.setupRetry||0,e,t)})}function f(e){return e.result}function v(e,t){return void 0===t&&(t={}),d(e,t).then(function(n){var i=new S(e,t);return i.setSocket(n),i})}function h(e,t){return e._subscribeConfig?e._subscribeConfig(t):new Promise(function(n,i){var r=null,s=null,o=[],c=null;t&&o.push(t);var u=function(e){r=Object.assign({},r,e);for(var t=0;t<o.length;t++)o[t](r)},a=function(e,t){return u({services:Object.assign({},r.services,(n={},n[e]=t,n))});var n},d=function(e){if(null!==r){var t=Object.assign({},r.core,{components:r.core.components.concat(e.data.component)});u({core:t})}},f=function(e){if(null!==r){var t,n=e.data,i=n.domain,s=n.service,o=Object.assign({},r.services[i]||{},(t={},t[s]={description:"",fields:{}},t));a(i,o)}},v=function(e){if(null!==r){var t=e.data,n=t.domain,i=t.service,s=r.services[n];if(s&&i in s){var o={};Object.keys(s).forEach(function(e){e!==i&&(o[e]=s[e])}),a(n,o)}}},h=function(){return Promise.all([e.getConfig(),e.getPanels(),e.getServices()]).then(function(e){var t=e[0],n=e[1],i=e[2];u({core:t,panels:n,services:i})})},l=function(e){e&&o.splice(o.indexOf(e),1),0===o.length&&s()};e._subscribeConfig=function(e){return e&&(o.push(e),null!==r&&e(r)),c.then(function(){return function(){return l(e)}})},c=Promise.all([e.subscribeEvents(d,"component_loaded"),e.subscribeEvents(f,"service_registered"),e.subscribeEvents(v,"service_removed"),h()]),c.then(function(i){var r=i[0],o=i[1],c=i[2];s=function(){removeEventListener("ready",h),r(),o(),c()},e.addEventListener("ready",h),n(function(){return l(t)})},function(){return i()})})}function l(e){for(var t={},n=0;n<e.length;n++){var i=e[n];t[i.entity_id]=i}return t}function p(e,t){var n=Object.assign({},e);return n[t.entity_id]=t,n}function b(e,t){var n=Object.assign({},e);return delete n[t],n}function g(e,t){return e._subscribeEntities?e._subscribeEntities(t):new Promise(function(n,i){function r(e){if(null!==c){var t=e.data,n=t.entity_id,i=t.new_state;c=i?p(c,i):b(c,n);for(var r=0;r<a.length;r++)a[r](c)}}function s(){return e.getStates().then(function(e){c=l(e);for(var t=0;t<a.length;t++)a[t](c)})}function o(t){t&&a.splice(a.indexOf(t),1),0===a.length&&(u(),e.removeEventListener("ready",s),e._subscribeEntities=null)}var c=null,u=null,a=[],d=null;t&&a.push(t),e._subscribeEntities=function(e){return e&&(a.push(e),null!==c&&e(c)),d.then(function(){return function(){return o(e)}})},d=Promise.all([e.subscribeEvents(r,"state_changed"),s()]),d.then(function(i){var r=i[0];u=r,e.addEventListener("ready",s),n(function(){return o(t)})},function(){return i()})})}function m(e){return e.substr(0,e.indexOf("."))}function y(e){return e.substr(e.indexOf(".")+1)}function _(e,t){var n={};return t.attributes.entity_id.forEach(function(t){var i=e[t];i&&(n[i.entity_id]=i)}),n}function E(e){var t=[],n={};return Object.keys(e).forEach(function(i){var r=e[i];"group"===m(i)?t.push(r):n[i]=r}),t.sort(function(e,t){return e.attributes.order-t.attributes.order}),t.forEach(function(e){return e.attributes.entity_id.forEach(function(e){delete n[e]})}),{groups:t,ungrouped:n}}function w(e,t){var n={};return t.attributes.entity_id.forEach(function(t){var i=e[t];if(i&&!i.attributes.hidden&&(n[i.entity_id]=i,"group"===m(i.entity_id))){var r=_(e,i);Object.keys(r).forEach(function(e){var t=r[e];t.attributes.hidden||(n[e]=t)})}}),n}function k(e){var t=[];return Object.keys(e).forEach(function(n){var i=e[n];i.attributes.view&&t.push(i)}),t.sort(function(e,t){return e.entity_id===j?-1:t.entity_id===j?1:e.attributes.order-t.attributes.order}),t}var O=1,C=2,S=function(e,t){this.url=e,this.options=t||{},this.commandId=1,this.commands={},this.eventListeners={},this.closeRequested=!1,this._handleMessage=this._handleMessage.bind(this),this._handleClose=this._handleClose.bind(this)};S.prototype.setSocket=function(e){var t=this,n=this.socket;if(this.socket=e,e.addEventLiLine truncated
|
||||
Binary file not shown.
@@ -21,7 +21,7 @@ window.hassUtil.DOMAINS_WITH_CARD = [
|
||||
];
|
||||
|
||||
window.hassUtil.DOMAINS_WITH_MORE_INFO = [
|
||||
'alarm_control_panel', 'automation', 'binary_sensor', 'camera', 'climate', 'configurator',
|
||||
'alarm_control_panel', 'automation', 'camera', 'climate', 'configurator',
|
||||
'cover', 'fan', 'group', 'light', 'lock', 'media_player', 'script',
|
||||
'sun', 'updater',
|
||||
];
|
||||
@@ -744,4 +744,4 @@ return performance.now()};else var t=function(){return Date.now()};var e=functio
|
||||
this.hass.callService('media_player', service, serviceData);
|
||||
},
|
||||
});
|
||||
}());</script><dom-module id="ha-media_player-card" assetpath="cards/"><template><style include="paper-material iron-flex iron-flex-alignment iron-positioning">:host{display:block;position:relative;font-size:0;border-radius:2px;overflow:hidden}.banner{position:relative;background-color:#fff;border-top-left-radius:2px;border-top-right-radius:2px}.banner:before{display:block;content:"";width:100%;padding-top:56%;transition:padding-top .8s}.banner.no-cover{background-position:center center;background-image:url(/static/images/card_media_player_bg.png);background-repeat:no-repeat;background-color:var(--primary-color)}.banner.content-type-music:before{padding-top:100%}.banner.no-cover:before{padding-top:88px}.banner>.cover{position:absolute;top:0;left:0;right:0;bottom:0;border-top-left-radius:2px;border-top-right-radius:2px;background-position:center center;background-size:cover;transition:opacity .8s;opacity:1}.banner.is-off>.cover{opacity:0}.banner>.caption{@apply(--paper-font-caption);position:absolute;left:0;right:0;bottom:0;background-color:rgba(0,0,0,var(--dark-secondary-opacity));padding:8px 16px;font-size:14px;font-weight:500;color:#fff;transition:background-color .5s}.banner.is-off>.caption{background-color:initial}.banner>.caption .title{@apply(--paper-font-common-nowrap);font-size:1.2em;margin:8px 0 4px}.progress{width:100%;--paper-progress-active-color:var(--accent-color);--paper-progress-container-color:#FFF}.controls{position:relative;@apply(--paper-font-body1);padding:8px;border-bottom-left-radius:2px;border-bottom-right-radius:2px;background-color:#fff}.controls paper-icon-button{width:44px;height:44px}paper-icon-button{opacity:var(--dark-primary-opacity)}paper-icon-button[disabled]{opacity:var(--dark-disabled-opacity)}paper-icon-button.primary{width:56px!important;height:56px!important;background-color:var(--primary-color);color:#fff;border-radius:50%;padding:8px;transition:background-color .5s}paper-icon-button.primary[disabled]{background-color:rgba(0,0,0,var(--dark-disabled-opacity))}[invisible]{visibility:hidden!important}</style><div class$="[[computeBannerClasses(playerObj)]]"><div class="cover" id="cover"></div><div class="caption">[[computeStateName(stateObj)]]<div class="title">[[playerObj.primaryText]]</div>[[playerObj.secondaryText]]<br></div></div><paper-progress max="[[stateObj.attributes.media_duration]]" value="[[playbackPosition]]" hidden$="[[computeHideProgress(playerObj)]]" class="progress"></paper-progress><div class="controls layout horizontal justified"><paper-icon-button icon="mdi:power" on-tap="handleTogglePower" invisible$="[[computeHidePowerButton(playerObj)]]" class="self-center secondary"></paper-icon-button><div><paper-icon-button icon="mdi:skip-previous" invisible$="[[!playerObj.supportsPreviousTrack]]" disabled="[[playerObj.isOff]]" on-tap="handlePrevious"></paper-icon-button><paper-icon-button class="primary" icon="[[computePlaybackControlIcon(playerObj)]]" invisible$="[[!computePlaybackControlIcon(playerObj)]]" disabled="[[playerObj.isOff]]" on-tap="handlePlaybackControl"></paper-icon-button><paper-icon-button icon="mdi:skip-next" invisible$="[[!playerObj.supportsNextTrack]]" disabled="[[playerObj.isOff]]" on-tap="handleNext"></paper-icon-button></div><paper-icon-button icon="mdi:dots-vertical" on-tap="handleOpenMoreInfo" class="self-center secondary"></paper-icon-button></div></template></dom-module><script>Polymer({is:"ha-media_player-card",properties:{hass:{type:Object},stateObj:{type:Object},playerObj:{type:Object,computed:"computePlayerObj(hass, stateObj)",observer:"playerObjChanged"},playbackControlIcon:{type:String,computed:"computePlaybackControlIcon(playerObj)"},playbackPosition:{type:Number},elevation:{type:Number,value:1,reflectToAttribute:!0}},created:function(){this.updatePlaybackPosition=this.updatePlaybackPosition.bind(this)},playerObjChanged:function(t){var e,o=t.stateObj.attributes.entity_picture;o?(e=document.createElement("IMG"),e.onload=function(){this.$.cover.style.backgroundImage="url("+o+")",e.onerror=e.onload=null,e.src="",e=null}.bind(this),e.onerror=function(){this.$.cover.style.backgroundImage="",this.toggleClass("no-cover",!0,this.$.cover.parentElement),e.onerror=e.onload=null,e.src="",e=null}.bind(this),this._timeout_id&&clearTimeout(this._timeout_id),this._timeout_id=setTimeout(function(){e&&(this.$.cover.style.backgroundImage=""),this._timeout_id=null},5e3),e.src=o):this.$.cover.style.backgroundImage="",t.isPlaying?(this._positionTracking||(this._positionTracking=setInterval(this.updatePlaybackPosition,1e3)),this.updatePlaybackPosition()):this._positionTracking&&(clearInterval(this._positionTracking),this._positionTracking=null,this.playbackPosition=0)},updatePlaybackPosition:function(){this.playbackPosition=this.playerObj.currentProgress},computeBannerClasses:function(t){var e="banner";return t.isOff||t.isIdle?e+=" is-off no-cover":t.stateObj.attributes.entity_picture?"music"===t.stateObj.attributes.media_content_type&&(e+=" contentLine truncated
|
||||
}());</script><dom-module id="ha-media_player-card" assetpath="cards/"><template><style include="paper-material iron-flex iron-flex-alignment iron-positioning">:host{display:block;position:relative;font-size:0;border-radius:2px;overflow:hidden}.banner{position:relative;background-color:#fff;border-top-left-radius:2px;border-top-right-radius:2px}.banner:before{display:block;content:"";width:100%;padding-top:56%;transition:padding-top .8s}.banner.no-cover{background-position:center center;background-image:url(/static/images/card_media_player_bg.png);background-repeat:no-repeat;background-color:var(--primary-color)}.banner.content-type-music:before{padding-top:100%}.banner.no-cover:before{padding-top:88px}.banner>.cover{position:absolute;top:0;left:0;right:0;bottom:0;border-top-left-radius:2px;border-top-right-radius:2px;background-position:center center;background-size:cover;transition:opacity .8s;opacity:1}.banner.is-off>.cover{opacity:0}.banner>.caption{@apply(--paper-font-caption);position:absolute;left:0;right:0;bottom:0;background-color:rgba(0,0,0,var(--dark-secondary-opacity));padding:8px 16px;font-size:14px;font-weight:500;color:#fff;transition:background-color .5s}.banner.is-off>.caption{background-color:initial}.banner>.caption .title{@apply(--paper-font-common-nowrap);font-size:1.2em;margin:8px 0 4px}.progress{width:100%;--paper-progress-active-color:var(--accent-color);--paper-progress-container-color:#FFF}.controls{position:relative;@apply(--paper-font-body1);padding:8px;border-bottom-left-radius:2px;border-bottom-right-radius:2px;background-color:#fff}.controls paper-icon-button{width:44px;height:44px}paper-icon-button{opacity:var(--dark-primary-opacity)}paper-icon-button[disabled]{opacity:var(--dark-disabled-opacity)}paper-icon-button.primary{width:56px!important;height:56px!important;background-color:var(--primary-color);color:#fff;border-radius:50%;padding:8px;transition:background-color .5s}paper-icon-button.primary[disabled]{background-color:rgba(0,0,0,var(--dark-disabled-opacity))}[invisible]{visibility:hidden!important}</style><div class$="[[computeBannerClasses(playerObj)]]"><div class="cover" id="cover"></div><div class="caption">[[computeStateName(stateObj)]]<div class="title">[[playerObj.primaryText]]</div>[[playerObj.secondaryText]]<br></div></div><paper-progress max="[[stateObj.attributes.media_duration]]" value="[[playbackPosition]]" hidden$="[[computeHideProgress(playerObj)]]" class="progress"></paper-progress><div class="controls layout horizontal justified"><paper-icon-button icon="mdi:power" on-tap="handleTogglePower" invisible$="[[computeHidePowerButton(playerObj)]]" class="self-center secondary"></paper-icon-button><div><paper-icon-button icon="mdi:skip-previous" invisible$="[[!playerObj.supportsPreviousTrack]]" disabled="[[playerObj.isOff]]" on-tap="handlePrevious"></paper-icon-button><paper-icon-button class="primary" icon="[[computePlaybackControlIcon(playerObj)]]" invisible$="[[!computePlaybackControlIcon(playerObj)]]" disabled="[[playerObj.isOff]]" on-tap="handlePlaybackControl"></paper-icon-button><paper-icon-button icon="mdi:skip-next" invisible$="[[!playerObj.supportsNextTrack]]" disabled="[[playerObj.isOff]]" on-tap="handleNext"></paper-icon-button></div><paper-icon-button icon="mdi:dots-vertical" on-tap="handleOpenMoreInfo" class="self-center secondary"></paper-icon-button></div></template></dom-module><script>Polymer({is:"ha-media_player-card",properties:{hass:{type:Object},stateObj:{type:Object},playerObj:{type:Object,computed:"computePlayerObj(hass, stateObj)",observer:"playerObjChanged"},playbackControlIcon:{type:String,computed:"computePlaybackControlIcon(playerObj)"},playbackPosition:{type:Number},elevation:{type:Number,value:1,reflectToAttribute:!0}},created:function(){this.updatePlaybackPosition=this.updatePlaybackPosition.bind(this)},playerObjChanged:function(t){var e,o=t.stateObj.attributes.entity_picture;o?(e=document.createElement("IMG"),e.onload=function(){this.$.cover.style.backgroundImage="url("+o+")",e.onerror=e.onload=null,e.src="",e=null}.bind(this),e.onerror=function(){this.$.cover.style.backgroundImage="",this.toggleClass("no-cover",!0,this.$.cover.parentElement),e.onerror=e.onload=null,e.src="",e=null}.bind(this),this._timeout_id&&clearTimeout(this._timeout_id),this._timeout_id=setTimeout(function(){e&&(this.$.cover.style.backgroundImage=""),this._timeout_id=null},5e3),e.src=o):this.$.cover.style.backgroundImage="",t.isPlaying?(this._positionTracking||(this._positionTracking=setInterval(this.updatePlaybackPosition,1e3)),this.updatePlaybackPosition()):this._positionTracking&&(clearInterval(this._positionTracking),this._positionTracking=null,this.playbackPosition=0)},updatePlaybackPosition:function(){this.playbackPosition=this.playerObj.currentProgress},computeBannerClasses:function(t){var e="banner";return t.isOff||t.isIdle?e+=" is-off no-cover":t.stateObj.attributes.entity_picture?"music"===t.stateObj.attributes.media_content_type&&(e+=" contentLine truncated
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,2 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><dom-module id="iron-image" assetpath="../../bower_components/iron-image/"><template><style>:host{display:inline-block;overflow:hidden;position:relative}#sizedImgDiv{@apply(--layout-fit);display:none}#img{display:block;width:var(--iron-image-width,auto);height:var(--iron-image-height,auto)}:host([sizing]) #sizedImgDiv{display:block}:host([sizing]) #img{display:none}#placeholder{@apply(--layout-fit);background-color:inherit;opacity:1;@apply(--iron-image-placeholder)}#placeholder.faded-out{transition:opacity .5s linear;opacity:0}</style><div id="sizedImgDiv" role="img" hidden$="[[_computeImgDivHidden(sizing)]]" aria-hidden$="[[_computeImgDivARIAHidden(alt)]]" aria-label$="[[_computeImgDivARIALabel(alt, src)]]"></div><img id="img" alt$="[[alt]]" hidden$="[[_computeImgHidden(sizing)]]"><div id="placeholder" hidden$="[[_computePlaceholderHidden(preload, fade, loading, loaded)]]" class$="[[_computePlaceholderClassName(preload, fade, loading, loaded)]]"></div></template><script>Polymer({is:"iron-image",properties:{src:{observer:"_srcChanged",type:String,value:""},alt:{type:String,value:null},preventLoad:{type:Boolean,value:!1,observer:"_preventLoadChanged"},sizing:{type:String,value:null,reflectToAttribute:!0},position:{type:String,value:"center"},preload:{type:Boolean,value:!1},placeholder:{type:String,value:null,observer:"_placeholderChanged"},fade:{type:Boolean,value:!1},loaded:{notify:!0,readOnly:!0,type:Boolean,value:!1},loading:{notify:!0,readOnly:!0,type:Boolean,value:!1},error:{notify:!0,readOnly:!0,type:Boolean,value:!1},width:{observer:"_widthChanged",type:Number,value:null},height:{observer:"_heightChanged",type:Number,value:null}},observers:["_transformChanged(sizing, position)"],ready:function(){var e=this.$.img;e.onload=function(){this.$.img.src===this._resolveSrc(this.src)&&(this._setLoading(!1),this._setLoaded(!0),this._setError(!1))}.bind(this),e.onerror=function(){this.$.img.src===this._resolveSrc(this.src)&&(this._reset(),this._setLoading(!1),this._setLoaded(!1),this._setError(!0))}.bind(this),this._resolvedSrc=""},_load:function(e){e?this.$.img.src=e:this.$.img.removeAttribute("src"),this.$.sizedImgDiv.style.backgroundImage=e?'url("'+e+'")':"",this._setLoading(!!e),this._setLoaded(!1),this._setError(!1)},_reset:function(){this.$.img.removeAttribute("src"),this.$.sizedImgDiv.style.backgroundImage="",this._setLoading(!1),this._setLoaded(!1),this._setError(!1)},_computePlaceholderHidden:function(){return!this.preload||!this.fade&&!this.loading&&this.loaded},_computePlaceholderClassName:function(){return this.preload&&this.fade&&!this.loading&&this.loaded?"faded-out":""},_computeImgDivHidden:function(){return!this.sizing},_computeImgDivARIAHidden:function(){return""===this.alt?"true":void 0},_computeImgDivARIALabel:function(){if(null!==this.alt)return this.alt;if(""===this.src)return"";var e=new URL(this._resolveSrc(this.src)).pathname.split("/");return e[e.length-1]},_computeImgHidden:function(){return!!this.sizing},_widthChanged:function(){this.style.width=isNaN(this.width)?this.width:this.width+"px"},_heightChanged:function(){this.style.height=isNaN(this.height)?this.height:this.height+"px"},_preventLoadChanged:function(){this.preventLoad||this.loaded||(this._reset(),this._load(this.src))},_srcChanged:function(e,t){var i=this._resolveSrc(e);i!==this._resolvedSrc&&(this._resolvedSrc=i,this._reset(),this.preventLoad||this._load(e))},_placeholderChanged:function(){this.$.placeholder.style.backgroundImage=this.placeholder?'url("'+this.placeholder+'")':""},_transformChanged:function(){var e=this.$.sizedImgDiv.style,t=this.$.placeholder.style;e.backgroundSize=t.backgroundSize=this.sizing,e.backgroundPosition=t.backgroundPosition=this.sizing?this.position:"",e.backgroundRepeat=t.backgroundRepeat=this.sizing?"no-repeat":""},_resolveSrc:function(e){var t=this.ownerDocument.baseURI;return new URL(Polymer.ResolveUrl.resolveUrl(e,t),t).href}})</script></dom-module><style is="custom-style">:root{--google-red-100:#f4c7c3;--google-red-300:#e67c73;--google-red-500:#db4437;--google-red-700:#c53929;--google-blue-100:#c6dafc;--google-blue-300:#7baaf7;--google-blue-500:#4285f4;--google-blue-700:#3367d6;--google-green-100:#b7e1cd;--google-green-300:#57bb8a;--google-green-500:#0f9d58;--google-green-700:#0b8043;--google-yellow-100:#fce8b2;--google-yellow-300:#f7cb4d;--google-yellow-500:#f4b400;--google-yellow-700:#f09300;--google-grey-100:#f5f5f5;--google-grey-300:#e0e0e0;--google-grey-500:#9e9e9e;--google-grey-700:#616161;--paper-red-50:#ffebee;--paper-red-100:#ffcdd2;--paper-red-200:#ef9a9a;--paper-red-300:#e57373;--paper-red-400:#ef5350;--paper-red-500:#f44336;--paper-red-600:#e53935;--paper-red-700:#d32f2f;--paper-red-800:#c62828;--paper-red-900:#b71c1c;--paper-red-a100:#ff8a80;--paper-red-a200:#ff5252;--paper-red-a400:#ff1744;--paper-red-a700:#d50000;--paper-pink-50:#fce4ec;--paper-pink-100:#f8bbd0;--paper-pink-200:#f48fb1;--paper-pinkLine truncated
|
||||
return performance.now()};else var t=function(){return Date.now()};var e=function(t,e,i){this.target=t,this.currentTime=e,this.timelineTime=i,this.type="cancel",this.bubbles=!1,this.cancelable=!1,this.currentTarget=t,this.defaultPrevented=!1,this.eventPhase=Event.AT_TARGET,this.timeStamp=Date.now()},i=window.Element.prototype.animate;window.Element.prototype.animate=function(n,r){var o=i.call(this,n,r);o._cancelHandlers=[],o.oncancel=null;var a=o.cancel;o.cancel=function(){a.call(this);var i=new e(this,null,t()),n=this._cancelHandlers.concat(this.oncancel?[this.oncancel]:[]);setTimeout(function(){n.forEach(function(t){t.call(i.target,i)})},0)};var s=o.addEventListener;o.addEventListener=function(t,e){"function"==typeof e&&"cancel"==t?this._cancelHandlers.push(e):s.call(this,t,e)};var u=o.removeEventListener;return o.removeEventListener=function(t,e){if("cancel"==t){var i=this._cancelHandlers.indexOf(e);i>=0&&this._cancelHandlers.splice(i,1)}else u.call(this,t,e)},o}}}(),function(t){var e=document.documentElement,i=null,n=!1;try{var r=getComputedStyle(e).getPropertyValue("opacity"),o="0"==r?"1":"0";i=e.animate({opacity:[o,o]},{duration:1}),i.currentTime=0,n=getComputedStyle(e).getPropertyValue("opacity")==o}catch(t){}finally{i&&i.cancel()}if(!n){var a=window.Element.prototype.animate;window.Element.prototype.animate=function(e,i){return window.Symbol&&Symbol.iterator&&Array.prototype.from&&e[Symbol.iterator]&&(e=Array.from(e)),Array.isArray(e)||null===e||(e=t.convertToArrayForm(e)),a.call(this,e,i)}}}(c),function(t,e,i){function n(t){var i=e.timeline;i.currentTime=t,i._discardAnimations(),0==i._animations.length?o=!1:requestAnimationFrame(n)}var r=window.requestAnimationFrame;window.requestAnimationFrame=function(t){return r(function(i){e.timeline._updateAnimationsPromises(),t(i),e.timeline._updateAnimationsPromises()})},e.AnimationTimeline=function(){this._animations=[],this.currentTime=void 0},e.AnimationTimeline.prototype={getAnimations:function(){return this._discardAnimations(),this._animations.slice()},_updateAnimationsPromises:function(){e.animationsWithPromises=e.animationsWithPromises.filter(function(t){return t._updatePromises()})},_discardAnimations:function(){this._updateAnimationsPromises(),this._animations=this._animations.filter(function(t){return"finished"!=t.playState&&"idle"!=t.playState})},_play:function(t){var i=new e.Animation(t,this);return this._animations.push(i),e.restartWebAnimationsNextTick(),i._updatePromises(),i._animation.play(),i._updatePromises(),i},play:function(t){return t&&t.remove(),this._play(t)}};var o=!1;e.restartWebAnimationsNextTick=function(){o||(o=!0,requestAnimationFrame(n))};var a=new e.AnimationTimeline;e.timeline=a;try{Object.defineProperty(window.document,"timeline",{configurable:!0,get:function(){return a}})}catch(t){}try{window.document.timeline=a}catch(t){}}(0,e),function(t,e,i){e.animationsWithPromises=[],e.Animation=function(e,i){if(this.id="",e&&e._id&&(this.id=e._id),this.effect=e,e&&(e._animation=this),!i)throw new Error("Animation with null timeline is not supported");this._timeline=i,this._sequenceNumber=t.sequenceNumber++,this._holdTime=0,this._paused=!1,this._isGroup=!1,this._animation=null,this._childAnimations=[],this._callback=null,this._oldPlayState="idle",this._rebuildUnderlyingAnimation(),this._animation.cancel(),this._updatePromises()},e.Animation.prototype={_updatePromises:function(){var t=this._oldPlayState,e=this.playState;return this._readyPromise&&e!==t&&("idle"==e?(this._rejectReadyPromise(),this._readyPromise=void 0):"pending"==t?this._resolveReadyPromise():"pending"==e&&(this._readyPromise=void 0)),this._finishedPromise&&e!==t&&("idle"==e?(this._rejectFinishedPromise(),this._finishedPromise=void 0):"finished"==e?this._resolveFinishedPromise():"finished"==t&&(this._finishedPromise=void 0)),this._oldPlayState=this.playState,this._readyPromise||this._finishedPromise},_rebuildUnderlyingAnimation:function(){this._updatePromises();var t,i,n,r,o=!!this._animation;o&&(t=this.playbackRate,i=this._paused,n=this.startTime,r=this.currentTime,this._animation.cancel(),this._animation._wrapper=null,this._animation=null),(!this.effect||this.effect instanceof window.KeyframeEffect)&&(this._animation=e.newUnderlyingAnimationForKeyframeEffect(this.effect),e.bindAnimationForKeyframeEffect(this)),(this.effect instanceof window.SequenceEffect||this.effect instanceof window.GroupEffect)&&(this._animation=e.newUnderlyingAnimationForGroup(this.effect),e.bindAnimationForGroup(this)),this.effect&&this.effect._onsample&&e.bindAnimationForCustomEffect(this),o&&(1!=t&&(this.playbackRate=t),null!==n?this.startTime=n:null!==r?this.currentTime=r:null!==this._holdTime&&(this.currentTime=this._holdTime),i&&this.pause()),this._updatePromises()},_updateChildren:function(){if(this.effect&&"idle"!=this.playState){var t=this.effect._timing.delay;this._childAnimations.forEach(function(i){this._arrangeChildren(i,t),this.effect instanceof window.SequenceEffect&&(t+=e.groupChiLine truncated
|
||||
Binary file not shown.
@@ -1,17 +1 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><dom-module id="iron-image" assetpath="../../bower_components/iron-image/"><template><style>:host{display:inline-block;overflow:hidden;position:relative}#sizedImgDiv{@apply(--layout-fit);display:none}#img{display:block;width:var(--iron-image-width,auto);height:var(--iron-image-height,auto)}:host([sizing]) #sizedImgDiv{display:block}:host([sizing]) #img{display:none}#placeholder{@apply(--layout-fit);background-color:inherit;opacity:1;@apply(--iron-image-placeholder)}#placeholder.faded-out{transition:opacity .5s linear;opacity:0}</style><div id="sizedImgDiv" role="img" hidden$="[[_computeImgDivHidden(sizing)]]" aria-hidden$="[[_computeImgDivARIAHidden(alt)]]" aria-label$="[[_computeImgDivARIALabel(alt, src)]]"></div><img id="img" alt$="[[alt]]" hidden$="[[_computeImgHidden(sizing)]]"><div id="placeholder" hidden$="[[_computePlaceholderHidden(preload, fade, loading, loaded)]]" class$="[[_computePlaceholderClassName(preload, fade, loading, loaded)]]"></div></template><script>Polymer({is:"iron-image",properties:{src:{observer:"_srcChanged",type:String,value:""},alt:{type:String,value:null},preventLoad:{type:Boolean,value:!1,observer:"_preventLoadChanged"},sizing:{type:String,value:null,reflectToAttribute:!0},position:{type:String,value:"center"},preload:{type:Boolean,value:!1},placeholder:{type:String,value:null,observer:"_placeholderChanged"},fade:{type:Boolean,value:!1},loaded:{notify:!0,readOnly:!0,type:Boolean,value:!1},loading:{notify:!0,readOnly:!0,type:Boolean,value:!1},error:{notify:!0,readOnly:!0,type:Boolean,value:!1},width:{observer:"_widthChanged",type:Number,value:null},height:{observer:"_heightChanged",type:Number,value:null}},observers:["_transformChanged(sizing, position)"],ready:function(){var e=this.$.img;e.onload=function(){this.$.img.src===this._resolveSrc(this.src)&&(this._setLoading(!1),this._setLoaded(!0),this._setError(!1))}.bind(this),e.onerror=function(){this.$.img.src===this._resolveSrc(this.src)&&(this._reset(),this._setLoading(!1),this._setLoaded(!1),this._setError(!0))}.bind(this),this._resolvedSrc=""},_load:function(e){e?this.$.img.src=e:this.$.img.removeAttribute("src"),this.$.sizedImgDiv.style.backgroundImage=e?'url("'+e+'")':"",this._setLoading(!!e),this._setLoaded(!1),this._setError(!1)},_reset:function(){this.$.img.removeAttribute("src"),this.$.sizedImgDiv.style.backgroundImage="",this._setLoading(!1),this._setLoaded(!1),this._setError(!1)},_computePlaceholderHidden:function(){return!this.preload||!this.fade&&!this.loading&&this.loaded},_computePlaceholderClassName:function(){return this.preload&&this.fade&&!this.loading&&this.loaded?"faded-out":""},_computeImgDivHidden:function(){return!this.sizing},_computeImgDivARIAHidden:function(){return""===this.alt?"true":void 0},_computeImgDivARIALabel:function(){if(null!==this.alt)return this.alt;if(""===this.src)return"";var e=new URL(this._resolveSrc(this.src)).pathname.split("/");return e[e.length-1]},_computeImgHidden:function(){return!!this.sizing},_widthChanged:function(){this.style.width=isNaN(this.width)?this.width:this.width+"px"},_heightChanged:function(){this.style.height=isNaN(this.height)?this.height:this.height+"px"},_preventLoadChanged:function(){this.preventLoad||this.loaded||(this._reset(),this._load(this.src))},_srcChanged:function(e,t){var i=this._resolveSrc(e);i!==this._resolvedSrc&&(this._resolvedSrc=i,this._reset(),this.preventLoad||this._load(e))},_placeholderChanged:function(){this.$.placeholder.style.backgroundImage=this.placeholder?'url("'+this.placeholder+'")':""},_transformChanged:function(){var e=this.$.sizedImgDiv.style,t=this.$.placeholder.style;e.backgroundSize=t.backgroundSize=this.sizing,e.backgroundPosition=t.backgroundPosition=this.sizing?this.position:"",e.backgroundRepeat=t.backgroundRepeat=this.sizing?"no-repeat":""},_resolveSrc:function(e){var t=this.ownerDocument.baseURI;return new URL(Polymer.ResolveUrl.resolveUrl(e,t),t).href}})</script></dom-module><dom-module id="paper-card" assetpath="../../bower_components/paper-card/"><template><style include="paper-material">:host{display:inline-block;position:relative;box-sizing:border-box;background-color:var(--paper-card-background-color,--primary-background-color);border-radius:2px;@apply(--paper-font-common-base);@apply(--paper-card)}[hidden]{display:none!important}.header{position:relative;border-top-left-radius:inherit;border-top-right-radius:inherit;overflow:hidden;@apply(--paper-card-header)}.header iron-image{display:block;width:100%;--iron-image-width:100%;pointer-events:none;@apply(--paper-card-header-image)}.header .title-text{padding:16px;font-size:24px;font-weight:400;color:var(--paper-card-header-color,#000);@apply(--paper-card-header-text)}.header .title-text.over-image{position:absolute;bottom:0;@apply(--paper-card-header-image-text)}:host ::content .card-content{padding:16px;position:relative;@apply(--paper-card-content)}:host ::content .card-actions{borderLine truncated
|
||||
is: 'hassio-addon-info',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
addonInfo: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
computeInstallStatus(addon) {
|
||||
return (addon && addon.installed) || 'Not installed';
|
||||
},
|
||||
});</script><dom-module id="hassio-addon-state" assetpath="addon-view/"><template><style include="ha-style">:host{display:block}paper-card{display:block}</style><paper-card heading="Installed"><template is="dom-if" if="[[!addonState]]"><div class="card-content"><div>Add-on is not installed.</div></div><div class="card-actions"><ha-call-api-button hass="[[hass]]" path="[[pathInstall(addon)]]">Install</ha-call-api-button></div></template><template is="dom-if" if="[[addonState]]"><div class="card-content"><table class="info"><tbody><tr><td>Version</td><td>[[addonState.version]]</td></tr><tr><td>State</td><td>[[addonState.state]]</td></tr><tr><td>Boot</td><td>[[addonState.boot]]</td></tr></tbody></table></div><div class="card-actions"><template is="dom-if" if="[[!isRunning]]"><ha-call-api-button hass="[[hass]]" path="[[pathStart(addon)]]">Start</ha-call-api-button></template><template is="dom-if" if="[[isRunning]]"><ha-call-api-button class="warning" hass="[[hass]]" path="[[pathStop(addon)]]">Stop</ha-call-api-button></template><template is="dom-if" if="[[computeUpdateAvailable(addonState)]]"><ha-call-api-button hass="[[hass]]" path="[[pathUpdate(addon)]]">Update</ha-call-api-button></template><ha-call-api-button class="warning" hass="[[hass]]" path="[[pathUninstall(addon)]]">Uninstall</ha-call-api-button></div></template></paper-card></template></dom-module><script>Polymer({is:"hassio-addon-state",properties:{hass:{type:Object},addon:{type:String},isRunning:{type:Boolean,computed:"computeIsRunning(addonState)"},addonInfo:{type:Object,value:null},addonState:{type:Object,value:null}},computeIsRunning:function(t){return t&&"started"===t.state},computeUpdateAvailable:function(t){return t&&!t.detached&&t.version!==t.last_version},pathStart:function(t){return"hassio/addons/"+t+"/start"},pathStop:function(t){return"hassio/addons/"+t+"/stop"},pathInstall:function(t){return"hassio/addons/"+t+"/install"},pathUninstall:function(t){return"hassio/addons/"+t+"/uninstall"},pathUpdate:function(t){return"hassio/addons/"+t+"/update"}})</script><dom-module id="hassio-addon-options" assetpath="addon-view/"><template><style include="ha-style">:host{display:block}paper-card{display:block}iron-autogrow-textarea{width:100%}</style><paper-card heading="Options"><div class="card-content"><iron-autogrow-textarea value="{{options}}"></iron-autogrow-textarea></div><div class="card-actions"><ha-call-api-button hass="[[hass]]" disabled="[[!optionsParsed]]" data="[[computeOptionsData(optionsParsed)]]" path="[[pathOptions(addon)]]">Save</ha-call-api-button></div></paper-card></template></dom-module><script>Polymer({is:"hassio-addon-options",properties:{hass:{type:Object},addon:{type:String},options:{type:String,value:""},optionsParsed:{type:Object,computed:"parseOptions(options)"},addonState:{type:Object,value:null,observer:"addonStateChanged"}},addonStateChanged:function(t){this.options=t?JSON.stringify(t.options,null,2):""},parseOptions:function(t){try{return JSON.parse(t)}catch(t){return null}},computeOptionsData:function(t){return{options:t}},pathOptions:function(t){return"hassio/addons/"+t+"/options"}})</script><dom-module id="hassio-addon-logs" assetpath="addon-view/"><template><style include="iron-flex ha-style">:host,paper-card{display:block}</style><paper-card heading="Logs"><div class="card-content"><pre>[[addonLogs]]</pre></div><div class="card-actions"><paper-button on-tap="refresh">Refresh</paper-button></div></paper-card></template></dom-module><script>Polymer({is:"hassio-addon-logs",properties:{hass:{type:Object},addon:{type:String,observer:"addonChanged"},addonLogs:{type:String,value:""}},addonChanged:function(s){if(!this.hass)return void setTimeout(function(){this.addonChanged(s)}.bind(this),0);this.refresh()},refresh:function(){this.hass.callApi("get","hassio/addons/"+this.addon+"/logs").then(function(s){this.addonLogs=s}.bind(this))}})</script><dom-module id="hassio-addon-view" assetpath="addon-view/"><template><style include="iron-flex ha-style">paper-card{display:block}.controls paper-card,hassio-addon-options{margin-bottom:24px}.content{padding:24px 0 32px;max-width:600px;margin:0 auto}.status{@apply(--layout-horizontal);margin-bottom:24px}.status>*{@apply(--layout-flex)}.status>:first-child{margin-right:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><paper-icon-button icon="mdi:arrow-left" on-tap="backTapped"></paper-icon-button><div main-title="">[[addonInfo.name]]</div></app-toolbar></app-header><div class="content"><div class="status"><hassio-addon-info hass="[[hass]]" addon-info="[[addonInfo]]"></hassio-addon-info><hassio-addon-state hass="[[hass]]" addon="[[addon]]" addon-info="[[addonInfo]]" addon-state="[[addonState]]"></hassio-addon-state></div><template is="dom-if" if="[[addonState]]"><div class="controls"><hassio-addon-options hass="[[hass]]" addon="[[addon]]" addon-state="[[addonState]]"></hassio-addon-options><hassio-addon-logs hass="[[hass]]" addon="[[addon]]"></hLine truncated
|
||||
<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>
|
||||
Binary file not shown.
@@ -1 +1,647 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><style>[hidden]{display:none!important}</style><style is="custom-style">:root{--layout:{display:-ms-flexbox;display:-webkit-flex;display:flex};--layout-inline:{display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex};--layout-horizontal:{@apply(--layout);-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row};--layout-horizontal-reverse:{@apply(--layout);-ms-flex-direction:row-reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse};--layout-vertical:{@apply(--layout);-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column};--layout-vertical-reverse:{@apply(--layout);-ms-flex-direction:column-reverse;-webkit-flex-direction:column-reverse;flex-direction:column-reverse};--layout-wrap:{-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap};--layout-no-wrap:{-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap};--layout-wrap-reverse:{-ms-flex-wrap:wrap-reverse;-webkit-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse};--layout-flex-auto:{-ms-flex:1 1 auto;-webkit-flex:1 1 auto;flex:1 1 auto};--layout-flex-none:{-ms-flex:none;-webkit-flex:none;flex:none};--layout-flex:{-ms-flex:1 1 .000000001px;-webkit-flex:1;flex:1;-webkit-flex-basis:.000000001px;flex-basis:.000000001px};--layout-flex-2:{-ms-flex:2;-webkit-flex:2;flex:2};--layout-flex-3:{-ms-flex:3;-webkit-flex:3;flex:3};--layout-flex-4:{-ms-flex:4;-webkit-flex:4;flex:4};--layout-flex-5:{-ms-flex:5;-webkit-flex:5;flex:5};--layout-flex-6:{-ms-flex:6;-webkit-flex:6;flex:6};--layout-flex-7:{-ms-flex:7;-webkit-flex:7;flex:7};--layout-flex-8:{-ms-flex:8;-webkit-flex:8;flex:8};--layout-flex-9:{-ms-flex:9;-webkit-flex:9;flex:9};--layout-flex-10:{-ms-flex:10;-webkit-flex:10;flex:10};--layout-flex-11:{-ms-flex:11;-webkit-flex:11;flex:11};--layout-flex-12:{-ms-flex:12;-webkit-flex:12;flex:12};--layout-start:{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start};--layout-center:{-ms-flex-align:center;-webkit-align-items:center;align-items:center};--layout-end:{-ms-flex-align:end;-webkit-align-items:flex-end;align-items:flex-end};--layout-baseline:{-ms-flex-align:baseline;-webkit-align-items:baseline;align-items:baseline};--layout-start-justified:{-ms-flex-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start};--layout-center-justified:{-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center};--layout-end-justified:{-ms-flex-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end};--layout-around-justified:{-ms-flex-pack:distribute;-webkit-justify-content:space-around;justify-content:space-around};--layout-justified:{-ms-flex-pack:justify;-webkit-justify-content:space-between;justify-content:space-between};--layout-center-center:{@apply(--layout-center);@apply(--layout-center-justified)};--layout-self-start:{-ms-align-self:flex-start;-webkit-align-self:flex-start;align-self:flex-start};--layout-self-center:{-ms-align-self:center;-webkit-align-self:center;align-self:center};--layout-self-end:{-ms-align-self:flex-end;-webkit-align-self:flex-end;align-self:flex-end};--layout-self-stretch:{-ms-align-self:stretch;-webkit-align-self:stretch;align-self:stretch};--layout-self-baseline:{-ms-align-self:baseline;-webkit-align-self:baseline;align-self:baseline};--layout-start-aligned:{-ms-flex-line-pack:start;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start};--layout-end-aligned:{-ms-flex-line-pack:end;-ms-align-content:flex-end;-webkit-align-content:flex-end;align-content:flex-end};--layout-center-aligned:{-ms-flex-line-pack:center;-ms-align-content:center;-webkit-align-content:center;align-content:center};--layout-between-aligned:{-ms-flex-line-pack:justify;-ms-align-content:space-between;-webkit-align-content:space-between;align-content:space-between};--layout-around-aligned:{-ms-flex-line-pack:distribute;-ms-align-content:space-around;-webkit-align-content:space-around;align-content:space-around};--layout-block:{display:block};--layout-invisible:{visibility:hidden!important};--layout-relative:{position:relative};--layout-fit:{position:absolute;top:0;right:0;bottom:0;left:0};--layout-scroll:{-webkit-overflow-scrolling:touch;overflow:auto};--layout-fullbleed:{margin:0;height:100vh};--layout-fixed-top:{position:fixed;top:0;left:0;right:0};--layout-fixed-right:{position:fixed;top:0;right:0;bottom:0};--layout-fixed-bottom:{position:fixed;right:0;bottom:0;left:0};--layout-fixed-left:{position:fixed;top:0;bottom:0;left:0};}</style><dom-module id="iron-image" assetpath="../../bower_components/iron-image/"><template><style>:host{display:inline-block;overflow:hidden;position:relative}#sizedImgDiv{@apply(--layout-fit);display:none}#img{display:block;width:var(--iron-image-width,auto);height:var(--iron-image-height,auto)}:host([sizing]) #sizedImgDiv{display:block}:host([sizing]) #img{display:none}#placeholder{@apply(--layout-fit);bLine truncated
|
||||
<html><head><meta charset="UTF-8"></head><body><div hidden="" by-vulcanize=""><style>[hidden]{display:none!important}</style><style is="custom-style">:root{--layout:{display:-ms-flexbox;display:-webkit-flex;display:flex};--layout-inline:{display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex};--layout-horizontal:{@apply(--layout);-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row};--layout-horizontal-reverse:{@apply(--layout);-ms-flex-direction:row-reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse};--layout-vertical:{@apply(--layout);-ms-flex-direction:column;-webkit-flex-direction:column;flex-direction:column};--layout-vertical-reverse:{@apply(--layout);-ms-flex-direction:column-reverse;-webkit-flex-direction:column-reverse;flex-direction:column-reverse};--layout-wrap:{-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap};--layout-no-wrap:{-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap};--layout-wrap-reverse:{-ms-flex-wrap:wrap-reverse;-webkit-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse};--layout-flex-auto:{-ms-flex:1 1 auto;-webkit-flex:1 1 auto;flex:1 1 auto};--layout-flex-none:{-ms-flex:none;-webkit-flex:none;flex:none};--layout-flex:{-ms-flex:1 1 .000000001px;-webkit-flex:1;flex:1;-webkit-flex-basis:.000000001px;flex-basis:.000000001px};--layout-flex-2:{-ms-flex:2;-webkit-flex:2;flex:2};--layout-flex-3:{-ms-flex:3;-webkit-flex:3;flex:3};--layout-flex-4:{-ms-flex:4;-webkit-flex:4;flex:4};--layout-flex-5:{-ms-flex:5;-webkit-flex:5;flex:5};--layout-flex-6:{-ms-flex:6;-webkit-flex:6;flex:6};--layout-flex-7:{-ms-flex:7;-webkit-flex:7;flex:7};--layout-flex-8:{-ms-flex:8;-webkit-flex:8;flex:8};--layout-flex-9:{-ms-flex:9;-webkit-flex:9;flex:9};--layout-flex-10:{-ms-flex:10;-webkit-flex:10;flex:10};--layout-flex-11:{-ms-flex:11;-webkit-flex:11;flex:11};--layout-flex-12:{-ms-flex:12;-webkit-flex:12;flex:12};--layout-start:{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start};--layout-center:{-ms-flex-align:center;-webkit-align-items:center;align-items:center};--layout-end:{-ms-flex-align:end;-webkit-align-items:flex-end;align-items:flex-end};--layout-baseline:{-ms-flex-align:baseline;-webkit-align-items:baseline;align-items:baseline};--layout-start-justified:{-ms-flex-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start};--layout-center-justified:{-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center};--layout-end-justified:{-ms-flex-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end};--layout-around-justified:{-ms-flex-pack:distribute;-webkit-justify-content:space-around;justify-content:space-around};--layout-justified:{-ms-flex-pack:justify;-webkit-justify-content:space-between;justify-content:space-between};--layout-center-center:{@apply(--layout-center);@apply(--layout-center-justified)};--layout-self-start:{-ms-align-self:flex-start;-webkit-align-self:flex-start;align-self:flex-start};--layout-self-center:{-ms-align-self:center;-webkit-align-self:center;align-self:center};--layout-self-end:{-ms-align-self:flex-end;-webkit-align-self:flex-end;align-self:flex-end};--layout-self-stretch:{-ms-align-self:stretch;-webkit-align-self:stretch;align-self:stretch};--layout-self-baseline:{-ms-align-self:baseline;-webkit-align-self:baseline;align-self:baseline};--layout-start-aligned:{-ms-flex-line-pack:start;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start};--layout-end-aligned:{-ms-flex-line-pack:end;-ms-align-content:flex-end;-webkit-align-content:flex-end;align-content:flex-end};--layout-center-aligned:{-ms-flex-line-pack:center;-ms-align-content:center;-webkit-align-content:center;align-content:center};--layout-between-aligned:{-ms-flex-line-pack:justify;-ms-align-content:space-between;-webkit-align-content:space-between;align-content:space-between};--layout-around-aligned:{-ms-flex-line-pack:distribute;-ms-align-content:space-around;-webkit-align-content:space-around;align-content:space-around};--layout-block:{display:block};--layout-invisible:{visibility:hidden!important};--layout-relative:{position:relative};--layout-fit:{position:absolute;top:0;right:0;bottom:0;left:0};--layout-scroll:{-webkit-overflow-scrolling:touch;overflow:auto};--layout-fullbleed:{margin:0;height:100vh};--layout-fixed-top:{position:fixed;top:0;left:0;right:0};--layout-fixed-right:{position:fixed;top:0;right:0;bottom:0};--layout-fixed-bottom:{position:fixed;right:0;bottom:0;left:0};--layout-fixed-left:{position:fixed;top:0;bottom:0;left:0};}</style><dom-module id="iron-image" assetpath="../../bower_components/iron-image/"><template><style>:host{display:inline-block;overflow:hidden;position:relative}#sizedImgDiv{@apply(--layout-fit);display:none}#img{display:block;width:var(--iron-image-width,auto);height:var(--iron-image-height,auto)}:host([sizing]) #sizedImgDiv{display:block}:host([sizing]) #img{display:none}#placeholder{@apply(--layout-fit);bLine truncated
|
||||
is: 'zwave-node-information',
|
||||
|
||||
properties: {
|
||||
nodes: {
|
||||
type: Array,
|
||||
observer: 'nodeChanged'
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'nodeChanged'
|
||||
},
|
||||
|
||||
selectedNodeAttrs: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
nodeInfoActive: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
nodeChanged: function (selectedNode) {
|
||||
if (!this.nodes || selectedNode === -1) return;
|
||||
var nodeAttrs = this.nodes[this.selectedNode].attributes;
|
||||
var att = [];
|
||||
Object.entries(nodeAttrs).forEach(([key, value]) => {
|
||||
att.push(key + ': ' + value);
|
||||
});
|
||||
this.selectedNodeAttrs = att.sort();
|
||||
},
|
||||
});</script><dom-module id="zwave-node-config" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node config options"><template is="dom-if" if="[[wakeupNode]]"><div class="card-actions"><paper-input float-label="Wakeup Interval" type="number" value="{{wakeupInput}}" placeholder="[[computeGetWakeupValue(selectedNode)]]"><div suffix="">seconds</div></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_wakeup" service-data="[[computeWakeupServiceData(wakeupInput)]]">Set Wakeup</ha-call-service-button></div></template><div class="device-picker"><paper-dropdown-menu label="Config parameter" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedConfigParameter}}"><template is="dom-repeat" items="[[config]]" as="state"><paper-item>[[computeSelectCaptionConfigParameter(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'List')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Byte Short Int')]]"><div class="card-actions"><paper-input label="{{selectedConfigParameterNumValues}}" type="number" value="{{selectedConfigValue}}" max="{{configParameterMax}}" min="{{configParameterMin}}"></paper-input></div></template><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button')]]"><div class="device-picker"><paper-dropdown-menu label="Config value" class="flex" placeholder="{{loadedConfigValue}}"><paper-listbox class="dropdown-content" selected="{{selectedConfigValue}}"><template is="dom-repeat" items="[[selectedConfigParameterValues]]" as="state"><paper-item>[[state]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><div class="help-text"><span>[[configValueHelpText]]</span></div><template is="dom-if" if="[[isConfigParameterSelected(selectedConfigParameter, 'Bool Button Byte Short Int List')]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="set_config_parameter" service-data="[[computeSetConfigParameterServiceData(selectedConfigValue)]]">Set Config Parameter</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-node-config',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
observer: 'nodesChanged'
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'nodesChanged'
|
||||
},
|
||||
|
||||
config: {
|
||||
type: Array,
|
||||
value: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
selectedConfigParameter: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedConfigParameterChanged'
|
||||
},
|
||||
|
||||
configParameterMax: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
configParameterMin: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
configValueHelpText: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: 'computeConfigValueHelp(selectedConfigParameter)'
|
||||
},
|
||||
|
||||
selectedConfigParameterType: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
|
||||
selectedConfigValue: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'computeSetConfigParameterServiceData'
|
||||
},
|
||||
|
||||
selectedConfigParameterValues: {
|
||||
type: Array,
|
||||
value: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
selectedConfigParameterNumValues: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
|
||||
loadedConfigValue: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
wakeupInput: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
wakeupNode: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshConfig(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
nodesChanged: function () {
|
||||
if (!this.nodes) return;
|
||||
this.wakeupNode = (this.nodes[this.selectedNode].attributes.wake_up_interval === 0 ||
|
||||
this.nodes[this.selectedNode].attributes.wake_up_interval);
|
||||
if (this.wakeupNode) {
|
||||
if (this.nodes[this.selectedNode].attributes.wake_up_interval === 0) this.wakeupInput = '';
|
||||
else this.wakeupInput = this.nodes[this.selectedNode].attributes.wake_up_interval;
|
||||
}
|
||||
},
|
||||
|
||||
computeGetWakeupValue: function (selectedNode) {
|
||||
if (this.selectedNode === -1 ||
|
||||
!this.nodes[selectedNode].attributes.wake_up_interval) return 'unknown';
|
||||
return (this.nodes[selectedNode].attributes.wake_up_interval);
|
||||
},
|
||||
|
||||
computeWakeupServiceData: function (wakeupInput) {
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
value: wakeupInput };
|
||||
},
|
||||
|
||||
computeConfigValueHelp: function (selectedConfigParameter) {
|
||||
if (selectedConfigParameter === -1) return '';
|
||||
var helpText = this.config[selectedConfigParameter].value.help;
|
||||
if (!helpText) return ['No helptext available'];
|
||||
return helpText;
|
||||
},
|
||||
|
||||
computeSetConfigParameterServiceData: function (selectedConfigValue) {
|
||||
if (this.selectedNode === -1 || this.selectedConfigParameter === -1) return -1;
|
||||
var valueData = null;
|
||||
if (('Short Byte Int').includes(this.selectedConfigParameterType)) {
|
||||
valueData = parseInt(selectedConfigValue, 10);
|
||||
} if (('Bool Button').includes(this.selectedConfigParameterType)) {
|
||||
valueData = this.selectedConfigParameterValues[selectedConfigValue];
|
||||
} if (this.selectedConfigParameterType === 'List') {
|
||||
valueData = this.selectedConfigParameterValues[selectedConfigValue];
|
||||
}
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
parameter: this.config[this.selectedConfigParameter].key,
|
||||
value: valueData };
|
||||
},
|
||||
|
||||
selectedConfigParameterChanged: function (selectedConfigParameter) {
|
||||
if (selectedConfigParameter === -1) return;
|
||||
this.selectedConfigValue = -1;
|
||||
this.loadedConfigValue = -1;
|
||||
this.selectedConfigParameterValues = [];
|
||||
this.selectedConfigParameterType = this.config[selectedConfigParameter].value.type;
|
||||
this.configParameterMax = this.config[selectedConfigParameter].value.max;
|
||||
this.configParameterMin = this.config[selectedConfigParameter].value.min;
|
||||
this.loadedConfigValue = this.config[selectedConfigParameter].value.data;
|
||||
this.configValueHelpText = this.config[selectedConfigParameter].value.help;
|
||||
if (('Short Byte Int').includes(this.selectedConfigParameterType)) {
|
||||
this.selectedConfigParameterNumValues = this.config[selectedConfigParameter].value.data_items;
|
||||
this.selectedConfigValue = this.loadedConfigValue;
|
||||
}
|
||||
if (('Bool Button').includes(this.selectedConfigParameterType)) {
|
||||
this.selectedConfigParameterValues = ['True', 'False'];
|
||||
if (this.config[selectedConfigParameter].value.data) {
|
||||
this.loadedConfigValue = 'True';
|
||||
} else this.loadedConfigValue = 'False';
|
||||
}
|
||||
if (('List').includes(this.selectedConfigParameterType)) {
|
||||
this.selectedConfigParameterValues = this.config[selectedConfigParameter].value.data_items;
|
||||
}
|
||||
},
|
||||
|
||||
isConfigParameterSelected: function (selectedConfigParameter, type) {
|
||||
if (selectedConfigParameter === -1) return false;
|
||||
if (this.config[selectedConfigParameter].value.type === type) return true;
|
||||
if (type.includes(this.config[selectedConfigParameter].value.type)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
computeSelectCaptionConfigParameter: function (stateObj) {
|
||||
return (stateObj.key + ': ' + stateObj.value.label);
|
||||
},
|
||||
|
||||
refreshConfig: function (selectedNode) {
|
||||
var configData = [];
|
||||
this.hass.callApi('GET', 'zwave/config/' + this.nodes[selectedNode].attributes.node_id).then(function (config) {
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
configData.push({ key, value });
|
||||
});
|
||||
this.config = configData;
|
||||
this.selectedConfigParameterChanged(this.selectedConfigParameter);
|
||||
}.bind(this));
|
||||
},
|
||||
});</script><dom-module id="zwave-usercodes" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><div class="content"><paper-card heading="Node user codes"><div class="device-picker"><paper-dropdown-menu label="Code slot" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedUserCode}}"><template is="dom-repeat" items="[[userCodes]]" as="state"><paper-item>[[computeSelectCaptionUserCodes(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[isUserCodeSelected(selectedUserCode)]]"><div class="card-actions"><paper-input label="User code" type="number" value="{{selectedUserCodeValue}}" maxlength="{{userCodeMaxLen}}" min="0"></paper-input></div><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="lock" service="set_usercode" service-data="[[computeUserCodeServiceData(selectedUserCodeValue, "Add")]]">Set Usercode</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="lock" service="clear_usercode" service-data="[[computeUserCodeServiceData(selectedUserCode, "Delete")]]">Delete Usercode</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-usercodes',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
userCodes: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
userCodeMaxLen: {
|
||||
type: Number,
|
||||
value: 4
|
||||
},
|
||||
|
||||
selectedUserCode: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedUserCodeChanged'
|
||||
},
|
||||
|
||||
selectedUserCodeValue: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshUserCodes(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
isUserCodeSelected: function (selectedUserCode) {
|
||||
if (selectedUserCode === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
computeSelectCaptionUserCodes: function (stateObj) {
|
||||
return (stateObj.key + ': ' + stateObj.value.label);
|
||||
},
|
||||
|
||||
selectedUserCodeChanged: function (selectedUserCode) {
|
||||
if (this.selectedUserCode === -1 || selectedUserCode === -1) return;
|
||||
var value = (parseInt((this.userCodes[selectedUserCode].value.code).trim()));
|
||||
this.userCodeMaxLen = (this.userCodes[selectedUserCode].value.length);
|
||||
if (isNaN(value)) this.selectedUserCodeValue = '';
|
||||
else this.selectedUserCodeValue = value;
|
||||
},
|
||||
|
||||
computeUserCodeServiceData: function (selectedUserCodeValue, type) {
|
||||
if (this.selectedNode === -1 || !selectedUserCodeValue) return -1;
|
||||
var serviceData = null;
|
||||
var valueData = null;
|
||||
if (type === 'Add') {
|
||||
valueData = selectedUserCodeValue;
|
||||
serviceData = { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
code_slot: this.selectedUserCode,
|
||||
usercode: valueData };
|
||||
}
|
||||
if (type === 'Delete') {
|
||||
serviceData = { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
code_slot: this.selectedUserCode };
|
||||
}
|
||||
return serviceData;
|
||||
},
|
||||
|
||||
refreshUserCodes: function (selectedNode) {
|
||||
this.selectedUserCodeValue = '';
|
||||
var userCodes = [];
|
||||
this.hass.callApi('GET', 'zwave/usercodes/' + this.nodes[selectedNode].attributes.node_id).then(function (usercodes) {
|
||||
Object.entries(usercodes).forEach(([key, value]) => {
|
||||
userCodes.push({ key, value });
|
||||
});
|
||||
this.userCodes = userCodes;
|
||||
this.selectedUserCodeChanged(this.selectedUserCode);
|
||||
}.bind(this));
|
||||
},
|
||||
});</script><dom-module id="zwave-groups" assetpath="./"><template><style include="iron-flex ha-style">.content{margin-top:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}.help-text{padding-left:24px;padding-right:24px}</style><div class="content"><paper-card heading="Node group associations"><div class="device-picker"><paper-dropdown-menu label="Node to control" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedTargetNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsTargetNodeSelected(selectedTargetNode)]]"><div class="device-picker"><paper-dropdown-menu label="Group" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedGroup}}"><template is="dom-repeat" items="[[groups]]" as="state"><paper-item>[[computeSelectCaptionGroup(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div></template><template is="dom-if" if="[[!computeIsGroupSelected(selectedGroup)]]"><div class="help-text"><span>Other Nodes in this group:</span><template is="dom-repeat" items="[[otherGroupNodes]]" as="state"><span>[[state]]</span></template></div><div class="help-text"><span>Max Associations:</span> <span>[[maxAssociations]]</span></div><div class="card-actions"><template is="dom-if" if="[[!noAssociationsLeft]]"><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "add")]]">Add To Group</ha-call-service-button></template><ha-call-service-button hass="[[hass]]" domain="zwave" service="change_association" service-data="[[computeAssocServiceData(selectedGroup, "remove")]]">Remove From Group</ha-call-service-button></div></template></paper-card></div></template></dom-module><script>Polymer({
|
||||
is: 'zwave-groups',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
groups: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
selectedTargetNode: {
|
||||
type: Number,
|
||||
value: -1
|
||||
},
|
||||
|
||||
selectedGroup: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedGroupChanged'
|
||||
},
|
||||
|
||||
otherGroupNodes: {
|
||||
type: Array,
|
||||
value: -1,
|
||||
computed: 'computeOtherGroupNodes(selectedGroup)'
|
||||
},
|
||||
|
||||
maxAssociations: {
|
||||
type: String,
|
||||
value: '',
|
||||
computed: 'computeMaxAssociations(selectedGroup)'
|
||||
},
|
||||
|
||||
noAssociationsLeft: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
computed: 'computeAssociationsLeft(selectedGroup)'
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'hass-service-called': 'serviceCalled',
|
||||
},
|
||||
|
||||
serviceCalled: function (ev) {
|
||||
if (ev.detail.success) {
|
||||
var foo = this;
|
||||
setTimeout(function () {
|
||||
foo.refreshGroups(foo.selectedNode);
|
||||
}, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
computeAssociationsLeft: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return true;
|
||||
return (this.maxAssociations === this.otherGroupNodes.length);
|
||||
},
|
||||
|
||||
computeMaxAssociations: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
if (!maxAssociations) return ['None'];
|
||||
return maxAssociations;
|
||||
},
|
||||
|
||||
computeOtherGroupNodes: function (selectedGroup) {
|
||||
if (selectedGroup === -1) return -1;
|
||||
var associations = Object.values(this.groups[selectedGroup].value.associations);
|
||||
if (!associations.length) return ['None'];
|
||||
return associations;
|
||||
},
|
||||
|
||||
computeSelectCaption: function (stateObj) {
|
||||
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
||||
stateObj.attributes.node_id + ' ' +
|
||||
stateObj.attributes.query_stage + ')';
|
||||
},
|
||||
|
||||
computeSelectCaptionGroup: function (stateObj) {
|
||||
return (stateObj.key + ': ' + stateObj.value.label);
|
||||
},
|
||||
|
||||
computeIsTargetNodeSelected: function (selectedTargetNode) {
|
||||
return (!this.nodes || selectedTargetNode === -1);
|
||||
},
|
||||
|
||||
computeIsGroupSelected: function (selectedGroup) {
|
||||
return (!this.nodes || this.selectedNode === -1 || selectedGroup === -1);
|
||||
},
|
||||
|
||||
computeAssocServiceData: function (selectedGroup, type) {
|
||||
if (!this.groups === -1 || selectedGroup === -1 || this.selectedNode === -1) return -1;
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
association: type,
|
||||
target_node_id: this.nodes[this.selectedTargetNode].attributes.node_id,
|
||||
group: this.groups[selectedGroup].key };
|
||||
},
|
||||
|
||||
refreshGroups: function (selectedNode) {
|
||||
var groupData = [];
|
||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
groupData.push({ key, value });
|
||||
});
|
||||
this.groups = groupData;
|
||||
this.selectedGroupChanged(this.selectedGroup);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
selectedGroupChanged: function (selectedGroup) {
|
||||
if (this.selectedGroup === -1 || selectedGroup === -1) return;
|
||||
this.maxAssociations = this.groups[selectedGroup].value.max_associations;
|
||||
this.otherGroupNodes = Object.values(this.groups[selectedGroup].value.associations);
|
||||
},
|
||||
});</script></div><dom-module id="ha-panel-zwave"><template><style include="iron-flex ha-style">.content{margin-top:24px}.node-info{margin-left:16px;text-transform:capitalize}.help-text{padding-left:24px;padding-right:24px}paper-card{display:block;margin:0 auto;max-width:600px}.device-picker{@apply(--layout-horizontal);@apply(--layout-center-center);padding-left:24px;padding-right:24px;padding-bottom:24px}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Z-Wave Manager</div></app-toolbar></app-header><div class="content"><zwave-network id="zwave-network" hass="[[hass]]"></zwave-network></div><div class="content"><paper-card heading="Z-Wave Node Management"><div class="card-content">Z-Wave Node controls.</div><div class="device-picker"><paper-dropdown-menu label="Nodes" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedNode}}"><template is="dom-repeat" items="[[nodes]]" as="state"><paper-item>[[computeSelectCaption(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_node" service-data="[[computeNodeServiceData(selectedNode)]]">Refresh Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="remove_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Remove Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="replace_failed_node" service-data="[[computeNodeServiceData(selectedNode)]]">Replace Failed Node</ha-call-service-button><ha-call-service-button hass="[[hass]]" domain="zwave" service="print_node" service-data="[[computeNodeServiceData(selectedNode)]]">Print Node</ha-call-service-button></div><div class="card-actions"><paper-input float-label="New node name" type="text" value="{{newNodeNameInput}}" placeholder="[[computeGetNodeName(selectedNode)]]"></paper-input><ha-call-service-button hass="[[hass]]" domain="zwave" service="rename_node" service-data="[[computeNodeNameServiceData(newNodeNameInput)]]">Rename Node</ha-call-service-button></div><div class="device-picker"><paper-dropdown-menu label="Entities of this node" class="flex"><paper-listbox class="dropdown-content" selected="{{selectedEntity}}"><template is="dom-repeat" items="[[entities]]" as="state"><paper-item>[[computeSelectCaptionEnt(state)]]</paper-item></template></paper-listbox></paper-dropdown-menu></div><template is="dom-if" if="[[!computeIsEntitySelected(selectedEntity)]]"><div class="card-actions"><ha-call-service-button hass="[[hass]]" domain="zwave" service="refresh_entity" service-data="[[computeRefreshEntityServiceData(selectedEntity)]]">Refresh Entity</ha-call-service-button></div><div class="content"><div class="card-actions"><paper-button toggles="" raised="" noink="" active="{{entityInfoActive}}">Entity Attributes</paper-button></div><template is="dom-if" if="{{entityInfoActive}}"><template is="dom-repeat" items="[[selectedEntityAttrs]]" as="state"><div class="node-info"><span>[[state]]</span></div></template></template></div></template></template></paper-card></div><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-information id="zwave-node-information" nodes="[[nodes]]" selected-node="[[selectedNode]]"></zwave-node-information></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-groups hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" groups="[[groups]]"></zwave-groups></template><template is="dom-if" if="[[!computeIsNodeSelected(selectedNode)]]"><zwave-node-config hass="[[hass]]" nodes="[[nodes]]" selected-node="[[selectedNode]]" config="[[config]]"></zwave-node-config></template><template is="dom-if" if="{{hasNodeUserCodes}}"><zwave-usercodes id="zwave-usercodes" hass="[[hass]]" nodes="[[nodes]]" user-codes="[[userCodes]]" selected-node="[[selectedNode]]"></zwave-usercodes></template><div class="content"><ozw-log id="ozw-log" hass="[[hass]]"></ozw-log></div></app-header-layout></template></dom-module><script>Polymer({
|
||||
is: 'ha-panel-zwave',
|
||||
|
||||
properties: {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
nodes: {
|
||||
type: Array,
|
||||
computed: 'computeNodes(hass)'
|
||||
},
|
||||
|
||||
selectedNode: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observer: 'selectedNodeChanged'
|
||||
},
|
||||
|
||||
config: {
|
||||
type: Array,
|
||||
value: function () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
entities: {
|
||||
type: Array,
|
||||
computed: 'computeEntities(selectedNode)',
|
||||
},
|
||||
|
||||
entityInfoActive: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
selectedEntity: {
|
||||
type: Number,
|
||||
value: -1,
|
||||
observers: ['computeIsEntitySelected',
|
||||
'computeRefreshEntityServiceData']
|
||||
},
|
||||
|
||||
selectedEntityAttrs: {
|
||||
type: Array,
|
||||
computed: 'computeSelectedEntityAttrs(selectedEntity)'
|
||||
},
|
||||
|
||||
groups: {
|
||||
type: Array,
|
||||
},
|
||||
|
||||
newNodeNameInput: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
userCodes: {
|
||||
type: Array,
|
||||
value: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
|
||||
hasNodeUserCodes: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
},
|
||||
|
||||
computeNodes: function (hass) {
|
||||
return Object.keys(hass.states)
|
||||
.map(function (key) { return hass.states[key]; })
|
||||
.filter(function (ent) {
|
||||
return (!ent.attributes.hidden &&
|
||||
(ent.entity_id).match('zwave[.]'));
|
||||
})
|
||||
.sort(function (entityA, entityB) {
|
||||
if (entityA.entity_id < entityB.entity_id) {
|
||||
return -1;
|
||||
}
|
||||
if (entityA.entity_id > entityB.entity_id) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
computeEntities: function (selectedNode) {
|
||||
if (!this.nodes || selectedNode === -1) return -1;
|
||||
var hass = this.hass;
|
||||
var nodeid = this.nodes[this.selectedNode].attributes.node_id;
|
||||
return Object.keys(hass.states)
|
||||
.map(function (key) { return hass.states[key]; })
|
||||
.filter(function (ent) {
|
||||
if (ent.attributes.node_id === undefined) {
|
||||
return false;
|
||||
}
|
||||
return (!ent.attributes.hidden &&
|
||||
'node_id' in ent.attributes &&
|
||||
ent.attributes.node_id === nodeid &&
|
||||
(!(ent.entity_id).match('zwave[.]')));
|
||||
})
|
||||
.sort(function (entityA, entityB) {
|
||||
if (entityA.entity_id < entityB.entity_id) {
|
||||
return -1;
|
||||
}
|
||||
if (entityA.entity_id > entityB.entity_id) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
},
|
||||
|
||||
selectedNodeChanged: function (selectedNode) {
|
||||
if (selectedNode === -1) return;
|
||||
this.selectedConfigParameter = -1;
|
||||
this.selectedConfigParameterValue = -1;
|
||||
this.selectedGroup = -1;
|
||||
var configData = [];
|
||||
this.hass.callApi('GET', 'zwave/config/' + this.nodes[selectedNode].attributes.node_id).then(function (configs) {
|
||||
Object.entries(configs).forEach(([key, value]) => {
|
||||
configData.push({ key, value });
|
||||
});
|
||||
this.config = configData;
|
||||
}.bind(this));
|
||||
var groupData = [];
|
||||
this.hass.callApi('GET', 'zwave/groups/' + this.nodes[selectedNode].attributes.node_id).then(function (groups) {
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
groupData.push({ key, value });
|
||||
});
|
||||
this.groups = groupData;
|
||||
}.bind(this));
|
||||
var userCodes = [];
|
||||
this.hasNodeUserCodes = false;
|
||||
this.notifyPath('hasNodeUserCodes');
|
||||
this.hass.callApi('GET', 'zwave/usercodes/' + this.nodes[selectedNode].attributes.node_id).then(function (usercodes) {
|
||||
Object.entries(usercodes).forEach(([key, value]) => {
|
||||
userCodes.push({ key, value });
|
||||
});
|
||||
this.userCodes = userCodes;
|
||||
if (Object.keys(userCodes).length === 0) {
|
||||
this.hasNodeUserCodes = false;
|
||||
} else {
|
||||
this.hasNodeUserCodes = true;
|
||||
}
|
||||
this.notifyPath('hasNodeUserCodes');
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
computeSelectedEntityAttrs: function (selectedEntity) {
|
||||
if (selectedEntity === -1) return 'No entity selected';
|
||||
var entityAttrs = this.entities[selectedEntity].attributes;
|
||||
var att = [];
|
||||
Object.entries(entityAttrs).forEach(([key, value]) => {
|
||||
att.push(key + ': ' + value);
|
||||
});
|
||||
return att.sort();
|
||||
},
|
||||
|
||||
computeSelectCaption: function (stateObj) {
|
||||
return window.hassUtil.computeStateName(stateObj) + ' (Node:' +
|
||||
stateObj.attributes.node_id + ' ' +
|
||||
stateObj.attributes.query_stage + ')';
|
||||
},
|
||||
|
||||
computeSelectCaptionEnt: function (stateObj) {
|
||||
return (window.hassUtil.computeDomain(stateObj) + '.'
|
||||
+ window.hassUtil.computeStateName(stateObj));
|
||||
},
|
||||
|
||||
computeIsNodeSelected: function () {
|
||||
return (!this.nodes || this.selectedNode === -1);
|
||||
},
|
||||
|
||||
computeIsEntitySelected: function (selectedEntity) {
|
||||
return (selectedEntity === -1);
|
||||
},
|
||||
|
||||
computeNodeServiceData: function (selectedNode) {
|
||||
return { node_id: this.nodes[selectedNode].attributes.node_id };
|
||||
},
|
||||
|
||||
computeGetNodeName: function (selectedNode) {
|
||||
if (this.selectedNode === -1 ||
|
||||
!this.nodes[selectedNode].entity_id) return -1;
|
||||
var str = (this.nodes[selectedNode].entity_id);
|
||||
var name = str.replace('zwave.', '');
|
||||
return name;
|
||||
},
|
||||
|
||||
computeNodeNameServiceData: function (newNodeNameInput) {
|
||||
return { node_id: this.nodes[this.selectedNode].attributes.node_id,
|
||||
name: newNodeNameInput };
|
||||
},
|
||||
|
||||
computeRefreshEntityServiceData: function (selectedEntity) {
|
||||
if (selectedEntity === -1) return -1;
|
||||
return { entity_id: this.entities[selectedEntity].entity_id };
|
||||
},
|
||||
});</script></body></html>
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","c255cdae4bfd691ee9fa43e38e8dc462"],["/frontend/panels/dev-event-2db9c218065ef0f61d8d08db8093cad2.html","b5b751e49b1bba55f633ae0d7a92677d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-415552027cb083badeff5f16080410ed.html","a4b1ec9bfa5bc3529af7783ae56cb55c"],["/frontend/panels/dev-state-d70314913b8923d750932367b1099750.html","c61b5b1461959aac106400e122993e9e"],["/frontend/panels/dev-template-567fbf86735e1b891e40c2f4060fec9b.html","d2853ecf45de1dbadf49fe99a7424ef3"],["/frontend/panels/map-31c592c239636f91e07c7ac232a5ebc4.html","182580419ce2c935ae6ec65502b6db96"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-5d08475f03adb5969bd31855d5ca0cfd.js","1cd99ba798bfcff9768c9d2bb2f58a7c"],["/static/frontend-5999c8fac69c503b846672cae75a12b0.html","d6ce8eb348fbea599933b2a72beb1337"],["/static/mdi-f407a5a57addbe93817ee1b244d33fbe.html","5459090f217c77747b08d06e0bf73388"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetcLine truncated
|
||||
"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","51bf4ea34a8917072df4e56498169606"],["/frontend/panels/dev-event-2db9c218065ef0f61d8d08db8093cad2.html","b5b751e49b1bba55f633ae0d7a92677d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-415552027cb083badeff5f16080410ed.html","a4b1ec9bfa5bc3529af7783ae56cb55c"],["/frontend/panels/dev-state-d70314913b8923d750932367b1099750.html","c61b5b1461959aac106400e122993e9e"],["/frontend/panels/dev-template-567fbf86735e1b891e40c2f4060fec9b.html","d2853ecf45de1dbadf49fe99a7424ef3"],["/frontend/panels/map-31c592c239636f91e07c7ac232a5ebc4.html","182580419ce2c935ae6ec65502b6db96"],["/static/compatibility-8e4c44b5f4288cc48ec1ba94a9bec812.js","4704a985ad259e324c3d8a0a40f6d937"],["/static/core-d4a7cb8c80c62b536764e0e81385f6aa.js","37e34ec6aa0fa155c7d50e2883be1ead"],["/static/frontend-fbb9d6bdd3d661db26cad9475a5e22f1.html","6b7621d45e2ad8ec8bb492bf97526b5d"],["/static/mdi-f407a5a57addbe93817ee1b244d33fbe.html","5459090f217c77747b08d06e0bf73388"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetcLine truncated
|
||||
Binary file not shown.
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.25']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.26']
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
|
||||
@@ -11,25 +11,26 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'image_processing'
|
||||
DEPENDENCIES = ['camera']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
'alpr', # automatic license plate recognition
|
||||
'face', # face
|
||||
'alpr', # Automatic license plate recognition
|
||||
'face', # Face
|
||||
'ocr', # OCR
|
||||
]
|
||||
|
||||
SERVICE_SCAN = 'scan'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Support for the demo image processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
from homeassistant.components.image_processing import ATTR_CONFIDENCE
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the demo image_processing platform."""
|
||||
"""Set up the demo image processing platform."""
|
||||
add_devices([
|
||||
DemoImageProcessingAlpr('camera.demo_camera', "Demo Alpr"),
|
||||
DemoImageProcessingFace(
|
||||
@@ -21,10 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class DemoImageProcessingAlpr(ImageProcessingAlprEntity):
|
||||
"""Demo alpr image processing entity."""
|
||||
"""Demo ALPR image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
"""Initialize demo alpr."""
|
||||
"""Initialize demo ALPR image processing entity."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
@@ -61,7 +61,7 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity):
|
||||
"""Demo face identify image processing entity."""
|
||||
|
||||
def __init__(self, camera_entity, name):
|
||||
"""Initialize demo alpr."""
|
||||
"""Initialize demo face image processing entity."""
|
||||
super().__init__()
|
||||
|
||||
self._name = name
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Component that will help set the dlib face detect processing.
|
||||
Component that will help set the Dlib face detect processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.dlib_face_detect/
|
||||
"""
|
||||
import logging
|
||||
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Microsoft Face detection platform."""
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(DlibFaceDetectEntity(
|
||||
@@ -35,7 +35,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
|
||||
"""Dlib Face API entity for identify."""
|
||||
|
||||
def __init__(self, camera_entity, name=None):
|
||||
"""Initialize Dlib."""
|
||||
"""Initialize Dlib face entity."""
|
||||
super().__init__()
|
||||
|
||||
self._camera = camera_entity
|
||||
@@ -62,7 +62,7 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity):
|
||||
import face_recognition
|
||||
|
||||
fak_file = io.BytesIO(image)
|
||||
fak_file.name = "snapshot.jpg"
|
||||
fak_file.name = 'snapshot.jpg'
|
||||
fak_file.seek(0)
|
||||
|
||||
image = face_recognition.load_image_file(fak_file)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Component that will help set the dlib face detect processing.
|
||||
Component that will help set the Dlib face detect processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.dlib_face_identify/
|
||||
"""
|
||||
import logging
|
||||
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Microsoft Face detection platform."""
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(DlibFaceIdentifyEntity(
|
||||
@@ -43,7 +43,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
|
||||
"""Dlib Face API entity for identify."""
|
||||
|
||||
def __init__(self, camera_entity, faces, name=None):
|
||||
"""Initialize Dlib."""
|
||||
"""Initialize Dlib face identify entry."""
|
||||
# pylint: disable=import-error
|
||||
import face_recognition
|
||||
super().__init__()
|
||||
@@ -77,7 +77,7 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity):
|
||||
import face_recognition
|
||||
|
||||
fak_file = io.BytesIO(image)
|
||||
fak_file.name = "snapshot.jpg"
|
||||
fak_file.name = 'snapshot.jpg'
|
||||
fak_file.seek(0)
|
||||
|
||||
image = face_recognition.load_image_file(fak_file)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Component that will help set the microsoft face detect processing.
|
||||
Component that will help set the Microsoft face detect processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.microsoft_face_detect/
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Component that will help set the microsoft face for verify processing.
|
||||
Component that will help set the Microsoft face for verify processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.microsoft_face_identify/
|
||||
@@ -62,8 +62,8 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base face identify/verify entity."""
|
||||
self.faces = [] # last scan data
|
||||
self.total_faces = 0 # face count
|
||||
self.faces = []
|
||||
self.total_faces = 0
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -71,11 +71,11 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
confidence = 0
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
# no confidence support
|
||||
# No confidence support
|
||||
if not self.confidence:
|
||||
return self.total_faces
|
||||
|
||||
# search high confidence
|
||||
# Search high confidence
|
||||
for face in self.faces:
|
||||
if ATTR_CONFIDENCE not in face:
|
||||
continue
|
||||
@@ -128,7 +128,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
# send events
|
||||
# Send events
|
||||
for face in faces:
|
||||
if ATTR_CONFIDENCE in face and self.confidence:
|
||||
if face[ATTR_CONFIDENCE] < self.confidence:
|
||||
@@ -139,7 +139,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity):
|
||||
self.hass.bus.async_fire, EVENT_DETECT_FACE, face
|
||||
)
|
||||
|
||||
# update entity store
|
||||
# Update entity store
|
||||
self.faces = faces
|
||||
self.total_faces = total
|
||||
|
||||
@@ -200,7 +200,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
|
||||
_LOGGER.error("Can't process image on Microsoft face: %s", err)
|
||||
return
|
||||
|
||||
# parse data
|
||||
# Parse data
|
||||
knwon_faces = []
|
||||
total = 0
|
||||
for face in detect:
|
||||
@@ -220,5 +220,4 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
|
||||
ATTR_CONFIDENCE: data['confidence'] * 100,
|
||||
})
|
||||
|
||||
# process data
|
||||
self.async_process_faces(knwon_faces, total)
|
||||
@@ -1,17 +1,18 @@
|
||||
"""
|
||||
Component that will help set the OpenALPR cloud for ALPR processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.openalpr_cloud/
|
||||
"""
|
||||
import asyncio
|
||||
from base64 import b64encode
|
||||
import logging
|
||||
from base64 import b64encode
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.components.image_processing import (
|
||||
@@ -19,7 +20,6 @@ from homeassistant.components.image_processing import (
|
||||
from homeassistant.components.image_processing.openalpr_local import (
|
||||
ImageProcessingAlprEntity)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,8 +44,7 @@ CONF_REGION = 'region'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_REGION):
|
||||
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
})
|
||||
|
||||
|
||||
@@ -70,7 +69,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
|
||||
class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
||||
"""OpenALPR cloud entity."""
|
||||
"""Representation of an OpenALPR cloud entity."""
|
||||
|
||||
def __init__(self, camera_entity, params, confidence, name=None):
|
||||
"""Initialize OpenALPR cloud API."""
|
||||
@@ -129,7 +128,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
|
||||
_LOGGER.error("Timeout for OpenALPR API")
|
||||
return
|
||||
|
||||
# processing api data
|
||||
# Processing API data
|
||||
vehicles = 0
|
||||
result = {}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
"""
|
||||
Component that will help set the OpenALPR local for ALPR processing.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.openalpr_local/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import io
|
||||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import split_entity_id, callback
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE,
|
||||
CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE)
|
||||
@@ -45,15 +45,13 @@ OPENALPR_REGIONS = [
|
||||
'vn2'
|
||||
]
|
||||
|
||||
|
||||
CONF_REGION = 'region'
|
||||
CONF_ALPR_BIN = 'alp_bin'
|
||||
CONF_REGION = 'region'
|
||||
|
||||
DEFAULT_BINARY = 'alpr'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_REGION):
|
||||
vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)),
|
||||
vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
})
|
||||
|
||||
@@ -77,9 +75,9 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
|
||||
"""Base entity class for ALPR image processing."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base alpr entity."""
|
||||
self.plates = {} # last scan data
|
||||
self.vehicles = 0 # vehicles count
|
||||
"""Initialize base ALPR entity."""
|
||||
self.plates = {}
|
||||
self.vehicles = 0
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@@ -128,7 +126,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
|
||||
if confidence >= self.confidence}
|
||||
new_plates = set(plates) - set(self.plates)
|
||||
|
||||
# send events
|
||||
# Send events
|
||||
for i_plate in new_plates:
|
||||
self.hass.async_add_job(
|
||||
self.hass.bus.async_fire, EVENT_FOUND_PLATE, {
|
||||
@@ -138,7 +136,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity):
|
||||
}
|
||||
)
|
||||
|
||||
# update entity store
|
||||
# Update entity store
|
||||
self.plates = plates
|
||||
self.vehicles = vehicles
|
||||
|
||||
@@ -192,7 +190,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity):
|
||||
stderr=asyncio.subprocess.DEVNULL
|
||||
)
|
||||
|
||||
# send image
|
||||
# Send image
|
||||
stdout, _ = yield from alpr.communicate(input=image)
|
||||
stdout = io.StringIO(str(stdout, 'utf-8'))
|
||||
|
||||
@@ -204,12 +202,12 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity):
|
||||
new_plates = RE_ALPR_PLATE.search(line)
|
||||
new_result = RE_ALPR_RESULT.search(line)
|
||||
|
||||
# found new vehicle
|
||||
# Found new vehicle
|
||||
if new_plates:
|
||||
vehicles += 1
|
||||
continue
|
||||
|
||||
# found plate result
|
||||
# Found plate result
|
||||
if new_result:
|
||||
try:
|
||||
result.update(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Component that performs OpenCV classification on images.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.opencv/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
@@ -9,22 +9,15 @@ import logging
|
||||
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.components.image_processing import (
|
||||
ImageProcessingEntity,
|
||||
PLATFORM_SCHEMA,
|
||||
)
|
||||
ImageProcessingEntity, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.opencv import (
|
||||
ATTR_MATCHES,
|
||||
CLASSIFIER_GROUP_CONFIG,
|
||||
CONF_CLASSIFIER,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
process_image,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ['opencv']
|
||||
ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID,
|
||||
CONF_NAME, process_image)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['opencv']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=2)
|
||||
@@ -33,18 +26,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG)
|
||||
|
||||
|
||||
def _create_processor_from_config(hass, camera_entity, config):
|
||||
"""Create an OpenCV processor from configurtaion."""
|
||||
"""Create an OpenCV processor from configuration."""
|
||||
classifier_config = config[CONF_CLASSIFIER]
|
||||
name = '{} {}'.format(
|
||||
config[CONF_NAME],
|
||||
split_entity_id(camera_entity)[1].replace('_', ' '))
|
||||
config[CONF_NAME], split_entity_id(camera_entity)[1].replace('_', ' '))
|
||||
|
||||
processor = OpenCVImageProcessor(
|
||||
hass,
|
||||
camera_entity,
|
||||
name,
|
||||
classifier_config,
|
||||
)
|
||||
hass, camera_entity, name, classifier_config)
|
||||
|
||||
return processor
|
||||
|
||||
@@ -57,10 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
devices = []
|
||||
for camera_entity in discovery_info[CONF_ENTITY_ID]:
|
||||
devices.append(
|
||||
_create_processor_from_config(
|
||||
hass,
|
||||
camera_entity,
|
||||
discovery_info))
|
||||
_create_processor_from_config(hass, camera_entity, discovery_info))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
@@ -115,6 +100,5 @@ class OpenCVImageProcessor(ImageProcessingEntity):
|
||||
def process_image(self, image):
|
||||
"""Process the image."""
|
||||
self._last_image = image
|
||||
self._matches = process_image(image,
|
||||
self._classifier_configs,
|
||||
False)
|
||||
self._matches = process_image(
|
||||
image, self._classifier_configs, False)
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Local optical character recognition processing of seven segements displays.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/image_processing.seven_segments/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import io
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA, ImageProcessingEntity, CONF_SOURCE, CONF_ENTITY_ID,
|
||||
CONF_NAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DIGITS = 'digits'
|
||||
CONF_HEIGHT = 'height'
|
||||
CONF_SSOCR_BIN = 'ssocr_bin'
|
||||
CONF_THRESHOLD = 'threshold'
|
||||
CONF_WIDTH = 'width'
|
||||
CONF_X_POS = 'x_position'
|
||||
CONF_Y_POS = 'y_position'
|
||||
|
||||
DEFAULT_BINARY = 'ssocr'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_DIGITS, default=-1): cv.positive_int,
|
||||
vol.Optional(CONF_HEIGHT, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_WIDTH, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_X_POS, default=0): cv.string,
|
||||
vol.Optional(CONF_Y_POS, default=0): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Seven segments OCR platform."""
|
||||
entities = []
|
||||
for camera in config[CONF_SOURCE]:
|
||||
entities.append(ImageProcessingSsocr(
|
||||
hass, camera[CONF_ENTITY_ID], config, camera.get(CONF_NAME)
|
||||
))
|
||||
|
||||
async_add_devices(entities)
|
||||
|
||||
|
||||
class ImageProcessingSsocr(ImageProcessingEntity):
|
||||
"""Representation of the seven segments OCR image processing entity."""
|
||||
|
||||
def __init__(self, hass, camera_entity, config, name):
|
||||
"""Initialize seven segments processing."""
|
||||
self.hass = hass
|
||||
self._camera_entity = camera_entity
|
||||
if name:
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "SevenSegement OCR {0}".format(
|
||||
split_entity_id(camera_entity)[1])
|
||||
self._state = None
|
||||
self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png')
|
||||
self._command = [
|
||||
config[CONF_SSOCR_BIN], 'erosion', 'make_mono', 'crop',
|
||||
str(config[CONF_X_POS]), str(config[CONF_Y_POS]),
|
||||
str(config[CONF_WIDTH]), str(config[CONF_HEIGHT]), '-t',
|
||||
str(config[CONF_THRESHOLD]), '-d', str(config[CONF_DIGITS]),
|
||||
self.filepath
|
||||
]
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'ocr'
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
"""Return camera entity id from process pictures."""
|
||||
return self._camera_entity
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the image processor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process the image."""
|
||||
from PIL import Image
|
||||
import subprocess
|
||||
|
||||
stream = io.BytesIO(image)
|
||||
img = Image.open(stream)
|
||||
img.save(self.filepath, 'png')
|
||||
|
||||
ocr = subprocess.Popen(
|
||||
self._command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out = ocr.communicate()
|
||||
if out[0] != b'':
|
||||
self._state = out[0].strip().decode('utf-8')
|
||||
else:
|
||||
self._state = None
|
||||
_LOGGER.warning(
|
||||
"Unable to detect value: %s", out[1].strip().decode('utf-8'))
|
||||
@@ -0,0 +1,142 @@
|
||||
"""KIRA interface to receive UDP packets from an IR-IP bridge."""
|
||||
# pylint: disable=import-error
|
||||
import logging
|
||||
import os
|
||||
import yaml
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.error import Error as VoluptuousError
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SENSORS,
|
||||
CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_UNKNOWN)
|
||||
|
||||
REQUIREMENTS = ["pykira==0.1.1"]
|
||||
|
||||
DOMAIN = 'kira'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 65432
|
||||
|
||||
CONF_CODE = "code"
|
||||
CONF_REPEAT = "repeat"
|
||||
CONF_REMOTES = "remotes"
|
||||
CONF_SENSOR = "sensor"
|
||||
CONF_REMOTE = "remote"
|
||||
|
||||
CODES_YAML = '{}_codes.yaml'.format(DOMAIN)
|
||||
|
||||
CODE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_TYPE): cv.string,
|
||||
vol.Optional(CONF_DEVICE): cv.string,
|
||||
vol.Optional(CONF_REPEAT): cv.positive_int,
|
||||
})
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DOMAIN):
|
||||
vol.Exclusive(cv.string, "sensors"),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
REMOTE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=DOMAIN):
|
||||
vol.Exclusive(cv.string, "remotes"),
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA],
|
||||
vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def load_codes(path):
|
||||
"""Load Kira codes from specified file."""
|
||||
codes = []
|
||||
if os.path.exists(path):
|
||||
with open(path) as code_file:
|
||||
data = yaml.load(code_file) or []
|
||||
for code in data:
|
||||
try:
|
||||
codes.append(CODE_SCHEMA(code))
|
||||
except VoluptuousError as exception:
|
||||
# keep going
|
||||
_LOGGER.warning('Kira Code Invalid Data: %s', exception)
|
||||
else:
|
||||
with open(path, 'w') as code_file:
|
||||
code_file.write('')
|
||||
return codes
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup KIRA capability."""
|
||||
import pykira
|
||||
|
||||
sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, [])
|
||||
remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, [])
|
||||
# If no sensors or remotes were specified, add a sensor
|
||||
if not(sensors or remotes):
|
||||
sensors.append({})
|
||||
|
||||
codes = load_codes(hass.config.path(CODES_YAML))
|
||||
|
||||
hass.data[DOMAIN] = {
|
||||
CONF_SENSOR: {},
|
||||
CONF_REMOTE: {},
|
||||
}
|
||||
|
||||
def load_module(platform, idx, module_conf):
|
||||
"""Set up Kira module and load platform."""
|
||||
# note: module_name is not the HA device name. it's just a unique name
|
||||
# to ensure the component and platform can share information
|
||||
module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN
|
||||
device_name = module_conf.get(CONF_NAME, DOMAIN)
|
||||
port = module_conf.get(CONF_PORT, DEFAULT_PORT)
|
||||
host = module_conf.get(CONF_HOST, DEFAULT_HOST)
|
||||
|
||||
if platform == CONF_SENSOR:
|
||||
module = pykira.KiraReceiver(host, port)
|
||||
module.start()
|
||||
else:
|
||||
module = pykira.KiraModule(host, port)
|
||||
|
||||
hass.data[DOMAIN][platform][module_name] = module
|
||||
for code in codes:
|
||||
code_tuple = (code.get(CONF_NAME),
|
||||
code.get(CONF_DEVICE, STATE_UNKNOWN))
|
||||
module.registerCode(code_tuple, code.get(CONF_CODE))
|
||||
|
||||
discovery.load_platform(hass, platform, DOMAIN,
|
||||
{'name': module_name, 'device': device_name},
|
||||
config)
|
||||
|
||||
for idx, module_conf in enumerate(sensors):
|
||||
load_module(CONF_SENSOR, idx, module_conf)
|
||||
|
||||
for idx, module_conf in enumerate(remotes):
|
||||
load_module(CONF_REMOTE, idx, module_conf)
|
||||
|
||||
def _stop_kira(_event):
|
||||
for receiver in hass.data[DOMAIN][CONF_SENSOR].values():
|
||||
receiver.stop()
|
||||
_LOGGER.info("Terminated receivers")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira)
|
||||
|
||||
return True
|
||||
@@ -50,13 +50,15 @@ ATTR_TRANSITION = "transition"
|
||||
ATTR_RGB_COLOR = "rgb_color"
|
||||
ATTR_XY_COLOR = "xy_color"
|
||||
ATTR_COLOR_TEMP = "color_temp"
|
||||
ATTR_KELVIN = "kelvin"
|
||||
ATTR_MIN_MIREDS = "min_mireds"
|
||||
ATTR_MAX_MIREDS = "max_mireds"
|
||||
ATTR_COLOR_NAME = "color_name"
|
||||
ATTR_WHITE_VALUE = "white_value"
|
||||
|
||||
# int with value 0 .. 255 representing brightness of the light.
|
||||
# Brightness of the light, 0..255 or percentage
|
||||
ATTR_BRIGHTNESS = "brightness"
|
||||
ATTR_BRIGHTNESS_PCT = "brightness_pct"
|
||||
|
||||
# String representing a profile (built-in ones or external defined).
|
||||
ATTR_PROFILE = "profile"
|
||||
@@ -92,18 +94,21 @@ PROP_TO_ATTR = {
|
||||
# Service call validation schemas
|
||||
VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
|
||||
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
|
||||
VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
|
||||
|
||||
LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
ATTR_PROFILE: cv.string,
|
||||
ATTR_TRANSITION: VALID_TRANSITION,
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME: cv.string,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||
ATTR_EFFECT: cv.string,
|
||||
@@ -142,20 +147,21 @@ def is_on(hass, entity_id=None):
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
rgb_color=None, xy_color=None, color_temp=None, white_value=None,
|
||||
brightness_pct=None, rgb_color=None, xy_color=None,
|
||||
color_temp=None, kelvin=None, white_value=None,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
hass.add_job(
|
||||
async_turn_on, hass, entity_id, transition, brightness,
|
||||
rgb_color, xy_color, color_temp, white_value,
|
||||
async_turn_on, hass, entity_id, transition, brightness, brightness_pct,
|
||||
rgb_color, xy_color, color_temp, kelvin, white_value,
|
||||
profile, flash, effect, color_name)
|
||||
|
||||
|
||||
@callback
|
||||
def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
rgb_color=None, xy_color=None, color_temp=None,
|
||||
white_value=None, profile=None, flash=None, effect=None,
|
||||
color_name=None):
|
||||
brightness_pct=None, rgb_color=None, xy_color=None,
|
||||
color_temp=None, kelvin=None, white_value=None,
|
||||
profile=None, flash=None, effect=None, color_name=None):
|
||||
"""Turn all or specified light on."""
|
||||
data = {
|
||||
key: value for key, value in [
|
||||
@@ -163,9 +169,11 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||
(ATTR_PROFILE, profile),
|
||||
(ATTR_TRANSITION, transition),
|
||||
(ATTR_BRIGHTNESS, brightness),
|
||||
(ATTR_BRIGHTNESS_PCT, brightness_pct),
|
||||
(ATTR_RGB_COLOR, rgb_color),
|
||||
(ATTR_XY_COLOR, xy_color),
|
||||
(ATTR_COLOR_TEMP, color_temp),
|
||||
(ATTR_KELVIN, kelvin),
|
||||
(ATTR_WHITE_VALUE, white_value),
|
||||
(ATTR_FLASH, flash),
|
||||
(ATTR_EFFECT, effect),
|
||||
@@ -207,6 +215,27 @@ def toggle(hass, entity_id=None, transition=None):
|
||||
hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
|
||||
|
||||
|
||||
def preprocess_turn_on_alternatives(params):
|
||||
"""Processing extra data for turn light on request."""
|
||||
profile = Profiles.get(params.pop(ATTR_PROFILE, None))
|
||||
if profile is not None:
|
||||
params.setdefault(ATTR_XY_COLOR, profile[:2])
|
||||
params.setdefault(ATTR_BRIGHTNESS, profile[2])
|
||||
|
||||
color_name = params.pop(ATTR_COLOR_NAME, None)
|
||||
if color_name is not None:
|
||||
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
|
||||
kelvin = params.pop(ATTR_KELVIN, None)
|
||||
if kelvin is not None:
|
||||
mired = color_util.color_temperature_kelvin_to_mired(kelvin)
|
||||
params[ATTR_COLOR_TEMP] = int(mired)
|
||||
|
||||
brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None)
|
||||
if brightness_pct is not None:
|
||||
params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Expose light control via statemachine and services."""
|
||||
@@ -215,10 +244,8 @@ def async_setup(hass, config):
|
||||
yield from component.async_setup(config)
|
||||
|
||||
# load profiles from files
|
||||
profiles = yield from hass.loop.run_in_executor(
|
||||
None, _load_profile_data, hass)
|
||||
|
||||
if profiles is None:
|
||||
profiles_valid = yield from Profiles.load_profiles(hass)
|
||||
if not profiles_valid:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -231,17 +258,7 @@ def async_setup(hass, config):
|
||||
target_lights = component.async_extract_from_service(service)
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
# Processing extra data for turn light on request.
|
||||
profile = profiles.get(params.pop(ATTR_PROFILE, None))
|
||||
|
||||
if profile:
|
||||
params.setdefault(ATTR_XY_COLOR, profile[:2])
|
||||
params.setdefault(ATTR_BRIGHTNESS, profile[2])
|
||||
|
||||
color_name = params.pop(ATTR_COLOR_NAME, None)
|
||||
|
||||
if color_name is not None:
|
||||
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
preprocess_turn_on_alternatives(params)
|
||||
|
||||
for light in target_lights:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
@@ -287,31 +304,51 @@ def async_setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _load_profile_data(hass):
|
||||
"""Load built-in profiles and custom profiles."""
|
||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
||||
LIGHT_PROFILES_FILE),
|
||||
hass.config.path(LIGHT_PROFILES_FILE)]
|
||||
profiles = {}
|
||||
class Profiles:
|
||||
"""Representation of available color profiles."""
|
||||
|
||||
for profile_path in profile_paths:
|
||||
if not os.path.isfile(profile_path):
|
||||
continue
|
||||
with open(profile_path) as inp:
|
||||
reader = csv.reader(inp)
|
||||
_all = None
|
||||
|
||||
# Skip the header
|
||||
next(reader, None)
|
||||
@classmethod
|
||||
@asyncio.coroutine
|
||||
def load_profiles(cls, hass):
|
||||
"""Load and cache profiles."""
|
||||
def load_profile_data(hass):
|
||||
"""Load built-in profiles and custom profiles."""
|
||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
||||
LIGHT_PROFILES_FILE),
|
||||
hass.config.path(LIGHT_PROFILES_FILE)]
|
||||
profiles = {}
|
||||
|
||||
try:
|
||||
for rec in reader:
|
||||
profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec)
|
||||
profiles[profile] = (color_x, color_y, brightness)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error("Error parsing light profile from %s: %s",
|
||||
profile_path, ex)
|
||||
return None
|
||||
return profiles
|
||||
for profile_path in profile_paths:
|
||||
if not os.path.isfile(profile_path):
|
||||
continue
|
||||
with open(profile_path) as inp:
|
||||
reader = csv.reader(inp)
|
||||
|
||||
# Skip the header
|
||||
next(reader, None)
|
||||
|
||||
try:
|
||||
for rec in reader:
|
||||
profile, color_x, color_y, brightness = \
|
||||
PROFILE_SCHEMA(rec)
|
||||
profiles[profile] = (color_x, color_y, brightness)
|
||||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error(
|
||||
"Error parsing light profile from %s: %s",
|
||||
profile_path, ex)
|
||||
return None
|
||||
return profiles
|
||||
|
||||
cls._all = yield from hass.loop.run_in_executor(
|
||||
None, load_profile_data, hass)
|
||||
return cls._all is not None
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
"""Return a named profile."""
|
||||
return cls._all.get(name)
|
||||
|
||||
|
||||
class Light(ToggleEntity):
|
||||
|
||||
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Add device specified by serial number."""
|
||||
"""Set up Blinkstick device specified by serial number."""
|
||||
from blinkstick import blinkstick
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
@@ -29,6 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Blinkt Light platform."""
|
||||
# pylint: disable=import-error
|
||||
import blinkt
|
||||
|
||||
# ensure that the lights are off when exiting
|
||||
|
||||
@@ -20,14 +20,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_SENDER_ID = 'sender_id'
|
||||
|
||||
DEFAULT_NAME = 'EnOcean Light'
|
||||
|
||||
DEPENDENCIES = ['enocean']
|
||||
|
||||
SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list,
|
||||
[vol.Coerce(int)]),
|
||||
vol.Optional(CONF_ID, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
@@ -35,26 +35,26 @@ SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT |
|
||||
MODE_RGB = 'rgb'
|
||||
MODE_RGBW = 'rgbw'
|
||||
|
||||
# List of Supported Effects which aren't already declared in LIGHT
|
||||
EFFECT_RED_FADE = "red_fade"
|
||||
EFFECT_GREEN_FADE = "green_fade"
|
||||
EFFECT_BLUE_FADE = "blue_fade"
|
||||
EFFECT_YELLOW_FADE = "yellow_fade"
|
||||
EFFECT_CYAN_FADE = "cyan_fade"
|
||||
EFFECT_PURPLE_FADE = "purple_fade"
|
||||
EFFECT_WHITE_FADE = "white_fade"
|
||||
EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade"
|
||||
EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade"
|
||||
EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade"
|
||||
EFFECT_COLORSTROBE = "colorstrobe"
|
||||
EFFECT_RED_STROBE = "red_strobe"
|
||||
EFFECT_GREEN_STROBE = "green_strobe"
|
||||
EFFECT_BLUE_STOBE = "blue_strobe"
|
||||
EFFECT_YELLOW_STROBE = "yellow_strobe"
|
||||
EFFECT_CYAN_STROBE = "cyan_strobe"
|
||||
EFFECT_PURPLE_STROBE = "purple_strobe"
|
||||
EFFECT_WHITE_STROBE = "white_strobe"
|
||||
EFFECT_COLORJUMP = "colorjump"
|
||||
# List of supported effects which aren't already declared in LIGHT
|
||||
EFFECT_RED_FADE = 'red_fade'
|
||||
EFFECT_GREEN_FADE = 'green_fade'
|
||||
EFFECT_BLUE_FADE = 'blue_fade'
|
||||
EFFECT_YELLOW_FADE = 'yellow_fade'
|
||||
EFFECT_CYAN_FADE = 'cyan_fade'
|
||||
EFFECT_PURPLE_FADE = 'purple_fade'
|
||||
EFFECT_WHITE_FADE = 'white_fade'
|
||||
EFFECT_RED_GREEN_CROSS_FADE = 'rg_cross_fade'
|
||||
EFFECT_RED_BLUE_CROSS_FADE = 'rb_cross_fade'
|
||||
EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade'
|
||||
EFFECT_COLORSTROBE = 'colorstrobe'
|
||||
EFFECT_RED_STROBE = 'red_strobe'
|
||||
EFFECT_GREEN_STROBE = 'green_strobe'
|
||||
EFFECT_BLUE_STOBE = 'blue_strobe'
|
||||
EFFECT_YELLOW_STROBE = 'yellow_strobe'
|
||||
EFFECT_CYAN_STROBE = 'cyan_strobe'
|
||||
EFFECT_PURPLE_STROBE = 'purple_strobe'
|
||||
EFFECT_WHITE_STROBE = 'white_strobe'
|
||||
EFFECT_COLORJUMP = 'colorjump'
|
||||
|
||||
FLUX_EFFECT_LIST = [
|
||||
EFFECT_COLORLOOP,
|
||||
@@ -121,7 +121,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
ipaddr = device['ipaddr']
|
||||
if ipaddr in light_ips:
|
||||
continue
|
||||
device['name'] = device['id'] + " " + ipaddr
|
||||
device['name'] = '{} {}'.format(device['id'], ipaddr)
|
||||
device[ATTR_MODE] = 'rgbw'
|
||||
device[CONF_PROTOCOL] = None
|
||||
light = FluxLight(device)
|
||||
@@ -167,7 +167,7 @@ class FluxLight(Light):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this light."""
|
||||
return "{}.{}".format(self.__class__, self._ipaddr)
|
||||
return '{}.{}'.format(self.__class__, self._ipaddr)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -84,7 +84,7 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback):
|
||||
request_id = _CONFIGURING.pop(device_id)
|
||||
configurator = get_component('configurator')
|
||||
configurator.request_done(request_id)
|
||||
_LOGGER.info("Device configuration done!")
|
||||
_LOGGER.debug("Device configuration done")
|
||||
|
||||
conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
|
||||
if device_id not in conf_lights:
|
||||
@@ -107,7 +107,7 @@ def config_from_file(filename, config=None):
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
except IOError as error:
|
||||
_LOGGER.error('Saving config file failed: %s', error)
|
||||
_LOGGER.error("Saving config file failed: %s", error)
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Support for INSTEON lights via PowerLinc Modem.
|
||||
Support for Insteon lights via PowerLinc Modem.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_plm/
|
||||
@@ -12,16 +12,16 @@ from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['insteon_plm']
|
||||
|
||||
MAX_BRIGHTNESS = 255
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
"""Set up the Insteon PLM device."""
|
||||
plm = hass.data['insteon_plm']
|
||||
|
||||
device_list = []
|
||||
@@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
address = device.get('address_hex')
|
||||
dimmable = bool('dimmable' in device.get('capabilities'))
|
||||
|
||||
_LOGGER.info('Registered %s with light platform.', name)
|
||||
_LOGGER.info("Registered %s with light platform", name)
|
||||
|
||||
device_list.append(
|
||||
InsteonPLMDimmerDevice(hass, plm, address, name, dimmable)
|
||||
@@ -72,14 +72,14 @@ class InsteonPLMDimmerDevice(Light):
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
|
||||
_LOGGER.debug('on level for %s is %s', self._address, onlevel)
|
||||
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
|
||||
return int(onlevel)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
onlevel = self._plm.get_device_attr(self._address, 'onlevel')
|
||||
_LOGGER.debug('on level for %s is %s', self._address, onlevel)
|
||||
_LOGGER.debug("on level for %s is %s", self._address, onlevel)
|
||||
return bool(onlevel)
|
||||
|
||||
@property
|
||||
@@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light):
|
||||
@callback
|
||||
def async_light_update(self, message):
|
||||
"""Receive notification from transport that new data exists."""
|
||||
_LOGGER.info('Received update calback from PLM for %s', self._address)
|
||||
_LOGGER.info("Received update calback from PLM for %s", self._address)
|
||||
self._hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -24,13 +24,12 @@ def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 light platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error('A connection has not been made to the ISY controller.')
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM,
|
||||
states=STATES):
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
|
||||
if node.dimmable or '51' in node.uom:
|
||||
devices.append(ISYLightDevice(node))
|
||||
|
||||
@@ -57,12 +56,12 @@ class ISYLightDevice(isy.ISYDevice, Light):
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 light device."""
|
||||
if not self._node.off():
|
||||
_LOGGER.debug('Unable to turn on light.')
|
||||
_LOGGER.debug("Unable to turn on light")
|
||||
|
||||
def turn_on(self, brightness=None, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 light device."""
|
||||
if not self._node.on(val=brightness):
|
||||
_LOGGER.debug('Unable to turn on light.')
|
||||
_LOGGER.debug("Unable to turn on light")
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
||||
@@ -9,6 +9,7 @@ import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import math
|
||||
from os import path
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
import async_timeout
|
||||
@@ -16,15 +17,19 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR,
|
||||
Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA,
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT)
|
||||
SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT,
|
||||
preprocess_turn_on_alternatives)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||
from homeassistant import util
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
@@ -41,7 +46,10 @@ BULB_LATENCY = 500
|
||||
|
||||
CONF_SERVER = 'server'
|
||||
|
||||
SERVICE_LIFX_SET_STATE = 'lifx_set_state'
|
||||
|
||||
ATTR_HSBK = 'hsbk'
|
||||
ATTR_POWER = 'power'
|
||||
|
||||
BYTE_MAX = 255
|
||||
SHORT_MAX = 65535
|
||||
@@ -53,6 +61,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||
})
|
||||
|
||||
LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({
|
||||
ATTR_POWER: cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
@@ -87,6 +99,41 @@ class LIFXManager(object):
|
||||
self.hass = hass
|
||||
self.async_add_devices = async_add_devices
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Apply a service."""
|
||||
tasks = []
|
||||
for light in self.service_to_entities(service):
|
||||
if service.service == SERVICE_LIFX_SET_STATE:
|
||||
task = light.async_set_state(**service.data)
|
||||
tasks.append(hass.async_add_job(task))
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = self.get_descriptions()
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle,
|
||||
descriptions.get(SERVICE_LIFX_SET_STATE),
|
||||
schema=LIFX_SET_STATE_SCHEMA)
|
||||
|
||||
@staticmethod
|
||||
def get_descriptions():
|
||||
"""Load and return descriptions for our own service calls."""
|
||||
return load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def service_to_entities(self, service):
|
||||
"""Return the known devices that a service call mentions."""
|
||||
entity_ids = extract_entity_ids(self.hass, service)
|
||||
if entity_ids:
|
||||
entities = [entity for entity in self.entities.values()
|
||||
if entity.entity_id in entity_ids]
|
||||
else:
|
||||
entities = list(self.entities.values())
|
||||
|
||||
return entities
|
||||
|
||||
@callback
|
||||
def register(self, device):
|
||||
"""Handle for newly detected bulb."""
|
||||
@@ -298,6 +345,18 @@ class LIFXLight(Light):
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
kwargs[ATTR_POWER] = True
|
||||
yield from self.async_set_state(**kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
kwargs[ATTR_POWER] = False
|
||||
yield from self.async_set_state(**kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_state(self, **kwargs):
|
||||
"""Set a color on the light and turn it on/off."""
|
||||
yield from self.stop_effect()
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
@@ -309,39 +368,41 @@ class LIFXLight(Light):
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
# These are both False if ATTR_POWER is not set
|
||||
power_on = kwargs.get(ATTR_POWER, False)
|
||||
power_off = not kwargs.get(ATTR_POWER, True)
|
||||
|
||||
hsbk, changed_color = self.find_hsbk(**kwargs)
|
||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||
self.who, self._power, fade, *hsbk)
|
||||
|
||||
if self._power == 0:
|
||||
if power_off:
|
||||
self.device.set_power(False, None, 0)
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, 0)
|
||||
self.device.set_power(True, None, fade)
|
||||
if power_on:
|
||||
self.device.set_power(True, None, fade)
|
||||
else:
|
||||
self.device.set_power(True, None, 0) # racing for power status
|
||||
if power_on:
|
||||
self.device.set_power(True, None, 0)
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, fade)
|
||||
if power_off:
|
||||
self.device.set_power(False, None, fade)
|
||||
|
||||
self.update_later(0)
|
||||
if fade < BULB_LATENCY:
|
||||
self.set_power(1)
|
||||
self.set_color(*hsbk)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
yield from self.stop_effect()
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
if power_on:
|
||||
self.update_later(0)
|
||||
else:
|
||||
fade = 0
|
||||
self.update_later(fade)
|
||||
|
||||
self.device.set_power(False, None, fade)
|
||||
|
||||
self.update_later(fade)
|
||||
if fade < BULB_LATENCY:
|
||||
self.set_power(0)
|
||||
if fade <= BULB_LATENCY:
|
||||
if power_on:
|
||||
self.set_power(1)
|
||||
if power_off:
|
||||
self.set_power(0)
|
||||
if changed_color:
|
||||
self.set_color(*hsbk)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
@@ -374,9 +435,7 @@ class LIFXLight(Light):
|
||||
if hsbk is not None:
|
||||
return [hsbk, True]
|
||||
|
||||
color_name = kwargs.pop(ATTR_COLOR_NAME, None)
|
||||
if color_name is not None:
|
||||
kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
|
||||
preprocess_turn_on_alternatives(kwargs)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import random
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT,
|
||||
ATTR_TRANSITION)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME,
|
||||
ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION,
|
||||
VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT)
|
||||
from homeassistant.const import (ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.service import extract_entity_ids
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -39,7 +37,8 @@ LIFX_EFFECT_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME: cv.string,
|
||||
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
|
||||
vol.Coerce(tuple)),
|
||||
@@ -52,7 +51,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA
|
||||
|
||||
LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({
|
||||
ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)),
|
||||
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT,
|
||||
vol.Optional(ATTR_PERIOD, default=60):
|
||||
vol.All(vol.Coerce(float), vol.Clamp(min=0.05)),
|
||||
vol.Optional(ATTR_CHANGE, default=20):
|
||||
@@ -73,19 +73,12 @@ def setup(hass, lifx_manager):
|
||||
@asyncio.coroutine
|
||||
def async_service_handle(service):
|
||||
"""Apply a service."""
|
||||
entity_ids = extract_entity_ids(hass, service)
|
||||
if entity_ids:
|
||||
devices = [entity for entity in lifx_manager.entities.values()
|
||||
if entity.entity_id in entity_ids]
|
||||
else:
|
||||
devices = list(lifx_manager.entities.values())
|
||||
|
||||
if devices:
|
||||
yield from start_effect(hass, devices,
|
||||
entities = lifx_manager.service_to_entities(service)
|
||||
if entities:
|
||||
yield from start_effect(hass, entities,
|
||||
service.service, **service.data)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = lifx_manager.get_descriptions()
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle,
|
||||
@@ -292,7 +285,7 @@ class LIFXEffectColorloop(LIFXEffect):
|
||||
direction = 1 if random.randint(0, 1) else -1
|
||||
|
||||
# Random start
|
||||
hue = random.randint(0, 359)
|
||||
hue = random.uniform(0, 360) % 360
|
||||
|
||||
while self.lights:
|
||||
hue = (hue + direction*change) % 360
|
||||
@@ -312,7 +305,7 @@ class LIFXEffectColorloop(LIFXEffect):
|
||||
brightness = light.effect_data.color[2]
|
||||
|
||||
hsbk = [
|
||||
int(65535/359*lhue),
|
||||
int(65535/360*lhue),
|
||||
int(random.uniform(0.8, 1.0)*65535),
|
||||
brightness,
|
||||
NEUTRAL_WHITE,
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
lifx_set_state:
|
||||
description: Set a color/brightness and possibliy turn the light on/off
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to set a state on
|
||||
example: 'light.garage'
|
||||
|
||||
'...':
|
||||
description: All turn_on parameters can be used to specify a color
|
||||
|
||||
transition:
|
||||
description: Duration in seconds it takes to get to the final state
|
||||
example: 10
|
||||
|
||||
power:
|
||||
description: Turn the light on (True) or off (False). Leave out to keep the power as it is.
|
||||
example: True
|
||||
|
||||
|
||||
lifx_effect_breathe:
|
||||
description: Run a breathe effect by fading to a color and back.
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Support for LimitlessLED bulbs.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.limitlessled/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -17,7 +16,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['limitlessled==1.0.5']
|
||||
REQUIREMENTS = ['limitlessled==1.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ class Luminary(Light):
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
|
||||
self._name, self._brightness)
|
||||
self._brightness = self._luminary.set_luminance(
|
||||
self._luminary.set_luminance(
|
||||
int(self._brightness / 2.55),
|
||||
transition)
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ turn_on:
|
||||
|
||||
color_temp:
|
||||
description: Color temperature for the light in mireds
|
||||
example: '250'
|
||||
example: 250
|
||||
|
||||
kelvin:
|
||||
description: Color temperature for the light in Kelvin
|
||||
example: 4000
|
||||
|
||||
white_value:
|
||||
description: Number between 0..255 indicating level of white
|
||||
@@ -36,6 +40,10 @@ turn_on:
|
||||
description: Number between 0..255 indicating brightness
|
||||
example: 120
|
||||
|
||||
brightness_pct:
|
||||
description: Number between 0..100 indicating percentage of full brightness
|
||||
example: 47
|
||||
|
||||
profile:
|
||||
description: Name of a light profile to use
|
||||
example: relax
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""Support for the IKEA Tradfri platform."""
|
||||
"""
|
||||
Support for the IKEA Tradfri platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.tradfri/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@@ -6,7 +11,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
|
||||
from homeassistant.components.light import \
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
|
||||
from homeassistant.components.tradfri import KEY_GATEWAY
|
||||
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -30,8 +35,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
lights = [dev for dev in devices if dev.has_light_control]
|
||||
add_devices(Tradfri(light) for light in lights)
|
||||
|
||||
groups = gateway.get_groups()
|
||||
add_devices(TradfriGroup(group) for group in groups)
|
||||
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
|
||||
if allow_tradfri_groups:
|
||||
groups = gateway.get_groups()
|
||||
add_devices(TradfriGroup(group) for group in groups)
|
||||
|
||||
|
||||
class TradfriGroup(Light):
|
||||
|
||||
@@ -4,6 +4,7 @@ Support for Wink lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import colorsys
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@@ -38,6 +39,11 @@ class WinkLight(WinkDevice, Light):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['light'].append(self)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
|
||||
@@ -55,3 +55,59 @@ unlock:
|
||||
code:
|
||||
description: An optional code to unlock the lock with
|
||||
example: 1234
|
||||
|
||||
wink_set_lock_vacation_mode:
|
||||
description: Set vacation mode for all or specified locks. Disables all user codes.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
enabled:
|
||||
description: enable or disable. true or false.
|
||||
example: true
|
||||
|
||||
wink_set_lock_alarm_mode:
|
||||
description: Set alarm mode for all or specified locks.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
mode:
|
||||
description: One of tamper, activity, or forced_entry
|
||||
example: tamper
|
||||
|
||||
wink_set_lock_alarm_sensitivity:
|
||||
description: Set alarm sensitivity for all or specified locks.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
sensitivity:
|
||||
description: One of low, medium_low, medium, medium_high, high
|
||||
example: medium
|
||||
|
||||
wink_set_lock_alarm_state:
|
||||
description: Set alarm state.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
enabled:
|
||||
description: enable or disable. true or false.
|
||||
example: true
|
||||
|
||||
wink_set_lock_beeper_state:
|
||||
description: Set beeper state.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of lock to unlock
|
||||
example: 'lock.front_door'
|
||||
enabled:
|
||||
description: enable or disable. true or false.
|
||||
example: true
|
||||
|
||||
@@ -4,11 +4,55 @@ Support for Wink locks.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.wink/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from os import path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lock import LockDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SET_VACATION_MODE = 'wink_set_lock_vacation_mode'
|
||||
SERVICE_SET_ALARM_MODE = 'wink_set_lock_alarm_mode'
|
||||
SERVICE_SET_ALARM_SENSITIVITY = 'wink_set_lock_alarm_sensitivity'
|
||||
SERVICE_SET_ALARM_STATE = 'wink_set_lock_alarm_state'
|
||||
SERVICE_SET_BEEPER_STATE = 'wink_set_lock_beeper_state'
|
||||
|
||||
ATTR_ENABLED = 'enabled'
|
||||
ATTR_SENSITIVITY = 'sensitivity'
|
||||
ATTR_MODE = 'mode'
|
||||
|
||||
ALARM_SENSITIVITY_MAP = {"low": 0.2, "medium_low": 0.4,
|
||||
"medium": 0.6, "medium_high": 0.8,
|
||||
"high": 1.0}
|
||||
|
||||
ALARM_MODES_MAP = {"tamper": "tamper",
|
||||
"activity": "alert",
|
||||
"forced_entry": "forced_entry"}
|
||||
|
||||
SET_ENABLED_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_ENABLED): cv.string,
|
||||
})
|
||||
|
||||
SET_SENSITIVITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_SENSITIVITY): vol.In(ALARM_SENSITIVITY_MAP)
|
||||
})
|
||||
|
||||
SET_ALARM_MODES_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP)
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Wink platform."""
|
||||
@@ -19,6 +63,58 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkLockDevice(lock, hass)])
|
||||
|
||||
def service_handle(service):
|
||||
"""Handler for services."""
|
||||
entity_ids = service.data.get('entity_id')
|
||||
all_locks = hass.data[DOMAIN]['entities']['lock']
|
||||
locks_to_set = []
|
||||
if entity_ids is None:
|
||||
locks_to_set = all_locks
|
||||
else:
|
||||
for lock in all_locks:
|
||||
if lock.entity_id in entity_ids:
|
||||
locks_to_set.append(lock)
|
||||
|
||||
for lock in locks_to_set:
|
||||
if service.service == SERVICE_SET_VACATION_MODE:
|
||||
lock.set_vacation_mode(service.data.get(ATTR_ENABLED))
|
||||
elif service.service == SERVICE_SET_ALARM_STATE:
|
||||
lock.set_alarm_state(service.data.get(ATTR_ENABLED))
|
||||
elif service.service == SERVICE_SET_BEEPER_STATE:
|
||||
lock.set_beeper_state(service.data.get(ATTR_ENABLED))
|
||||
elif service.service == SERVICE_SET_ALARM_MODE:
|
||||
lock.set_alarm_mode(service.data.get(ATTR_MODE))
|
||||
elif service.service == SERVICE_SET_ALARM_SENSITIVITY:
|
||||
lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY))
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_VACATION_MODE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_VACATION_MODE),
|
||||
schema=SET_ENABLED_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_ALARM_STATE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_ALARM_STATE),
|
||||
schema=SET_ENABLED_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_BEEPER_STATE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_BEEPER_STATE),
|
||||
schema=SET_ENABLED_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_ALARM_MODE,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_ALARM_MODE),
|
||||
schema=SET_ALARM_MODES_SCHEMA)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_ALARM_SENSITIVITY,
|
||||
service_handle,
|
||||
descriptions.get(SERVICE_SET_ALARM_SENSITIVITY),
|
||||
schema=SET_SENSITIVITY_SCHEMA)
|
||||
|
||||
|
||||
class WinkLockDevice(WinkDevice, LockDevice):
|
||||
"""Representation of a Wink lock."""
|
||||
@@ -27,6 +123,11 @@ class WinkLockDevice(WinkDevice, LockDevice):
|
||||
"""Initialize the lock."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Callback when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['lock'].append(self)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is locked."""
|
||||
@@ -39,3 +140,60 @@ class WinkLockDevice(WinkDevice, LockDevice):
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self.wink.set_state(False)
|
||||
|
||||
def set_alarm_state(self, enabled):
|
||||
"""Set lock's alarm state."""
|
||||
self.wink.set_alarm_state(enabled)
|
||||
|
||||
def set_vacation_mode(self, enabled):
|
||||
"""Set lock's vacation mode."""
|
||||
self.wink.set_vacation_mode(enabled)
|
||||
|
||||
def set_beeper_state(self, enabled):
|
||||
"""Set lock's beeper mode."""
|
||||
self.wink.set_beeper_mode(enabled)
|
||||
|
||||
def set_alarm_sensitivity(self, sensitivity):
|
||||
"""
|
||||
Set lock's alarm sensitivity.
|
||||
|
||||
Valid sensitivities:
|
||||
0.2, 0.4, 0.6, 0.8, 1.0
|
||||
"""
|
||||
self.wink.set_alarm_sensitivity(sensitivity)
|
||||
|
||||
def set_alarm_mode(self, mode):
|
||||
"""
|
||||
Set lock's alarm mode.
|
||||
|
||||
Valid modes:
|
||||
alert - Beep when lock is locked or unlocked
|
||||
tamper - 15 sec alarm when lock is disturbed when locked
|
||||
forced_entry - 3 min alarm when significant force applied
|
||||
to door when locked.
|
||||
"""
|
||||
self.wink.set_alarm_mode(mode)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
super_attrs = super().device_state_attributes
|
||||
sensitivity = dict_value_to_key(ALARM_SENSITIVITY_MAP,
|
||||
self.wink.alarm_sensitivity())
|
||||
super_attrs['alarm sensitivity'] = sensitivity
|
||||
super_attrs['vacation mode'] = self.wink.vacation_mode_enabled()
|
||||
super_attrs['beeper mode'] = self.wink.beeper_enabled()
|
||||
super_attrs['auto lock'] = self.wink.auto_lock_enabled()
|
||||
alarm_mode = dict_value_to_key(ALARM_MODES_MAP,
|
||||
self.wink.alarm_mode())
|
||||
super_attrs['alarm mode'] = alarm_mode
|
||||
super_attrs['alarm enabled'] = self.wink.alarm_enabled()
|
||||
return super_attrs
|
||||
|
||||
|
||||
def dict_value_to_key(dict_map, comp_value):
|
||||
"""Return the key that has the provided value."""
|
||||
for key, value in dict_map.items():
|
||||
if value == comp_value:
|
||||
return key
|
||||
return STATE_UNKNOWN
|
||||
@@ -141,9 +141,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class_id=zwave.const.COMMAND_CLASS_USER_CODE).values():
|
||||
if value.index != code_slot:
|
||||
continue
|
||||
if len(str(usercode)) > 4:
|
||||
if len(str(usercode)) < 4:
|
||||
_LOGGER.error("Invalid code provided: (%s) "
|
||||
"usercode must %s or less digits",
|
||||
"usercode must be atleast 4 and at most"
|
||||
" %s digits",
|
||||
usercode, len(value.data))
|
||||
break
|
||||
value.data = str(usercode)
|
||||
|
||||
@@ -19,7 +19,8 @@ from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST)
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST,
|
||||
EVENT_LOGBOOK_ENTRY)
|
||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||
|
||||
DOMAIN = 'logbook'
|
||||
@@ -47,10 +48,10 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
|
||||
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
|
||||
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_MESSAGE = 'message'
|
||||
ATTR_DOMAIN = 'domain'
|
||||
@@ -191,7 +192,8 @@ def humanify(events):
|
||||
if entity_id is None:
|
||||
continue
|
||||
|
||||
if entity_id.startswith('sensor.'):
|
||||
if entity_id.startswith(tuple('{}.'.format(
|
||||
domain) for domain in CONTINUOUS_DOMAINS)):
|
||||
last_sensor_event[entity_id] = event
|
||||
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
@@ -223,12 +225,12 @@ def humanify(events):
|
||||
domain = to_state.domain
|
||||
|
||||
# Skip all but the last sensor state
|
||||
if domain == 'sensor' and \
|
||||
if domain in CONTINUOUS_DOMAINS and \
|
||||
event != last_sensor_event[to_state.entity_id]:
|
||||
continue
|
||||
|
||||
# Don't show continuous sensor value changes in the logbook
|
||||
if domain == 'sensor' and \
|
||||
if domain in CONTINUOUS_DOMAINS and \
|
||||
to_state.attributes.get('unit_of_measurement'):
|
||||
continue
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['https://github.com/gurumitts/'
|
||||
'pylutron-caseta/archive/v0.2.6.zip#'
|
||||
'pylutron-caseta==v0.2.6']
|
||||
REQUIREMENTS = ['pylutron-caseta==0.2.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -46,7 +44,7 @@ def setup(hass, base_config):
|
||||
|
||||
_LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST])
|
||||
|
||||
for component in ('light', 'switch'):
|
||||
for component in ('light', 'switch', 'cover'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['denonavr==0.4.0']
|
||||
REQUIREMENTS = ['denonavr==0.4.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,20 +9,22 @@ from functools import wraps
|
||||
import logging
|
||||
import urllib
|
||||
import re
|
||||
import os
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
|
||||
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
|
||||
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
|
||||
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||
MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
||||
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -32,6 +34,8 @@ REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EVENT_KODI_CALL_METHOD_RESULT = 'kodi_call_method_result'
|
||||
|
||||
CONF_TCP_PORT = 'tcp_port'
|
||||
CONF_TURN_OFF_ACTION = 'turn_off_action'
|
||||
CONF_ENABLE_WEBSOCKET = 'enable_websocket'
|
||||
@@ -61,8 +65,9 @@ MEDIA_TYPES = {
|
||||
}
|
||||
|
||||
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_SHUFFLE_SET | \
|
||||
SUPPORT_PLAY | SUPPORT_VOLUME_STEP
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@@ -71,6 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
|
||||
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
|
||||
vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||
vol.Optional(CONF_ENABLE_WEBSOCKET, default=DEFAULT_ENABLE_WEBSOCKET):
|
||||
@@ -78,16 +84,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
|
||||
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
|
||||
SERVICE_CALL_METHOD = 'kodi_call_method'
|
||||
|
||||
DATA_KODI = 'kodi'
|
||||
|
||||
ATTR_MEDIA_TYPE = 'media_type'
|
||||
ATTR_MEDIA_NAME = 'media_name'
|
||||
ATTR_MEDIA_ARTIST_NAME = 'artist_name'
|
||||
ATTR_MEDIA_ID = 'media_id'
|
||||
|
||||
MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required('shuffle_on'): cv.boolean,
|
||||
})
|
||||
ATTR_METHOD = 'method'
|
||||
|
||||
MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_TYPE): cv.string,
|
||||
@@ -95,20 +100,25 @@ MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Optional(ATTR_MEDIA_NAME): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
|
||||
})
|
||||
MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_METHOD): cv.string,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ADD_MEDIA: {
|
||||
'method': 'async_add_media_to_playlist',
|
||||
'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA},
|
||||
SERVICE_SET_SHUFFLE: {
|
||||
'method': 'async_set_shuffle',
|
||||
'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA},
|
||||
SERVICE_CALL_METHOD: {
|
||||
'method': 'async_call_method',
|
||||
'schema': MEDIA_PLAYER_CALL_METHOD_SCHEMA},
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Kodi platform."""
|
||||
if DATA_KODI not in hass.data:
|
||||
hass.data[DATA_KODI] = []
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
tcp_port = config.get(CONF_TCP_PORT)
|
||||
@@ -128,8 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
host=host, port=port, tcp_port=tcp_port, encryption=encryption,
|
||||
username=config.get(CONF_USERNAME),
|
||||
password=config.get(CONF_PASSWORD),
|
||||
turn_off_action=config.get(CONF_TURN_OFF_ACTION), websocket=websocket)
|
||||
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
|
||||
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
|
||||
|
||||
hass.data[DATA_KODI].append(entity)
|
||||
async_add_devices([entity], update_before_add=True)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -141,23 +153,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
params = {key: value for key, value in service.data.items()
|
||||
if key != 'entity_id'}
|
||||
|
||||
yield from getattr(entity, method['method'])(**params)
|
||||
entity_ids = service.data.get('entity_id')
|
||||
if entity_ids:
|
||||
target_players = [player for player in hass.data[DATA_KODI]
|
||||
if player.entity_id in entity_ids]
|
||||
else:
|
||||
target_players = hass.data[DATA_KODI]
|
||||
|
||||
update_tasks = []
|
||||
if entity.should_poll:
|
||||
update_coro = entity.async_update_ha_state(True)
|
||||
update_tasks.append(update_coro)
|
||||
for player in target_players:
|
||||
yield from getattr(player, method['method'])(**params)
|
||||
|
||||
for player in target_players:
|
||||
if player.should_poll:
|
||||
update_coro = player.async_update_ha_state(True)
|
||||
update_tasks.append(update_coro)
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
|
||||
return
|
||||
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
schema = SERVICE_TO_METHOD[service].get(
|
||||
'schema', MEDIA_PLAYER_SCHEMA)
|
||||
schema = SERVICE_TO_METHOD[service]['schema']
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler,
|
||||
description=None, schema=schema)
|
||||
description=descriptions.get(service), schema=schema)
|
||||
|
||||
|
||||
def cmd(func):
|
||||
@@ -185,7 +211,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
def __init__(self, hass, name, host, port, tcp_port, encryption=False,
|
||||
username=None, password=None, turn_off_action=None,
|
||||
websocket=True):
|
||||
timeout=DEFAULT_TIMEOUT, websocket=True):
|
||||
"""Initialize the Kodi device."""
|
||||
import jsonrpc_async
|
||||
import jsonrpc_websocket
|
||||
@@ -193,7 +219,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
self._name = name
|
||||
|
||||
kwargs = {
|
||||
'timeout': DEFAULT_TIMEOUT,
|
||||
'timeout': timeout,
|
||||
'session': async_get_clientsession(hass),
|
||||
}
|
||||
|
||||
@@ -657,16 +683,40 @@ class KodiDevice(MediaPlayerDevice):
|
||||
{"item": {"file": str(media_id)}})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_shuffle(self, shuffle_on):
|
||||
def async_set_shuffle(self, shuffle):
|
||||
"""Set shuffle mode, for the first player."""
|
||||
if len(self._players) < 1:
|
||||
raise RuntimeError("Error: No active player.")
|
||||
yield from self.server.Player.SetShuffle(
|
||||
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on})
|
||||
{"playerid": self._players[0]['playerid'], "shuffle": shuffle})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_call_method(self, method, **kwargs):
|
||||
"""Run Kodi JSONRPC API method with params."""
|
||||
import jsonrpc_base
|
||||
_LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs)
|
||||
result_ok = False
|
||||
try:
|
||||
result = yield from getattr(self.server, method)(**kwargs)
|
||||
result_ok = True
|
||||
except jsonrpc_base.jsonrpc.ProtocolError as exc:
|
||||
result = exc.args[2]['error']
|
||||
_LOGGER.error('Run API method %s.%s(%s) error: %s',
|
||||
self.entity_id, method, kwargs, result)
|
||||
|
||||
if isinstance(result, dict):
|
||||
event_data = {'entity_id': self.entity_id,
|
||||
'result': result,
|
||||
'result_ok': result_ok,
|
||||
'input': {'method': method, 'params': kwargs}}
|
||||
_LOGGER.debug('EVENT kodi_call_method_result: %s', event_data)
|
||||
self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT,
|
||||
event_data=event_data)
|
||||
return result
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_add_media_to_playlist(
|
||||
self, media_type, media_id=None, media_name='', artist_name=''):
|
||||
self, media_type, media_id=None, media_name='ALL', artist_name=''):
|
||||
"""Add a media to default playlist (i.e. playlistid=0).
|
||||
|
||||
First the media type must be selected, then
|
||||
@@ -675,13 +725,14 @@ class KodiDevice(MediaPlayerDevice):
|
||||
All the albums of an artist can be added with
|
||||
media_name="ALL"
|
||||
"""
|
||||
import jsonrpc_base
|
||||
params = {"playlistid": 0}
|
||||
if media_type == "SONG":
|
||||
if media_id is None:
|
||||
media_id = yield from self.async_find_song(
|
||||
media_name, artist_name)
|
||||
|
||||
yield from self.server.Playlist.Add(
|
||||
{"playlistid": 0, "item": {"songid": int(media_id)}})
|
||||
if media_id:
|
||||
params["item"] = {"songid": int(media_id)}
|
||||
|
||||
elif media_type == "ALBUM":
|
||||
if media_id is None:
|
||||
@@ -691,12 +742,22 @@ class KodiDevice(MediaPlayerDevice):
|
||||
|
||||
media_id = yield from self.async_find_album(
|
||||
media_name, artist_name)
|
||||
if media_id:
|
||||
params["item"] = {"albumid": int(media_id)}
|
||||
|
||||
yield from self.server.Playlist.Add(
|
||||
{"playlistid": 0, "item": {"albumid": int(media_id)}})
|
||||
else:
|
||||
raise RuntimeError("Unrecognized media type.")
|
||||
|
||||
if media_id is not None:
|
||||
try:
|
||||
yield from self.server.Playlist.Add(params)
|
||||
except jsonrpc_base.jsonrpc.ProtocolError as exc:
|
||||
result = exc.args[2]['error']
|
||||
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s',
|
||||
self.entity_id, media_type, result)
|
||||
else:
|
||||
_LOGGER.warning('No media detected for Playlist.Add')
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_add_all_albums(self, artist_name):
|
||||
"""Add all albums of an artist to default playlist (i.e. playlistid=0).
|
||||
@@ -734,9 +795,13 @@ class KodiDevice(MediaPlayerDevice):
|
||||
def async_find_artist(self, artist_name):
|
||||
"""Find artist by name."""
|
||||
artists = yield from self.async_get_artists()
|
||||
out = self._find(
|
||||
artist_name, [a['artist'] for a in artists['artists']])
|
||||
return artists['artists'][out[0][0]]['artistid']
|
||||
try:
|
||||
out = self._find(
|
||||
artist_name, [a['artist'] for a in artists['artists']])
|
||||
return artists['artists'][out[0][0]]['artistid']
|
||||
except KeyError:
|
||||
_LOGGER.warning('No artists were found: %s', artist_name)
|
||||
return None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_songs(self, artist_id=None):
|
||||
@@ -769,8 +834,14 @@ class KodiDevice(MediaPlayerDevice):
|
||||
artist_id = yield from self.async_find_artist(artist_name)
|
||||
|
||||
albums = yield from self.async_get_albums(artist_id)
|
||||
out = self._find(album_name, [a['label'] for a in albums['albums']])
|
||||
return albums['albums'][out[0][0]]['albumid']
|
||||
try:
|
||||
out = self._find(
|
||||
album_name, [a['label'] for a in albums['albums']])
|
||||
return albums['albums'][out[0][0]]['albumid']
|
||||
except KeyError:
|
||||
_LOGGER.warning('No albums were found with artist: %s, album: %s',
|
||||
artist_name, album_name)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find(key_word, words):
|
||||
|
||||
@@ -14,9 +14,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/'
|
||||
'066023aec04770518d494c32fb72eea0ec5c1b7c.zip#'
|
||||
'onkyo-eiscp==1.0']
|
||||
REQUIREMENTS = ['onkyo-eiscp==1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.loader as loader
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/bah2830/python-roku/archive/3.1.3.zip'
|
||||
'#roku==3.1.3']
|
||||
REQUIREMENTS = ['python-roku==3.1.3']
|
||||
|
||||
KNOWN_HOSTS = []
|
||||
DEFAULT_PORT = 8060
|
||||
|
||||
@@ -15,9 +15,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/laf/russound/archive/0.1.7.zip'
|
||||
'#russound==0.1.7']
|
||||
REQUIREMENTS = ['russound==0.1.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -269,3 +269,34 @@ soundtouch_remove_zone_slave:
|
||||
slaves:
|
||||
description: Name of slaves entities to remove from the existing zone
|
||||
example: 'media_player.soundtouch_bedroom'
|
||||
|
||||
kodi_add_to_playlist:
|
||||
description: Add music to the default playlist (i.e. playlistid=0).
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the Kodi entities where to add the media.
|
||||
example: 'media_player.living_room_kodi'
|
||||
media_type:
|
||||
description: Media type identifier. It must be one of SONG or ALBUM.
|
||||
example: ALBUM
|
||||
media_id:
|
||||
description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library.
|
||||
example: 123456
|
||||
media_name:
|
||||
description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist.
|
||||
example: 'Highway to Hell'
|
||||
artist_name:
|
||||
description: Optional artist name for filtering media.
|
||||
example: 'AC/DC'
|
||||
|
||||
kodi_call_method:
|
||||
description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.'
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of the Kodi entities where to run the API method.
|
||||
example: 'media_player.living_room_kodi'
|
||||
method:
|
||||
description: Name of the Kodi JSONRPC API method to be called.
|
||||
example: 'VideoLibrary.GetRecentlyAddedEpisodes'
|
||||
@@ -51,6 +51,7 @@ SERVICE_SNAPSHOT = 'sonos_snapshot'
|
||||
SERVICE_RESTORE = 'sonos_restore'
|
||||
SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
|
||||
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
|
||||
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
|
||||
|
||||
DATA_SONOS = 'sonos'
|
||||
|
||||
@@ -62,6 +63,11 @@ CONF_INTERFACE_ADDR = 'interface_addr'
|
||||
|
||||
# Service call validation schemas
|
||||
ATTR_SLEEP_TIME = 'sleep_time'
|
||||
ATTR_ALARM_ID = 'alarm_id'
|
||||
ATTR_VOLUME = 'volume'
|
||||
ATTR_ENABLED = 'enabled'
|
||||
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
|
||||
ATTR_TIME = 'time'
|
||||
ATTR_MASTER = 'master'
|
||||
ATTR_WITH_GROUP = 'with_group'
|
||||
|
||||
@@ -90,6 +96,14 @@ SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=86399))
|
||||
})
|
||||
|
||||
SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
|
||||
vol.Required(ATTR_ALARM_ID): cv.positive_int,
|
||||
vol.Optional(ATTR_TIME): cv.time,
|
||||
vol.Optional(ATTR_VOLUME): cv.small_float,
|
||||
vol.Optional(ATTR_ENABLED): cv.boolean,
|
||||
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Sonos platform."""
|
||||
@@ -163,9 +177,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
elif service.service == SERVICE_RESTORE:
|
||||
device.restore(service.data[ATTR_WITH_GROUP])
|
||||
elif service.service == SERVICE_SET_TIMER:
|
||||
device.set_timer(service.data[ATTR_SLEEP_TIME])
|
||||
device.set_sleep_timer(service.data[ATTR_SLEEP_TIME])
|
||||
elif service.service == SERVICE_CLEAR_TIMER:
|
||||
device.clear_timer()
|
||||
device.clear_sleep_timer()
|
||||
elif service.service == SERVICE_UPDATE_ALARM:
|
||||
device.update_alarm(**service.data)
|
||||
|
||||
device.schedule_update_ha_state(True)
|
||||
|
||||
@@ -193,6 +209,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
DOMAIN, SERVICE_CLEAR_TIMER, service_handle,
|
||||
descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_UPDATE_ALARM, service_handle,
|
||||
descriptions.get(SERVICE_UPDATE_ALARM),
|
||||
schema=SONOS_UPDATE_ALARM_SCHEMA)
|
||||
|
||||
|
||||
def _parse_timespan(timespan):
|
||||
"""Parse a time-span into number of seconds."""
|
||||
@@ -1034,6 +1055,30 @@ class SonosDevice(MediaPlayerDevice):
|
||||
"""Clear the timer on the player."""
|
||||
self._player.set_sleep_timer(None)
|
||||
|
||||
@soco_error
|
||||
@soco_coordinator
|
||||
def update_alarm(self, **data):
|
||||
"""Set the alarm clock on the player."""
|
||||
from soco import alarms
|
||||
a = None
|
||||
for alarm in alarms.get_alarms(self.soco):
|
||||
# pylint: disable=protected-access
|
||||
if alarm._alarm_id == str(data[ATTR_ALARM_ID]):
|
||||
a = alarm
|
||||
if a is None:
|
||||
_LOGGER.warning("did not find alarm with id %s",
|
||||
data[ATTR_ALARM_ID])
|
||||
return
|
||||
if ATTR_TIME in data:
|
||||
a.start_time = data[ATTR_TIME]
|
||||
if ATTR_VOLUME in data:
|
||||
a.volume = int(data[ATTR_VOLUME] * 100)
|
||||
if ATTR_ENABLED in data:
|
||||
a.enabled = data[ATTR_ENABLED]
|
||||
if ATTR_INCLUDE_LINKED_ZONES in data:
|
||||
a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
|
||||
a.save()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
|
||||
@@ -176,6 +176,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
self._state = STATE_PAUSED
|
||||
if current.get('is_playing'):
|
||||
self._state = STATE_PLAYING
|
||||
self._shuffle = current.get('shuffle_state')
|
||||
device = current.get('device')
|
||||
if device is None:
|
||||
self._state = STATE_IDLE
|
||||
@@ -184,8 +185,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
self._volume = device.get('volume_percent') / 100
|
||||
if device.get('name'):
|
||||
self._current_device = device.get('name')
|
||||
if device.get('shuffle_state'):
|
||||
self._shuffle = device.get('shuffle_state')
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set the volume level."""
|
||||
@@ -213,7 +212,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select playback device."""
|
||||
self._player.transfer_playback(self._devices[source])
|
||||
self._player.transfer_playback(self._devices[source],
|
||||
self._state == STATE_PLAYING)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play media."""
|
||||
|
||||
@@ -28,10 +28,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'microsoft_face'
|
||||
DEPENDENCIES = ['camera']
|
||||
|
||||
FACE_API_URL = "https://westus.api.cognitive.microsoft.com/face/v1.0/{0}"
|
||||
FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}"
|
||||
|
||||
DATA_MICROSOFT_FACE = 'microsoft_face'
|
||||
|
||||
CONF_AZURE_REGION = 'azure_region'
|
||||
|
||||
SERVICE_CREATE_GROUP = 'create_group'
|
||||
SERVICE_DELETE_GROUP = 'delete_group'
|
||||
SERVICE_TRAIN_GROUP = 'train_group'
|
||||
@@ -49,6 +51,7 @@ DEFAULT_TIMEOUT = 10
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@@ -115,6 +118,7 @@ def async_setup(hass, config):
|
||||
entities = {}
|
||||
face = MicrosoftFace(
|
||||
hass,
|
||||
config[DOMAIN].get(CONF_AZURE_REGION),
|
||||
config[DOMAIN].get(CONF_API_KEY),
|
||||
config[DOMAIN].get(CONF_TIMEOUT),
|
||||
entities
|
||||
@@ -304,12 +308,13 @@ class MicrosoftFaceGroupEntity(Entity):
|
||||
class MicrosoftFace(object):
|
||||
"""Microsoft Face api for HomeAssistant."""
|
||||
|
||||
def __init__(self, hass, api_key, timeout, entities):
|
||||
def __init__(self, hass, server_loc, api_key, timeout, entities):
|
||||
"""Initialize Microsoft Face api."""
|
||||
self.hass = hass
|
||||
self.websession = async_get_clientsession(hass)
|
||||
self.timeout = timeout
|
||||
self._api_key = api_key
|
||||
self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL)
|
||||
self._store = {}
|
||||
self._entities = entities
|
||||
|
||||
@@ -346,7 +351,7 @@ class MicrosoftFace(object):
|
||||
params=None):
|
||||
"""Make a api call."""
|
||||
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
|
||||
url = FACE_API_URL.format(function)
|
||||
url = self._server_url.format(function)
|
||||
|
||||
payload = None
|
||||
if binary:
|
||||
|
||||
@@ -25,8 +25,7 @@ from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.frontend import add_manifest_json_key
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
# pyelliptic is dependency of pywebpush and 1.5.8 contains a breaking change
|
||||
REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2', 'pyelliptic==1.5.7']
|
||||
REQUIREMENTS = ['pywebpush==1.0.0', 'PyJWT==1.4.2']
|
||||
|
||||
DEPENDENCIES = ['frontend']
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==4.0.0']
|
||||
REQUIREMENTS = ['sendgrid==4.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.image import MIMEImage
|
||||
import email.utils
|
||||
from email.mime.application import MIMEApplication
|
||||
|
||||
import email.utils
|
||||
import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
@@ -26,10 +26,12 @@ import homeassistant.util.dt as dt_util
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_IMAGES = 'images' # optional embedded image file attachments
|
||||
ATTR_HTML = 'html'
|
||||
|
||||
CONF_STARTTLS = 'starttls'
|
||||
CONF_DEBUG = 'debug'
|
||||
CONF_SERVER = 'server'
|
||||
CONF_SENDER_NAME = 'sender_name'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 25
|
||||
@@ -47,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SENDER_NAME): cv.string,
|
||||
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
|
||||
})
|
||||
|
||||
@@ -62,6 +65,7 @@ def get_service(hass, config, discovery_info=None):
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
config.get(CONF_RECIPIENT),
|
||||
config.get(CONF_SENDER_NAME),
|
||||
config.get(CONF_DEBUG))
|
||||
|
||||
if mail_service.connection_is_valid():
|
||||
@@ -74,7 +78,7 @@ class MailNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for E-Mail messages."""
|
||||
|
||||
def __init__(self, server, port, timeout, sender, starttls, username,
|
||||
password, recipients, debug):
|
||||
password, recipients, sender_name, debug):
|
||||
"""Initialize the service."""
|
||||
self._server = server
|
||||
self._port = port
|
||||
@@ -84,6 +88,8 @@ class MailNotificationService(BaseNotificationService):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.recipients = recipients
|
||||
self._sender_name = sender_name
|
||||
self._timeout = timeout
|
||||
self.debug = debug
|
||||
self.tries = 2
|
||||
|
||||
@@ -128,19 +134,28 @@ class MailNotificationService(BaseNotificationService):
|
||||
Build and send a message to a user.
|
||||
|
||||
Will send plain text normally, or will build a multipart HTML message
|
||||
with inline image attachments if images config is defined.
|
||||
with inline image attachments if images config is defined, or will
|
||||
build a multipart HTML if html config is defined.
|
||||
"""
|
||||
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
if data:
|
||||
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES))
|
||||
if ATTR_HTML in data:
|
||||
msg = _build_html_msg(message, data[ATTR_HTML],
|
||||
images=data.get(ATTR_IMAGES))
|
||||
else:
|
||||
msg = _build_multipart_msg(message,
|
||||
images=data.get(ATTR_IMAGES))
|
||||
else:
|
||||
msg = _build_text_msg(message)
|
||||
|
||||
msg['Subject'] = subject
|
||||
msg['To'] = ','.join(self.recipients)
|
||||
msg['From'] = self._sender
|
||||
if self._sender_name:
|
||||
msg['From'] = '{} <{}>'.format(self._sender_name, self._sender)
|
||||
else:
|
||||
msg['From'] = self._sender
|
||||
msg['X-Mailer'] = 'HomeAssistant'
|
||||
msg['Date'] = email.utils.format_datetime(dt_util.now())
|
||||
msg['Message-Id'] = email.utils.make_msgid()
|
||||
@@ -155,12 +170,16 @@ class MailNotificationService(BaseNotificationService):
|
||||
mail.sendmail(self._sender, self.recipients,
|
||||
msg.as_string())
|
||||
break
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
_LOGGER.warning(
|
||||
"SMTPServerDisconnected sending mail: retrying connection")
|
||||
mail.quit()
|
||||
mail = self.connect()
|
||||
except smtplib.SMTPException:
|
||||
_LOGGER.warning(
|
||||
"SMTPException sending mail: retrying connection")
|
||||
mail.quit()
|
||||
mail = self.connect()
|
||||
|
||||
mail.quit()
|
||||
|
||||
|
||||
@@ -204,3 +223,25 @@ def _build_multipart_msg(message, images):
|
||||
body_html = MIMEText(''.join(body_text), 'html')
|
||||
msg_alt.attach(body_html)
|
||||
return msg
|
||||
|
||||
|
||||
def _build_html_msg(text, html, images):
|
||||
"""Build Multipart message with in-line images and rich html (UTF-8)."""
|
||||
_LOGGER.debug("Building html rich email")
|
||||
msg = MIMEMultipart('related')
|
||||
alternative = MIMEMultipart('alternative')
|
||||
alternative.attach(MIMEText(text, _charset='utf-8'))
|
||||
alternative.attach(MIMEText(html, ATTR_HTML, _charset='utf-8'))
|
||||
msg.attach(alternative)
|
||||
|
||||
for atch_num, atch_name in enumerate(images):
|
||||
name = os.path.basename(atch_name)
|
||||
try:
|
||||
with open(atch_name, 'rb') as attachment_file:
|
||||
attachment = MIMEImage(attachment_file.read(), filename=name)
|
||||
msg.attach(attachment)
|
||||
attachment.add_header('Content-ID', '<{}>'.format(name))
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
|
||||
atch_name, atch_num)
|
||||
return msg
|
||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user