Merge pull request #2760 from home-assistant/dev

0.26
This commit is contained in:
Paulus Schoutsen 2016-08-13 12:01:56 -07:00 committed by GitHub
commit 0270ae05e9
171 changed files with 4272 additions and 830 deletions

View file

@ -4,6 +4,7 @@ source = homeassistant
omit = omit =
homeassistant/__main__.py homeassistant/__main__.py
homeassistant/scripts/*.py homeassistant/scripts/*.py
homeassistant/helpers/typing.py
# omit pieces of code that rely on external devices being present # omit pieces of code that rely on external devices being present
homeassistant/components/apcupsd.py homeassistant/components/apcupsd.py
@ -88,6 +89,9 @@ omit =
homeassistant/components/homematic.py homeassistant/components/homematic.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/knx.py homeassistant/components/knx.py
homeassistant/components/switch/knx.py homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py homeassistant/components/binary_sensor/knx.py
@ -100,6 +104,7 @@ omit =
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/generic.py homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
@ -123,8 +128,9 @@ omit =
homeassistant/components/discovery.py homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/feedreader.py homeassistant/components/feedreader.py
homeassistant/components/garage_door/wink.py homeassistant/components/foursquare.py
homeassistant/components/garage_door/rpi_gpio.py homeassistant/components/garage_door/rpi_gpio.py
homeassistant/components/garage_door/wink.py
homeassistant/components/hdmi_cec.py homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py homeassistant/components/joaoapps_join.py
@ -192,23 +198,27 @@ omit =
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py
homeassistant/components/sensor/onewire.py homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/openexchangerates.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/plex.py homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/steam_online.py

2
.dockerignore Normal file
View file

@ -0,0 +1,2 @@
.tox
.git

10
.gitignore vendored
View file

@ -7,7 +7,9 @@ config/custom_components/*
!config/custom_components/example.py !config/custom_components/example.py
!config/custom_components/hello_world.py !config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py !config/custom_components/mqtt_example.py
!config/custom_components/react_panel !config/panels
config/panels/*
!config/panels/react.html
tests/testing_config/deps tests/testing_config/deps
tests/testing_config/home-assistant.log tests/testing_config/home-assistant.log
@ -52,7 +54,8 @@ develop-eggs
lib lib
lib64 lib64
# Installer logs # Logs
*.log
pip-log.txt pip-log.txt
# Unit test / coverage reports # Unit test / coverage reports
@ -91,3 +94,6 @@ ctags.tmp
virtualization/vagrant/setup_done virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant virtualization/vagrant/.vagrant
virtualization/vagrant/config virtualization/vagrant/config
# Visual Studio Code
.vscode

View file

@ -10,8 +10,8 @@ homeassistant:
# Impacts weather/sunrise data # Impacts weather/sunrise data
elevation: 665 elevation: 665
# C for Celsius, F for Fahrenheit # 'metric' for Metric System, 'imperial' for imperial system
temperature_unit: C unit_system: metric
# Pick yours from here: # Pick yours from here:
# http://en.wikipedia.org/wiki/List_of_tz_database_time_zones # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones

View file

@ -1,30 +0,0 @@
"""
Custom panel example showing TodoMVC using React.
Will add a panel to control lights and switches using React. Allows configuring
the title via configuration.yaml:
react_panel:
title: 'home'
"""
import os
from homeassistant.components.frontend import register_panel
DOMAIN = 'react_panel'
DEPENDENCIES = ['frontend']
PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html')
def setup(hass, config):
"""Initialize custom panel."""
title = config.get(DOMAIN, {}).get('title')
config = None if title is None else {'title': title}
register_panel(hass, 'react', PANEL_PATH,
title='TodoMVC', icon='mdi:checkbox-marked-outline',
config=config)
return True

View file

@ -1,3 +1,20 @@
<!--
Custom Home Assistant panel example.
Currently only works in Firefox and Chrome because it uses ES6.
Make sure this file is in <config>/panels/react.html
Add to your configuration.yaml:
panel_custom:
- name: react
sidebar_title: TodoMVC
sidebar_icon: mdi:checkbox-marked-outline
config:
title: Wow hello!
-->
<script src="https://fb.me/react-15.2.1.min.js"></script> <script src="https://fb.me/react-15.2.1.min.js"></script>
<script src="https://fb.me/react-dom-15.2.1.min.js"></script> <script src="https://fb.me/react-dom-15.2.1.min.js"></script>

View file

@ -419,8 +419,9 @@ definitions:
description: Longitude of Home Assistant server description: Longitude of Home Assistant server
location_name: location_name:
type: string type: string
temperature_unit: unit_system:
type: string type: string
description: The system for measurement units
time_zone: time_zone:
type: string type: string
version: version:

View file

@ -274,7 +274,7 @@ def try_to_restart() -> None:
# thread left (which is us). Nothing we really do with it, but it might be # thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues. # useful when debugging shutdown/restart issues.
try: try:
nthreads = sum(thread.isAlive() and not thread.isDaemon() nthreads = sum(thread.is_alive() and not thread.daemon
for thread in threading.enumerate()) for thread in threading.enumerate())
if nthreads > 1: if nthreads > 1:
sys.stderr.write( sys.stderr.write(

View file

@ -11,12 +11,12 @@ from types import ModuleType
from typing import Any, Optional, Dict from typing import Any, Optional, Dict
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error
import homeassistant.components as core_components import homeassistant.components as core_components
from homeassistant.components import group, persistent_notification from homeassistant.components import group, persistent_notification
import homeassistant.config as conf_util import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.package as pkg_util import homeassistant.util.package as pkg_util
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
@ -103,7 +103,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
config = component.CONFIG_SCHEMA(config) config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, config) _log_exception(ex, domain, config)
return False return False
elif hasattr(component, 'PLATFORM_SCHEMA'): elif hasattr(component, 'PLATFORM_SCHEMA'):
@ -113,7 +113,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
p_validated = component.PLATFORM_SCHEMA(p_config) p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, p_config) _log_exception(ex, domain, p_config)
return False return False
# Not all platform components follow same pattern for platforms # Not all platform components follow same pattern for platforms
@ -134,8 +134,8 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try: try:
p_validated = platform.PLATFORM_SCHEMA(p_validated) p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, '{}.{}' _log_exception(ex, '{}.{}'.format(domain, p_name),
.format(domain, p_name), p_validated) p_validated)
return False return False
platforms.append(p_validated) platforms.append(p_validated)
@ -232,14 +232,14 @@ def from_config_dict(config: Dict[str, Any],
if config_dir is not None: if config_dir is not None:
config_dir = os.path.abspath(config_dir) config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
_mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
conf_util.process_ha_core_config(hass, core_config) conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex: except vol.Invalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config) _log_exception(ex, 'homeassistant', core_config)
return None return None
conf_util.process_ha_config_upgrade(hass) conf_util.process_ha_config_upgrade(hass)
@ -300,7 +300,7 @@ def from_config_file(config_path: str,
# Set config dir to directory holding config file # Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path)) config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
_mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
enable_logging(hass, verbose, log_rotate_days) enable_logging(hass, verbose, log_rotate_days)
@ -371,6 +371,26 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
loader.prepare(hass) loader.prepare(hass)
def _mount_local_lib_path(config_dir: str) -> None: def _log_exception(ex, domain, config):
"""Generate log exception for config validation."""
message = 'Invalid config for [{}]: '.format(domain)
if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
.format(ex.path[-1], domain, domain,
'->'.join('%s' % m for m in ex.path))
else:
message += humanize_error(config, ex)
if hasattr(config, '__line__'):
message += " (See {}:{})".format(config.__config_file__,
config.__line__ or '?')
_LOGGER.error(message)
def mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path.""" """Add local library to Python Path."""
deps_dir = os.path.join(config_dir, 'deps')
if deps_dir not in sys.path:
sys.path.insert(0, os.path.join(config_dir, 'deps')) sys.path.insert(0, os.path.join(config_dir, 'deps'))
return deps_dir

View file

@ -11,7 +11,6 @@ import itertools as it
import logging import logging
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers.service import extract_entity_ids from homeassistant.helpers.service import extract_entity_ids
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import ( from homeassistant.const import (
@ -35,7 +34,7 @@ def is_on(hass, entity_id=None):
entity_ids = hass.states.entity_ids() entity_ids = hass.states.entity_ids()
for entity_id in entity_ids: for entity_id in entity_ids:
domain = split_entity_id(entity_id)[0] domain = ha.split_entity_id(entity_id)[0]
module = get_component(domain) module = get_component(domain)
@ -95,7 +94,7 @@ def setup(hass, config):
# Group entity_ids by domain. groupby requires sorted data. # Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(sorted(entity_ids), by_domain = it.groupby(sorted(entity_ids),
lambda item: split_entity_id(item)[0]) lambda item: ha.split_entity_id(item)[0])
for domain, ent_ids in by_domain: for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls # We want to block for all calls and only return when all calls

View file

@ -20,6 +20,7 @@ from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = 'alarm_control_panel'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -124,6 +125,11 @@ class AlarmControlPanel(Entity):
"""Regex for code format or None if no code is required.""" """Regex for code format or None if no code is required."""
return None return None
@property
def changed_by(self):
"""Last change triggered by."""
return None
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
raise NotImplementedError() raise NotImplementedError()
@ -145,5 +151,6 @@ class AlarmControlPanel(Entity):
"""Return the state attributes.""" """Return the state attributes."""
state_attr = { state_attr = {
ATTR_CODE_FORMAT: self.code_format, ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
} }
return state_attr return state_attr

View file

@ -0,0 +1,43 @@
alarm_disarm:
description: Send the alarm the command for disarm
fields:
entity_id:
description: Name of alarm control panel to disarm
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to disarm the alarm control panel with
example: 1234
alarm_arm_home:
description: Send the alarm the command for arm home
fields:
entity_id:
description: Name of alarm control panel to arm home
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to arm home the alarm control panel with
example: 1234
alarm_arm_away:
description: Send the alarm the command for arm away
fields:
entity_id:
description: Name of alarm control panel to arm away
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to arm away the alarm control panel with
example: 1234
alarm_trigger:
description: Send the alarm the command for trigger
fields:
entity_id:
description: Name of alarm control panel to trigger
example: 'alarm_control_panel.downstairs'
code:
description: An optional code to trigger the alarm control panel with
example: 1234

View file

@ -37,6 +37,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
self._id = device_id self._id = device_id
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._digits = int(hub.config.get('code_digits', '4')) self._digits = int(hub.config.get('code_digits', '4'))
self._changed_by = None
@property @property
def name(self): def name(self):
@ -58,6 +59,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""The code format as regex.""" """The code format as regex."""
return '^\\d{%s}$' % self._digits return '^\\d{%s}$' % self._digits
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
def update(self): def update(self):
"""Update alarm status.""" """Update alarm status."""
hub.update_alarms() hub.update_alarms()
@ -72,6 +78,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
_LOGGER.error( _LOGGER.error(
'Unknown alarm state %s', 'Unknown alarm state %s',
hub.alarm_status[self._id].status) hub.alarm_status[self._id].status)
self._changed_by = hub.alarm_status[self._id].name
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View file

@ -0,0 +1,75 @@
"""
Support for Cameras with FFmpeg as decoder.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.ffmpeg/
"""
import logging
from contextlib import closing
import voluptuous as vol
from homeassistant.components.camera import Camera
from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME, CONF_PLATFORM
REQUIREMENTS = ["ha-ffmpeg==0.4"]
CONF_INPUT = 'input'
CONF_FFMPEG_BIN = 'ffmpeg_bin'
CONF_EXTRA_ARGUMENTS = 'extra_arguments'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "ffmpeg",
vol.Optional(CONF_NAME, default="FFmpeg"): cv.string,
vol.Required(CONF_INPUT): cv.string,
vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string,
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup a FFmpeg Camera."""
add_devices_callback([FFmpegCamera(config)])
class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera."""
def __init__(self, config):
"""Initialize a FFmpeg camera."""
super().__init__()
self._name = config.get(CONF_NAME)
self._input = config.get(CONF_INPUT)
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN)
def _ffmpeg_stream(self):
"""Return a FFmpeg process object."""
from haffmpeg import CameraMjpeg
ffmpeg = CameraMjpeg(self._ffmpeg_bin)
ffmpeg.open_camera(self._input, extra_cmd=self._extra_arguments)
return ffmpeg
def camera_image(self):
"""Return a still image response from the camera."""
with closing(self._ffmpeg_stream()) as stream:
return extract_image_from_mjpeg(stream)
def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera."""
stream = self._ffmpeg_stream()
return response(
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver',
direct_passthrough=True
)
@property
def name(self):
"""Return the name of this camera."""
return self._name

View file

@ -28,6 +28,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback([MjpegCamera(config)]) add_devices_callback([MjpegCamera(config)])
def extract_image_from_mjpeg(stream):
"""Take in a MJPEG stream object, return the jpg from it."""
data = b''
for chunk in stream:
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
class MjpegCamera(Camera): class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL.""" """An implementation of an IP camera that is reachable over a URL."""
@ -52,19 +64,8 @@ class MjpegCamera(Camera):
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
def process_response(response):
"""Take in a response object, return the jpg from it."""
data = b''
for chunk in response.iter_content(1024):
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
with closing(self.camera_stream()) as response: with closing(self.camera_stream()) as response:
return process_response(response) return extract_image_from_mjpeg(response.iter_content(1024))
def mjpeg_stream(self, response): def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""

View file

@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)') REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.11.0'] REQUIREMENTS = ['fuzzywuzzy==0.11.1']
def setup(hass, config): def setup(hass, config):

View file

@ -21,10 +21,10 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home # Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval" CONF_HOME_INTERVAL = "home_interval"
REQUIREMENTS = ['python-nmap==0.6.0'] REQUIREMENTS = ['python-nmap==0.6.1']
def get_scanner(hass, config): def get_scanner(hass, config):

View file

@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery" DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.7.0'] REQUIREMENTS = ['netdisco==0.7.1']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds

View file

@ -0,0 +1,99 @@
"""
Allows utilizing the Foursquare (Swarm) API.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/foursquare/
"""
import logging
import os
import json
import requests
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
DOMAIN = "foursquare"
SERVICE_CHECKIN = "checkin"
EVENT_PUSH = "foursquare.push"
EVENT_CHECKIN = "foursquare.checkin"
CHECKIN_SERVICE_SCHEMA = vol.Schema({
vol.Required("venueId"): cv.string,
vol.Optional("eventId"): cv.string,
vol.Optional("shout"): cv.string,
vol.Optional("mentions"): cv.string,
vol.Optional("broadcast"): cv.string,
vol.Optional("ll"): cv.string,
vol.Optional("llAcc"): cv.string,
vol.Optional("alt"): cv.string,
vol.Optional("altAcc"): cv.string,
})
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["http"]
def setup(hass, config):
"""Setup the Foursquare component."""
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), "services.yaml"))
config = config[DOMAIN]
def checkin_user(call):
"""Check a user in on Swarm."""
url = ("https://api.foursquare.com/v2/checkins/add"
"?oauth_token={}"
"&v=20160802"
"&m=swarm").format(config["access_token"])
response = requests.post(url, data=call.data, timeout=10)
if response.status_code not in (200, 201):
_LOGGER.exception(
"Error checking in user. Response %d: %s:",
response.status_code, response.reason)
hass.bus.fire(EVENT_CHECKIN, response.text)
# Register our service with Home Assistant.
hass.services.register(DOMAIN, "checkin", checkin_user,
descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA)
hass.wsgi.register_view(FoursquarePushReceiver(hass,
config["push_secret"]))
return True
class FoursquarePushReceiver(HomeAssistantView):
"""Handle pushes from the Foursquare API."""
requires_auth = False
url = "/api/foursquare"
name = "foursquare"
def __init__(self, hass, push_secret):
"""Initialize the OAuth callback view."""
super().__init__(hass)
self.push_secret = push_secret
def post(self, request):
"""Accept the POST from Foursquare."""
raw_data = request.form
_LOGGER.debug("Received Foursquare push: %s", raw_data)
if self.push_secret != raw_data["secret"]:
_LOGGER.error("Received Foursquare push with invalid"
"push secret! Data: %s", raw_data)
return
parsed_payload = {
key: json.loads(val) for key, val in raw_data.items()
if key != "secret"
}
self.hass.bus.fire(EVENT_PUSH, parsed_payload)

View file

@ -20,8 +20,8 @@ _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, title=None, icon=None, def register_built_in_panel(hass, component_name, sidebar_title=None,
url_name=None, config=None): sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel.""" """Register a built-in panel."""
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name) path = 'panels/ha-panel-{}.html'.format(component_name)
@ -33,30 +33,31 @@ def register_built_in_panel(hass, component_name, title=None, icon=None,
url = None # use default url generate mechanism url = None # use default url generate mechanism
register_panel(hass, component_name, os.path.join(STATIC_PATH, path), register_panel(hass, component_name, os.path.join(STATIC_PATH, path),
FINGERPRINTS[path], title, icon, url_name, url, config) FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path,
url, config)
def register_panel(hass, component_name, path, md5=None, title=None, icon=None, def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
url_name=None, url=None, config=None): sidebar_icon=None, url_path=None, url=None, config=None):
"""Register a panel for the frontend. """Register a panel for the frontend.
component_name: name of the web component component_name: name of the web component
path: path to the HTML of the web component path: path to the HTML of the web component
md5: the md5 hash of the web component (for versioning, optional) md5: the md5 hash of the web component (for versioning, optional)
title: title to show in the sidebar (optional) sidebar_title: title to show in the sidebar (optional)
icon: icon to show next to title in sidebar (optional) sidebar_icon: icon to show next to title in sidebar (optional)
url_name: name to use in the url (defaults to component_name) url_path: name to use in the url (defaults to component_name)
url: for the web component (for dev environment, optional) url: for the web component (for dev environment, optional)
config: config to be passed into the web component config: config to be passed into the web component
Warning: this API will probably change. Use at own risk. Warning: this API will probably change. Use at own risk.
""" """
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
if url_name is None: if url_path is None:
url_name = component_name url_path = component_name
if url_name in PANELS: if url_path in PANELS:
_LOGGER.warning('Overwriting component %s', url_name) _LOGGER.warning('Overwriting component %s', url_path)
if not os.path.isfile(path): if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s', _LOGGER.error('Panel %s component does not exist: %s',
component_name, path) component_name, path)
@ -67,14 +68,14 @@ def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest() md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = { data = {
'url_name': url_name, 'url_path': url_path,
'component_name': component_name, 'component_name': component_name,
} }
if title: if sidebar_title:
data['title'] = title data['title'] = sidebar_title
if icon: if sidebar_icon:
data['icon'] = icon data['icon'] = sidebar_icon
if config is not None: if config is not None:
data['config'] = config data['config'] = config
@ -90,7 +91,7 @@ def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url data['url'] = fprinted_url
PANELS[url_name] = data PANELS[url_path] = data
def setup(hass, config): def setup(hass, config):
@ -195,6 +196,6 @@ class IndexView(HomeAssistantView):
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url) panel_url=panel_url, panels=PANELS)
return self.Response(resp, mimetype='text/html') return self.Response(resp, mimetype='text/html')

View file

@ -8,6 +8,9 @@
<link rel='icon' href='/static/icons/favicon.ico'> <link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180' <link rel='apple-touch-icon' sizes='180x180'
href='/static/icons/favicon-apple-180x180.png'> href='/static/icons/favicon-apple-180x180.png'>
{% for panel in panels.values() -%}
<link rel='prefetch' href='{{ panel.url }}'>
{% endfor -%}
<meta name='apple-mobile-web-app-capable' content='yes'> <meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/> <meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/> <meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
@ -86,9 +89,9 @@
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #} {# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script src='{{ core_url }}'></script> <script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'> <link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url %} {% if panel_url -%}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async> <link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif %} {% endif -%}
<link rel='import' href='{{ icons_url }}' async> <link rel='import' href='{{ icons_url }}' async>
<script> <script>
var webComponentsSupported = ( var webComponentsSupported = (

View file

@ -1,16 +1,16 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "bc78f21f5280217aa2c78dfc5848134f", "core.js": "457d5acd123e7dc38947c07984b3a5e8",
"frontend.html": "6c52e8cb797bafa3124d936af5ce1fcc", "frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
"mdi.html": "f6c6cc64c2ec38a80e91f801b41119b3", "mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
"panels/ha-panel-dev-event.html": "20327fbd4fb0370aec9be4db26fd723f", "panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
"panels/ha-panel-dev-info.html": "28e0a19ceb95aa714fd53228d9983a49", "panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
"panels/ha-panel-dev-service.html": "85fd5b48600418bb5a6187539a623c38", "panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
"panels/ha-panel-dev-state.html": "25d84d7b7aea779bb3bb3cd6c155f8d9", "panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
"panels/ha-panel-dev-template.html": "d079abf61cff9690f828cafb0d29b7e7", "panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
"panels/ha-panel-history.html": "7e051b5babf5653b689e0107ea608acb", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "7bdb564a8f37971d7b89b718935810a1", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "9b285357b0b2d82ee282e634f4e1cab2", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
"panels/ha-panel-map.html": "dfe141a3fa5fd403be554def1dd039a9" "panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 697f9397de357cec9662626575fc01d6f921ef22 Subproject commit 474366c536ec3e471da12d5f15b07b79fe9b07e2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style is="custom-style" include="iron-positioning"></style><style>.content{margin-top:64px;padding:24px;background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1) <html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
clear: both;white-space:pre-wrap}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">About</span><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a><a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></partial-base></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html> clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a><a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a><a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><template><style>iframe{border:0;width:100%;height:100%}</style><partial-base narrow="[[narrow]]" show-menu="[[showMenu]]"><span header-title="">[[panel.title]]</span><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></partial-base></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html> <html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-iframe"><template><style include="ha-style">iframe{border:0;width:100%;height:calc(100% - 64px)}</style><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">[[panel.title]]</div></app-toolbar><iframe src="[[panel.config.url]]" sandbox="allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts"></iframe></template></dom-module><script>Polymer({is:"ha-panel-iframe",properties:{panel:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean}}})</script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -72,7 +72,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
def update(self): def update(self):
"""Update the state of the garage door.""" """Update the state of the garage door."""
self._state = rpi_gpio.read_input(self._state_pin) is True self._state = rpi_gpio.read_input(self._state_pin)
@property @property
def is_closed(self): def is_closed(self):

View file

@ -0,0 +1,15 @@
open:
description: Open all or specified garage door
fields:
entity_id:
description: Name(s) of garage door(s) to open
example: 'garage.main'
close:
description: Close all or a specified garage door
fields:
entity_id:
description: Name(s) of garage door(s) to close
example: 'garage.main'

View file

@ -13,7 +13,7 @@ from homeassistant.components import zwave
from homeassistant.components.garage_door import GarageDoorDevice from homeassistant.components.garage_door import GarageDoorDevice
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
COMMAND_CLASS_BARRIER_OPERATOR = 0x66 # 102
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,7 +25,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY: if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY and \
value.command_class != zwave.COMMAND_CLASS_BARRIER_OPERATOR:
return return
if value.type != zwave.TYPE_BOOL: if value.type != zwave.TYPE_BOOL:
return return
@ -62,8 +63,8 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
def close_door(self): def close_door(self):
"""Close the garage door.""" """Close the garage door."""
self._value.node.set_switch(self._value.value_id, False) self._value.data = False
def open_door(self): def open_door(self):
"""Open the garage door.""" """Open the garage door."""
self._value.node.set_switch(self._value.value_id, True) self._value.data = True

View file

@ -14,8 +14,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE) STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
from homeassistant.helpers.entity import ( from homeassistant.helpers.entity import Entity, generate_entity_id
Entity, generate_entity_id, split_entity_id)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -101,7 +100,7 @@ def expand_entity_ids(hass, entity_ids):
try: try:
# If entity_id points at a group, expand it # If entity_id points at a group, expand it
domain, _ = split_entity_id(entity_id) domain, _ = ha.split_entity_id(entity_id)
if domain == DOMAIN: if domain == DOMAIN:
found_ids.extend( found_ids.extend(

View file

@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.10"] REQUIREMENTS = ["pyhomematic==0.1.11"]
HOMEMATIC = None HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_LINK_DELAY = 0.5

View file

@ -20,12 +20,12 @@ from homeassistant.const import (
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.entity import split_entity_id from homeassistant.core import split_entity_id
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = "http" DOMAIN = "http"
REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10") REQUIREMENTS = ("cherrypy==7.1.0", "static3==0.7.0", "Werkzeug==0.11.10")
CONF_API_PASSWORD = "api_password" CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
@ -453,6 +453,10 @@ class HomeAssistantView(object):
"""Handle request to url.""" """Handle request to url."""
from werkzeug.exceptions import MethodNotAllowed, Unauthorized from werkzeug.exceptions import MethodNotAllowed, Unauthorized
if request.method == "OPTIONS":
# For CORS preflight requests.
return self.options(request)
try: try:
handler = getattr(self, request.method.lower()) handler = getattr(self, request.method.lower())
except AttributeError: except AttributeError:
@ -473,16 +477,16 @@ class HomeAssistantView(object):
self.hass.wsgi.api_password): self.hass.wsgi.api_password):
authenticated = True authenticated = True
if authenticated: if self.requires_auth and not authenticated:
_LOGGER.info('Successful login/request from %s',
request.remote_addr)
elif self.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid' _LOGGER.warning('Login attempt or request with an invalid'
'password from %s', request.remote_addr) 'password from %s', request.remote_addr)
raise Unauthorized() raise Unauthorized()
request.authenticated = authenticated request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.remote_addr, authenticated)
result = handler(request, **values) result = handler(request, **values)
if isinstance(result, self.Response): if isinstance(result, self.Response):

View file

@ -6,17 +6,18 @@ https://home-assistant.io/components/hvac/
""" """
import logging import logging
import os import os
from numbers import Number
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELCIUS) TEMP_CELSIUS)
DOMAIN = "hvac" DOMAIN = "hvac"
@ -204,8 +205,8 @@ def setup(hass, config):
return return
for hvac in target_hvacs: for hvac in target_hvacs:
hvac.set_temperature(convert( hvac.set_temperature(convert_temperature(
temperature, hass.config.temperature_unit, temperature, hass.config.units.temperature_unit,
hvac.unit_of_measurement)) hvac.unit_of_measurement))
if hvac.should_poll: if hvac.should_poll:
@ -462,12 +463,12 @@ class HvacDevice(Entity):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return convert(19, TEMP_CELCIUS, self.unit_of_measurement) return convert_temperature(19, TEMP_CELSIUS, self.unit_of_measurement)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return convert(30, TEMP_CELCIUS, self.unit_of_measurement) return convert_temperature(30, TEMP_CELSIUS, self.unit_of_measurement)
@property @property
def min_humidity(self): def min_humidity(self):
@ -481,13 +482,13 @@ class HvacDevice(Entity):
def _convert_for_display(self, temp): def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes.""" """Convert temperature into preferred units for display purposes."""
if temp is None: if temp is None or not isinstance(temp, Number):
return None return temp
value = convert(temp, self.unit_of_measurement, value = convert_temperature(temp, self.unit_of_measurement,
self.hass.config.temperature_unit) self.hass.config.units.temperature_unit)
if self.hass.config.temperature_unit is TEMP_CELCIUS: if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
decimal_count = 1 decimal_count = 1
else: else:
# Users of fahrenheit generally expect integer units. # Users of fahrenheit generally expect integer units.

View file

@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
CONF_SSL = 'ssl' CONF_SSL = 'ssl'
CONF_VERIFY_SSL = 'verify_ssl' CONF_VERIFY_SSL = 'verify_ssl'
CONF_BLACKLIST = 'blacklist' CONF_BLACKLIST = 'blacklist'
CONF_WHITELIST = 'whitelist'
CONF_TAGS = 'tags' CONF_TAGS = 'tags'
@ -57,6 +58,7 @@ def setup(hass, config):
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool, verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
DEFAULT_VERIFY_SSL) DEFAULT_VERIFY_SSL)
blacklist = conf.get(CONF_BLACKLIST, []) blacklist = conf.get(CONF_BLACKLIST, [])
whitelist = conf.get(CONF_WHITELIST, [])
tags = conf.get(CONF_TAGS, {}) tags = conf.get(CONF_TAGS, {})
try: try:
@ -79,6 +81,9 @@ def setup(hass, config):
return return
try: try:
if len(whitelist) > 0 and state.entity_id not in whitelist:
return
_state = state_helper.state_as_number(state) _state = state_helper.state_as_number(state)
except ValueError: except ValueError:
_state = state.state _state = state.state

View file

@ -9,11 +9,12 @@ import logging
import socket import socket
import voluptuous as vol import voluptuous as vol
from homeassistant.components.light import Light from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
Light)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip' REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip'
'#flux_led==0.3'] '#flux_led==0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "flux_led" DOMAIN = "flux_led"
@ -37,7 +38,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
light_ips = [] light_ips = []
for ipaddr, device_config in config["devices"].items(): for ipaddr, device_config in config["devices"].items():
device = {} device = {}
device['id'] = device_config[ATTR_NAME] device['name'] = device_config[ATTR_NAME]
device['ipaddr'] = ipaddr device['ipaddr'] = ipaddr
light = FluxLight(device) light = FluxLight(device)
if light.is_valid: if light.is_valid:
@ -50,11 +51,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
# Find the bulbs on the LAN # Find the bulbs on the LAN
scanner = flux_led.BulbScanner() scanner = flux_led.BulbScanner()
scanner.scan(timeout=20) scanner.scan(timeout=10)
for device in scanner.getBulbInfo(): for device in scanner.getBulbInfo():
light = FluxLight(device)
ipaddr = device['ipaddr'] ipaddr = device['ipaddr']
if light.is_valid and ipaddr not in light_ips: if ipaddr in light_ips:
continue
device['name'] = device['id'] + " " + ipaddr
light = FluxLight(device)
if light.is_valid:
lights.append(light) lights.append(light)
light_ips.append(ipaddr) light_ips.append(ipaddr)
@ -69,7 +73,7 @@ class FluxLight(Light):
"""Initialize the light.""" """Initialize the light."""
import flux_led import flux_led
self._name = device['id'] self._name = device['name']
self._ipaddr = device['ipaddr'] self._ipaddr = device['ipaddr']
self.is_valid = True self.is_valid = True
self._bulb = None self._bulb = None
@ -96,10 +100,28 @@ class FluxLight(Light):
"""Return true if device is on.""" """Return true if device is on."""
return self._bulb.isOn() return self._bulb.isOn()
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._bulb.getWarmWhite255()
@property
def rgb_color(self):
"""Return the color property."""
return self._bulb.getRgb()
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the specified or all lights on.""" """Turn the specified or all lights on."""
if not self.is_on:
self._bulb.turnOn() self._bulb.turnOn()
rgb = kwargs.get(ATTR_RGB_COLOR)
brightness = kwargs.get(ATTR_BRIGHTNESS)
if rgb:
self._bulb.setRgb(*tuple(rgb))
elif brightness:
self._bulb.setWarmWhite255(brightness)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the specified or all lights off.""" """Turn the specified or all lights off."""
self._bulb.turnOff() self._bulb.turnOff()

View file

@ -72,6 +72,11 @@ class Hyperion(Light):
"""Get the remote's active color.""" """Get the remote's active color."""
response = self.json_request({"command": "serverinfo"}) response = self.json_request({"command": "serverinfo"})
if response: if response:
# workaround for outdated Hyperion
if "activeLedColor" not in response["info"]:
self._rgb_color = self._default_color
return
if response["info"]["activeLedColor"] == []: if response["info"]["activeLedColor"] == []:
self._rgb_color = [0, 0, 0] self._rgb_color = [0, 0, 0]
else: else:

View file

@ -4,8 +4,6 @@ Support for the LIFX platform that implements lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.lifx/ https://home-assistant.io/components/light.lifx/
""" """
# pylint: disable=missing-docstring
import colorsys import colorsys
import logging import logging
@ -16,7 +14,6 @@ from homeassistant.helpers.event import track_time_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['liffylights==0.9.4'] REQUIREMENTS = ['liffylights==0.9.4']
DEPENDENCIES = []
CONF_SERVER = "server" # server address configuration item CONF_SERVER = "server" # server address configuration item
CONF_BROADCAST = "broadcast" # broadcast address configuration item CONF_BROADCAST = "broadcast" # broadcast address configuration item
@ -94,11 +91,11 @@ class LIFX():
# pylint: disable=unused-argument # pylint: disable=unused-argument
def poll(self, now): def poll(self, now):
"""Initialize the light.""" """Polling for the light."""
self.probe() self.probe()
def probe(self, address=None): def probe(self, address=None):
"""Initialize the light.""" """Probe the light."""
self._liffylights.probe(address) self._liffylights.probe(address)

View file

@ -0,0 +1,233 @@
"""
Support for MQTT JSON lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt_json/
"""
import logging
import json
import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, Light)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_PLATFORM
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = "mqtt_json"
DEPENDENCIES = ["mqtt"]
DEFAULT_NAME = "MQTT JSON Light"
DEFAULT_OPTIMISTIC = False
DEFAULT_BRIGHTNESS = False
DEFAULT_RGB = False
DEFAULT_FLASH_TIME_SHORT = 2
DEFAULT_FLASH_TIME_LONG = 10
CONF_BRIGHTNESS = "brightness"
CONF_RGB = "rgb"
CONF_FLASH_TIME_SHORT = "flash_time_short"
CONF_FLASH_TIME_LONG = "flash_time_long"
# Stealing some of these from the base MQTT configs.
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
vol.Optional(CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT):
cv.positive_int,
vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG):
cv.positive_int
})
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup a MQTT JSON Light."""
add_devices_callback([MqttJson(
hass,
config[CONF_NAME],
{
key: config.get(key) for key in (
CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC
)
},
config[CONF_QOS],
config[CONF_RETAIN],
config[CONF_OPTIMISTIC],
config[CONF_BRIGHTNESS],
config[CONF_RGB],
{
key: config.get(key) for key in (
CONF_FLASH_TIME_SHORT,
CONF_FLASH_TIME_LONG
)
}
)])
class MqttJson(Light):
"""Representation of a MQTT JSON light."""
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(self, hass, name, topic, qos, retain,
optimistic, brightness, rgb, flash_times):
"""Initialize MQTT JSON light."""
self._hass = hass
self._name = name
self._topic = topic
self._qos = qos
self._retain = retain
self._optimistic = optimistic or topic["state_topic"] is None
self._state = False
if brightness:
self._brightness = 255
else:
self._brightness = None
if rgb:
self._rgb = [0, 0, 0]
else:
self._rgb = None
self._flash_times = flash_times
def state_received(topic, payload, qos):
"""A new MQTT message has been received."""
values = json.loads(payload)
if values["state"] == "ON":
self._state = True
elif values["state"] == "OFF":
self._state = False
if self._rgb is not None:
try:
red = int(values["color"]["r"])
green = int(values["color"]["g"])
blue = int(values["color"]["b"])
self._rgb = [red, green, blue]
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid color value received.")
if self._brightness is not None:
try:
self._brightness = int(values["brightness"])
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid brightness value received.")
self.update_ha_state()
if self._topic["state_topic"] is not None:
mqtt.subscribe(self._hass, self._topic["state_topic"],
state_received, self._qos)
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
def rgb_color(self):
"""Return the RGB color value."""
return self._rgb
@property
def should_poll(self):
"""No polling needed for a MQTT light."""
return False
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def is_on(self):
"""Return true if device is on."""
return self._state
@property
def assumed_state(self):
"""Return true if we do optimistic updates."""
return self._optimistic
def turn_on(self, **kwargs):
"""Turn the device on."""
should_update = False
message = {"state": "ON"}
if ATTR_RGB_COLOR in kwargs:
message["color"] = {
"r": kwargs[ATTR_RGB_COLOR][0],
"g": kwargs[ATTR_RGB_COLOR][1],
"b": kwargs[ATTR_RGB_COLOR][2]
}
if self._optimistic:
self._rgb = kwargs[ATTR_RGB_COLOR]
should_update = True
if ATTR_FLASH in kwargs:
flash = kwargs.get(ATTR_FLASH)
if flash == FLASH_LONG:
message["flash"] = self._flash_times[CONF_FLASH_TIME_LONG]
elif flash == FLASH_SHORT:
message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT]
if ATTR_TRANSITION in kwargs:
message["transition"] = kwargs[ATTR_TRANSITION]
if ATTR_BRIGHTNESS in kwargs:
message["brightness"] = int(kwargs[ATTR_BRIGHTNESS])
if self._optimistic:
self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True
mqtt.publish(self._hass, self._topic["command_topic"],
json.dumps(message), self._qos, self._retain)
if self._optimistic:
# Optimistically assume that the light has changed state.
self._state = True
should_update = True
if should_update:
self.update_ha_state()
def turn_off(self, **kwargs):
"""Turn the device off."""
message = {"state": "OFF"}
if ATTR_TRANSITION in kwargs:
message["transition"] = kwargs[ATTR_TRANSITION]
mqtt.publish(self._hass, self._topic["command_topic"],
json.dumps(message), self._qos, self._retain)
if self._optimistic:
# Optimistically assume that the light has changed state.
self._state = False
self.update_ha_state()

View file

@ -22,6 +22,7 @@ from homeassistant.components import group
DOMAIN = 'lock' DOMAIN = 'lock'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
ATTR_CHANGED_BY = 'changed_by'
GROUP_NAME_ALL_LOCKS = 'all locks' GROUP_NAME_ALL_LOCKS = 'all locks'
ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks') ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
@ -101,6 +102,11 @@ def setup(hass, config):
class LockDevice(Entity): class LockDevice(Entity):
"""Representation of a lock.""" """Representation of a lock."""
@property
def changed_by(self):
"""Last change triggered by."""
return None
# pylint: disable=no-self-use # pylint: disable=no-self-use
@property @property
def code_format(self): def code_format(self):
@ -127,6 +133,7 @@ class LockDevice(Entity):
return None return None
state_attr = { state_attr = {
ATTR_CODE_FORMAT: self.code_format, ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
} }
return state_attr return state_attr

View file

@ -0,0 +1,21 @@
lock:
description: Lock all or specified locks
fields:
entity_id:
description: Name of lock to lock
example: 'lock.front_door'
code:
description: An optional code to lock the lock with
example: 1234
unlock:
description: Unlock all or specified locks
fields:
entity_id:
description: Name of lock to unlock
example: 'lock.front_door'
code:
description: An optional code to unlock the lock with
example: 1234

View file

@ -35,6 +35,7 @@ class VerisureDoorlock(LockDevice):
self._id = device_id self._id = device_id
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._digits = int(hub.config.get('code_digits', '4')) self._digits = int(hub.config.get('code_digits', '4'))
self._changed_by = None
@property @property
def name(self): def name(self):
@ -51,6 +52,11 @@ class VerisureDoorlock(LockDevice):
"""Return True if entity is available.""" """Return True if entity is available."""
return hub.available return hub.available
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
@property @property
def code_format(self): def code_format(self):
"""Return the required six digit code.""" """Return the required six digit code."""
@ -68,6 +74,7 @@ class VerisureDoorlock(LockDevice):
_LOGGER.error( _LOGGER.error(
'Unknown lock state %s', 'Unknown lock state %s',
hub.lock_status[self._id].status) hub.lock_status[self._id].status)
self._changed_by = hub.lock_status[self._id].name
@property @property
def is_locked(self): def is_locked(self):

View file

@ -18,10 +18,8 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START, from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON) STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
from homeassistant.core import State
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'frontend'] DEPENDENCIES = ['recorder', 'frontend']
@ -196,6 +194,11 @@ def humanify(events):
event != last_sensor_event[to_state.entity_id]: event != last_sensor_event[to_state.entity_id]:
continue continue
# Don't show continuous sensor value changes in the logbook
if domain == 'sensor' and \
to_state.attributes.get('unit_of_measurement'):
continue
yield Entry( yield Entry(
event.time_fired, event.time_fired,
name=to_state.name, name=to_state.name,

View file

@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player/
""" """
import logging import logging
import os import os
import requests
import voluptuous as vol import voluptuous as vol
@ -13,6 +14,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
@ -25,10 +27,13 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'media_player' DOMAIN = 'media_player'
DEPENDENCIES = ['http']
SCAN_INTERVAL = 10 SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}'
SERVICE_PLAY_MEDIA = 'play_media' SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source' SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist' SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
@ -286,6 +291,8 @@ def setup(hass, config):
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(MediaPlayerImageView(hass, component.entities))
component.setup(config) component.setup(config)
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
@ -398,6 +405,11 @@ class MediaPlayerDevice(Entity):
"""State of the player.""" """State of the player."""
return STATE_UNKNOWN return STATE_UNKNOWN
@property
def access_token(self):
"""Access token for this media player."""
return str(id(self))
@property @property
def volume_level(self): def volume_level(self):
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
@ -633,7 +645,8 @@ class MediaPlayerDevice(Entity):
@property @property
def entity_picture(self): def entity_picture(self):
"""Return image of the media playing.""" """Return image of the media playing."""
return None if self.state == STATE_OFF else self.media_image_url return None if self.state == STATE_OFF else \
ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
@property @property
def state_attributes(self): def state_attributes(self):
@ -649,3 +662,39 @@ class MediaPlayerDevice(Entity):
} }
return state_attr return state_attr
class MediaPlayerImageView(HomeAssistantView):
"""Media player view to serve an image."""
url = "/api/media_player_proxy/<entity(domain=media_player):entity_id>"
name = "api:media_player:image"
def __init__(self, hass, entities):
"""Initialize a media player view."""
super().__init__(hass)
self.entities = entities
def get(self, request, entity_id):
"""Start a get request."""
player = self.entities.get(entity_id)
if player is None:
return self.Response(status=404)
authenticated = (request.authenticated or
request.args.get('token') == player.access_token)
if not authenticated:
return self.Response(status=401)
image_url = player.media_image_url
if image_url:
response = requests.get(image_url)
else:
response = None
if response is None:
return self.Response(status=500)
return self.Response(response)

View file

@ -41,9 +41,12 @@ def _get_mac_address(ip_address):
pid = Popen(["arp", "-n", ip_address], stdout=PIPE) pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
pid_component = pid.communicate()[0] pid_component = pid.communicate()[0]
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'), match = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
pid_component).groups()[0] pid_component)
return mac if match is not None:
return match.groups()[0]
else:
return None
def _config_from_file(filename, config=None): def _config_from_file(filename, config=None):

View file

@ -163,6 +163,11 @@ class LgTVDevice(MediaPlayerDevice):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return SUPPORT_LGTV return SUPPORT_LGTV
@property
def media_image_url(self):
"""URL for obtaining a screen capture."""
return self._client.url + 'data?target=screen_image'
def turn_off(self): def turn_off(self):
"""Turn off media player.""" """Turn off media player."""
self.send_command(1) self.send_command(1)

View file

@ -12,15 +12,16 @@ from urllib.parse import urlparse
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice) SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN) STATE_UNKNOWN)
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers.event import (track_utc_time_change) from homeassistant.helpers.event import (track_utc_time_change)
REQUIREMENTS = ['plexapi==1.1.0'] REQUIREMENTS = ['plexapi==2.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
@ -30,7 +31,8 @@ PLEX_CONFIG_FILE = 'plex.conf'
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_VOLUME_SET
def config_from_file(filename, config=None): def config_from_file(filename, config=None):
@ -193,6 +195,9 @@ class PlexClient(MediaPlayerDevice):
# pylint: disable=too-many-public-methods, attribute-defined-outside-init # pylint: disable=too-many-public-methods, attribute-defined-outside-init
def __init__(self, device, plex_sessions, update_devices, update_sessions): def __init__(self, device, plex_sessions, update_devices, update_sessions):
"""Initialize the Plex device.""" """Initialize the Plex device."""
from plexapi.utils import NA
self.na_type = NA
self.plex_sessions = plex_sessions self.plex_sessions = plex_sessions
self.update_devices = update_devices self.update_devices = update_devices
self.update_sessions = update_sessions self.update_sessions = update_sessions
@ -211,20 +216,17 @@ class PlexClient(MediaPlayerDevice):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self.device.name or DEVICE_DEFAULT_NAME return self.device.title or DEVICE_DEFAULT_NAME
@property @property
def session(self): def session(self):
"""Return the session, if any.""" """Return the session, if any."""
if self.device.machineIdentifier not in self.plex_sessions: return self.plex_sessions.get(self.device.machineIdentifier, None)
return None
return self.plex_sessions[self.device.machineIdentifier]
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
if self.session: if self.session and self.session.player:
state = self.session.player.state state = self.session.player.state
if state == 'playing': if state == 'playing':
return STATE_PLAYING return STATE_PLAYING
@ -243,11 +245,30 @@ class PlexClient(MediaPlayerDevice):
self.update_devices(no_throttle=True) self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True) self.update_sessions(no_throttle=True)
# pylint: disable=no-self-use, singleton-comparison
def _convert_na_to_none(self, value):
"""Convert PlexAPI _NA() instances to None."""
# PlexAPI will return a "__NA__" object which can be compared to
# None, but isn't actually None - this converts it to a real None
# type so that lower layers don't think it's a URL and choke on it
if value is self.na_type:
return None
else:
return value
@property
def _active_media_plexapi_type(self):
"""Get the active media type required by PlexAPI commands."""
if self.media_content_type is MEDIA_TYPE_MUSIC:
return 'music'
else:
return 'video'
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
if self.session is not None: if self.session is not None:
return self.session.ratingKey return self._convert_na_to_none(self.session.ratingKey)
@property @property
def media_content_type(self): def media_content_type(self):
@ -259,65 +280,82 @@ class PlexClient(MediaPlayerDevice):
return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_TVSHOW
elif media_type == 'movie': elif media_type == 'movie':
return MEDIA_TYPE_VIDEO return MEDIA_TYPE_VIDEO
elif media_type == 'track':
return MEDIA_TYPE_MUSIC
return None return None
@property @property
def media_duration(self): def media_duration(self):
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
if self.session is not None: if self.session is not None:
return self.session.duration return self._convert_na_to_none(self.session.duration)
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
if self.session is not None: if self.session is not None:
return self.session.thumbUrl thumb_url = self._convert_na_to_none(self.session.thumbUrl)
if str(self.na_type) in thumb_url:
# Audio tracks build their thumb urls internally before passing
# back a URL with the PlexAPI _NA type already converted to a
# string and embedded into a malformed URL
thumb_url = None
return thumb_url
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
# find a string we can use as a title # find a string we can use as a title
if self.session is not None: if self.session is not None:
return self.session.title return self._convert_na_to_none(self.session.title)
@property @property
def media_season(self): def media_season(self):
"""Season of curent playing media (TV Show only).""" """Season of curent playing media (TV Show only)."""
from plexapi.video import Show from plexapi.video import Show
if isinstance(self.session, Show): if isinstance(self.session, Show):
return self.session.seasons()[0].index return self._convert_na_to_none(self.session.seasons()[0].index)
@property @property
def media_series_title(self): def media_series_title(self):
"""The title of the series of current playing media (TV Show only).""" """The title of the series of current playing media (TV Show only)."""
from plexapi.video import Show from plexapi.video import Show
if isinstance(self.session, Show): if isinstance(self.session, Show):
return self.session.grandparentTitle return self._convert_na_to_none(self.session.grandparentTitle)
@property @property
def media_episode(self): def media_episode(self):
"""Episode of current playing media (TV Show only).""" """Episode of current playing media (TV Show only)."""
from plexapi.video import Show from plexapi.video import Show
if isinstance(self.session, Show): if isinstance(self.session, Show):
return self.session.index return self._convert_na_to_none(self.session.index)
@property @property
def supported_media_commands(self): def supported_media_commands(self):
"""Flag of media commands that are supported.""" """Flag of media commands that are supported."""
return SUPPORT_PLEX return SUPPORT_PLEX
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self.device.setVolume(int(volume * 100),
self._active_media_plexapi_type)
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
self.device.play() self.device.play(self._active_media_plexapi_type)
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
self.device.pause() self.device.pause(self._active_media_plexapi_type)
def media_stop(self):
"""Send stop command."""
self.device.stop(self._active_media_plexapi_type)
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """Send next track command."""
self.device.skipNext() self.device.skipNext(self._active_media_plexapi_type)
def media_previous_track(self): def media_previous_track(self):
"""Send previous track command.""" """Send previous track command."""
self.device.skipPrevious() self.device.skipPrevious(self._active_media_plexapi_type)

View file

@ -88,7 +88,8 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None self.current_app = None
except (requests.exceptions.ConnectionError, except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout): requests.exceptions.ReadTimeout):
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
pass
def get_source_list(self): def get_source_list(self):
"""Get the list of applications to be used as sources.""" """Get the list of applications to be used as sources."""

View file

@ -10,7 +10,7 @@ import socket
from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC, from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
STATE_OFF, STATE_ON, TEMP_CELSIUS) STATE_OFF, STATE_ON)
from homeassistant.helpers import validate_config, discovery from homeassistant.helpers import validate_config, discovery
CONF_GATEWAYS = 'gateways' CONF_GATEWAYS = 'gateways'
@ -53,7 +53,7 @@ def setup(hass, config): # pylint: disable=too-many-locals
import mysensors.mysensors as mysensors import mysensors.mysensors as mysensors
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
is_metric = (hass.config.temperature_unit == TEMP_CELSIUS) is_metric = hass.config.units.is_metric
persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) persistence = config[DOMAIN].get(CONF_PERSISTENCE, True)
def setup_gateway(device, persistence_file, baud_rate, tcp_port): def setup_gateway(device, persistence_file, baud_rate, tcp_port):

View file

@ -44,16 +44,19 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def send_message(hass, message, title=None): def send_message(hass, message, title=None, data=None):
"""Send a notification message.""" """Send a notification message."""
data = { info = {
ATTR_MESSAGE: message ATTR_MESSAGE: message
} }
if title is not None: if title is not None:
data[ATTR_TITLE] = title info[ATTR_TITLE] = title
hass.services.call(DOMAIN, SERVICE_NOTIFY, data) if data is not None:
info[ATTR_DATA] = data
hass.services.call(DOMAIN, SERVICE_NOTIFY, info)
def setup(hass, config): def setup(hass, config):

View file

@ -4,7 +4,7 @@ Demo notification service.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.notify import ATTR_TITLE, BaseNotificationService from homeassistant.components.notify import BaseNotificationService
EVENT_NOTIFY = "notify" EVENT_NOTIFY = "notify"
@ -24,5 +24,5 @@ class DemoNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""
title = kwargs.get(ATTR_TITLE) kwargs['message'] = message
self.hass.bus.fire(EVENT_NOTIFY, {"title": title, "message": message}) self.hass.bus.fire(EVENT_NOTIFY, kwargs)

View file

@ -10,7 +10,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService) ATTR_TITLE, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
REQUIREMENTS = ['sendgrid==3.0.7'] REQUIREMENTS = ['sendgrid==3.1.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -6,14 +6,18 @@ https://home-assistant.io/components/notify.smtp/
""" """
import logging import logging
import smtplib import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService) ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
def get_service(hass, config): def get_service(hass, config):
"""Get the mail notification service.""" """Get the mail notification service."""
@ -22,52 +26,21 @@ def get_service(hass, config):
_LOGGER): _LOGGER):
return None return None
smtp_server = config.get('server', 'localhost') mail_service = MailNotificationService(
port = int(config.get('port', '25')) config.get('server', 'localhost'),
username = config.get('username', None) int(config.get('port', '25')),
password = config.get('password', None) config.get('sender', None),
starttls = int(config.get('starttls', 0)) int(config.get('starttls', 0)),
debug = config.get('debug', 0) config.get('username', None),
config.get('password', None),
config.get('recipient', None),
config.get('debug', 0))
server = None if mail_service.connection_is_valid():
try: return mail_service
server = smtplib.SMTP(smtp_server, port, timeout=5) else:
server.set_debuglevel(debug)
server.ehlo()
if starttls == 1:
server.starttls()
server.ehlo()
if username and password:
try:
server.login(username, password)
except (smtplib.SMTPException, smtplib.SMTPSenderRefused):
_LOGGER.exception("Please check your settings.")
return None return None
except smtplib.socket.gaierror:
_LOGGER.exception(
"SMTP server not found (%s:%s). "
"Please check the IP address or hostname of your SMTP server.",
smtp_server, port)
return None
except smtplib.SMTPAuthenticationError:
_LOGGER.exception(
"Login not possible. "
"Please check your setting and/or your credentials.")
return None
finally:
if server:
server.quit()
return MailNotificationService(
smtp_server, port, config['sender'], starttls, username, password,
config['recipient'], debug)
# pylint: disable=too-few-public-methods, too-many-instance-attributes # pylint: disable=too-few-public-methods, too-many-instance-attributes
class MailNotificationService(BaseNotificationService): class MailNotificationService(BaseNotificationService):
@ -99,17 +72,57 @@ class MailNotificationService(BaseNotificationService):
mail.login(self.username, self.password) mail.login(self.username, self.password)
return mail return mail
def send_message(self, message="", **kwargs): def connection_is_valid(self):
"""Send a message to a user.""" """Check for valid config, verify connectivity."""
mail = self.connect() server = None
subject = kwargs.get(ATTR_TITLE) try:
server = self.connect()
except smtplib.socket.gaierror:
_LOGGER.exception(
"SMTP server not found (%s:%s). "
"Please check the IP address or hostname of your SMTP server.",
self._server, self._port)
return False
except (smtplib.SMTPAuthenticationError, ConnectionRefusedError):
_LOGGER.exception(
"Login not possible. "
"Please check your setting and/or your credentials.")
return False
finally:
if server:
server.quit()
return True
def send_message(self, message="", **kwargs):
"""
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.
"""
subject = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA)
if data:
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES))
else:
msg = _build_text_msg(message)
msg = MIMEText(message)
msg['Subject'] = subject msg['Subject'] = subject
msg['To'] = self.recipient msg['To'] = self.recipient
msg['From'] = self._sender msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant' msg['X-Mailer'] = 'HomeAssistant'
return self._send_email(msg)
def _send_email(self, msg):
"""Send the message."""
mail = self.connect()
for _ in range(self.tries): for _ in range(self.tries):
try: try:
mail.sendmail(self._sender, self.recipient, mail.sendmail(self._sender, self.recipient,
@ -122,3 +135,36 @@ class MailNotificationService(BaseNotificationService):
mail = self.connect() mail = self.connect()
mail.quit() mail.quit()
def _build_text_msg(message):
"""Build plaintext email."""
_LOGGER.debug('Building plain text email.')
return MIMEText(message)
def _build_multipart_msg(message, images):
"""Build Multipart message with in-line images."""
_LOGGER.debug('Building multipart email with embedded attachment(s).')
msg = MIMEMultipart('related')
msg_alt = MIMEMultipart('alternative')
msg.attach(msg_alt)
body_txt = MIMEText(message)
msg_alt.attach(body_txt)
body_text = ['<p>{}</p><br>'.format(message)]
for atch_num, atch_name in enumerate(images):
cid = 'image{}'.format(atch_num)
body_text.append('<img src="cid:{}"><br>'.format(cid))
try:
with open(atch_name, 'rb') as attachment_file:
attachment = MIMEImage(attachment_file.read())
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(cid))
except FileNotFoundError:
_LOGGER.warning('Attachment %s not found. Skipping.',
atch_name)
body_html = MIMEText(''.join(body_text), 'html')
msg_alt.attach(body_html)
return msg

View file

@ -0,0 +1,69 @@
"""
Register a custom front end panel.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/panel_custom/
"""
import logging
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_panel
DOMAIN = 'panel_custom'
DEPENDENCIES = ['frontend']
CONF_COMPONENT_NAME = 'name'
CONF_SIDEBAR_TITLE = 'sidebar_title'
CONF_SIDEBAR_ICON = 'sidebar_icon'
CONF_URL_PATH = 'url_path'
CONF_CONFIG = 'config'
CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
DEFAULT_ICON = 'mdi:bookmark'
PANEL_DIR = 'panels'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [{
vol.Required(CONF_COMPONENT_NAME): cv.slug,
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
vol.Optional(CONF_URL_PATH): cv.string,
vol.Optional(CONF_CONFIG): cv.match_all,
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
}])
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Initialize custom panel."""
success = False
for panel in config.get(DOMAIN):
name = panel.get(CONF_COMPONENT_NAME)
panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
if panel_path is None:
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
if not os.path.isfile(panel_path):
_LOGGER.error('Unable to find webcomponent for %s: %s',
name, panel_path)
continue
register_panel(
hass, name, panel_path,
sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
url_path=panel.get(CONF_URL_PATH),
config=panel.get(CONF_CONFIG),
)
success = True
return success

View file

@ -23,9 +23,9 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup iframe frontend panels.""" """Setup iframe frontend panels."""
for url_name, info in config[DOMAIN].items(): for url_path, info in config[DOMAIN].items():
register_built_in_panel( register_built_in_panel(
hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_name, {'url': info[CONF_URL]}) url_path, {'url': info[CONF_URL]})
return True return True

View file

@ -0,0 +1,109 @@
"""
Component to create an interface to a Pilight daemon (https://pilight.org/).
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/pilight/
"""
# pylint: disable=import-error
import logging
import socket
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ensure_list
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.const import CONF_HOST, CONF_PORT
REQUIREMENTS = ['pilight==0.0.2']
DOMAIN = "pilight"
EVENT = 'pilight_received'
SERVICE_NAME = 'send'
CONF_WHITELIST = 'whitelist'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST, default='127.0.0.1'): cv.string,
vol.Required(CONF_PORT, default=5000): vol.Coerce(int),
vol.Optional(CONF_WHITELIST): {cv.string: [cv.string]}
}),
}, extra=vol.ALLOW_EXTRA)
# The pilight code schema depends on the protocol
# Thus only require to have the protocol information
ATTR_PROTOCOL = 'protocol'
RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): cv.string},
extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Setup the pilight component."""
from pilight import pilight
try:
pilight_client = pilight.Client(host=config[DOMAIN][CONF_HOST],
port=config[DOMAIN][CONF_PORT])
except (socket.error, socket.timeout) as err:
_LOGGER.error(
"Unable to connect to %s on port %s: %s",
config[CONF_HOST], config[CONF_PORT], err)
return False
# Start / stop pilight-daemon connection with HA start/stop
def start_pilight_client(_):
"""Called once when home assistant starts."""
pilight_client.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_pilight_client)
def stop_pilight_client(_):
"""Called once when home assistant stops."""
pilight_client.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_pilight_client)
def send_code(call):
"""Send RF code to the pilight-daemon."""
message_data = call.data
# Patch data because of bug:
# https://github.com/pilight/pilight/issues/296
# Protocol has to be in a list otherwise segfault in pilight-daemon
message_data["protocol"] = ensure_list(message_data["protocol"])
try:
pilight_client.send_code(message_data)
except IOError:
_LOGGER.error('Pilight send failed for %s', str(message_data))
hass.services.register(DOMAIN, SERVICE_NAME,
send_code, schema=RF_CODE_SCHEMA)
# Publish received codes on the HA event bus
# A whitelist of codes to be published in the event bus
whitelist = config[DOMAIN].get('whitelist', False)
def handle_received_code(data):
"""Called when RF codes are received."""
# Unravel dict of dicts to make event_data cut in automation rule
# possible
data = dict(
{'protocol': data['protocol'],
'uuid': data['uuid']},
**data['message'])
# No whitelist defined, put data on event bus
if not whitelist:
hass.bus.fire(EVENT, data)
# Check if data matches the defined whitelist
elif all(data[key] in whitelist[key] for key in whitelist):
hass.bus.fire(EVENT, data)
pilight_client.set_callback(handle_received_code)
return True

View file

@ -12,17 +12,30 @@ import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.util.location import distance from homeassistant.util.location import distance
from homeassistant.util.distance import convert
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
DEPENDENCIES = ['zone', 'device_tracker'] DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity' DOMAIN = 'proximity'
NOT_SET = 'not set'
# Default tolerance # Default tolerance
DEFAULT_TOLERANCE = 1 DEFAULT_TOLERANCE = 1
# Default zone # Default zone
DEFAULT_PROXIMITY_ZONE = 'home' DEFAULT_PROXIMITY_ZONE = 'home'
# Default distance to zone
DEFAULT_DIST_TO_ZONE = NOT_SET
# Default direction of travel
DEFAULT_DIR_OF_TRAVEL = NOT_SET
# Default nearest device
DEFAULT_NEAREST = NOT_SET
# Entity attributes # Entity attributes
ATTR_DIST_FROM = 'dist_to_zone' ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel' ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
@ -31,43 +44,41 @@ ATTR_NEAREST = 'nearest'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements def setup_proximity_component(hass, config):
"""Get the zones and offsets from configuration.yaml.""" """Set up individual proximity component."""
ignored_zones = []
if 'ignored_zones' in config[DOMAIN]:
for variable in config[DOMAIN]['ignored_zones']:
ignored_zones.append(variable)
# Get the devices from configuration.yaml. # Get the devices from configuration.yaml.
if 'devices' not in config[DOMAIN]: if 'devices' not in config:
_LOGGER.error('devices not found in config') _LOGGER.error('devices not found in config')
return False return False
ignored_zones = []
if 'ignored_zones' in config:
for variable in config['ignored_zones']:
ignored_zones.append(variable)
proximity_devices = [] proximity_devices = []
for variable in config[DOMAIN]['devices']: for variable in config['devices']:
proximity_devices.append(variable) proximity_devices.append(variable)
# Get the direction of travel tolerance from configuration.yaml. # Get the direction of travel tolerance from configuration.yaml.
tolerance = config[DOMAIN].get('tolerance', DEFAULT_TOLERANCE) tolerance = config.get('tolerance', DEFAULT_TOLERANCE)
# Get the zone to monitor proximity to from configuration.yaml. # Get the zone to monitor proximity to from configuration.yaml.
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE) proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
entity_id = DOMAIN + '.' + proximity_zone # Get the unit of measurement from configuration.yaml.
proximity_zone = 'zone.' + proximity_zone unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT,
hass.config.units.length_unit)
state = hass.states.get(proximity_zone) zone_id = 'zone.{}'.format(proximity_zone)
state = hass.states.get(zone_id)
zone_friendly_name = (state.name).lower() zone_friendly_name = (state.name).lower()
# Set the default values. proximity = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
dist_to_zone = 'not set' DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
dir_of_travel = 'not set' ignored_zones, proximity_devices, tolerance,
nearest = 'not set' zone_id, unit_of_measure)
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
proximity = Proximity(hass, zone_friendly_name, dist_to_zone,
dir_of_travel, nearest, ignored_zones,
proximity_devices, tolerance, proximity_zone)
proximity.entity_id = entity_id
proximity.update_ha_state() proximity.update_ha_state()
@ -78,13 +89,26 @@ def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
return True return True
def setup(hass, config):
"""Get the zones and offsets from configuration.yaml."""
result = True
if isinstance(config[DOMAIN], list):
for proximity_config in config[DOMAIN]:
if not setup_proximity_component(hass, proximity_config):
result = False
elif not setup_proximity_component(hass, config[DOMAIN]):
result = False
return result
class Proximity(Entity): # pylint: disable=too-many-instance-attributes class Proximity(Entity): # pylint: disable=too-many-instance-attributes
"""Representation of a Proximity.""" """Representation of a Proximity."""
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel, def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance, nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone): proximity_zone, unit_of_measure):
"""Initialize the proximity.""" """Initialize the proximity."""
self.hass = hass self.hass = hass
self.friendly_name = zone_friendly_name self.friendly_name = zone_friendly_name
@ -95,6 +119,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
self.proximity_devices = proximity_devices self.proximity_devices = proximity_devices
self.tolerance = tolerance self.tolerance = tolerance
self.proximity_zone = proximity_zone self.proximity_zone = proximity_zone
self.unit_of_measure = unit_of_measure
@property @property
def name(self): def name(self):
@ -109,7 +134,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement of this entity.""" """Return the unit of measurement of this entity."""
return "km" return self.unit_of_measure
@property @property
def state_attributes(self): def state_attributes(self):
@ -183,15 +208,16 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
device_state.attributes['longitude']) device_state.attributes['longitude'])
# Add the device and distance to a dictionary. # Add the device and distance to a dictionary.
distances_to_zone[device] = round(dist_to_zone / 1000, 1) distances_to_zone[device] = round(
convert(dist_to_zone, 'm', self.unit_of_measure), 1)
# Loop through each of the distances collected and work out the # Loop through each of the distances collected and work out the
# closest. # closest.
closest_device = '' closest_device = None # type: str
dist_to_zone = 1000000 dist_to_zone = None # type: float
for device in distances_to_zone: for device in distances_to_zone:
if distances_to_zone[device] < dist_to_zone: if not dist_to_zone or distances_to_zone[device] < dist_to_zone:
closest_device = device closest_device = device
dist_to_zone = distances_to_zone[device] dist_to_zone = distances_to_zone[device]

View file

@ -11,15 +11,18 @@ import logging
import queue import queue
import threading import threading
import time import time
from datetime import timedelta from datetime import timedelta, datetime
from typing import Any, Union, Optional, List
import voluptuous as vol import voluptuous as vol
import homeassistant.util.dt as dt_util from homeassistant.core import HomeAssistant
from homeassistant.const import (EVENT_HOMEASSISTANT_START, from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL) EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, QueryType
import homeassistant.util.dt as dt_util
DOMAIN = "recorder" DOMAIN = "recorder"
@ -44,15 +47,16 @@ CONFIG_SCHEMA = vol.Schema({
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
_INSTANCE = None _INSTANCE = None # type: Any
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# These classes will be populated during setup() # These classes will be populated during setup()
# pylint: disable=invalid-name # pylint: disable=invalid-name,no-member
Session = None Session = None # pylint: disable=no-member
def execute(q): def execute(q: QueryType) \
-> List[Any]: # pylint: disable=invalid-sequence-index
"""Query the database and convert the objects to HA native form. """Query the database and convert the objects to HA native form.
This method also retries a few times in the case of stale connections. This method also retries a few times in the case of stale connections.
@ -68,11 +72,11 @@ def execute(q):
except sqlalchemy.exc.SQLAlchemyError as e: except sqlalchemy.exc.SQLAlchemyError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True) log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
finally: finally:
Session().close() Session.close()
return [] return []
def run_information(point_in_time=None): def run_information(point_in_time: Optional[datetime]=None):
"""Return information about current run. """Return information about current run.
There is also the run that covers point_in_time. There is also the run that covers point_in_time.
@ -91,7 +95,7 @@ def run_information(point_in_time=None):
(recorder_runs.end > point_in_time)).first() (recorder_runs.end > point_in_time)).first()
def setup(hass, config): def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Setup the recorder.""" """Setup the recorder."""
# pylint: disable=global-statement # pylint: disable=global-statement
global _INSTANCE global _INSTANCE
@ -112,30 +116,36 @@ def setup(hass, config):
return True return True
def query(model_name, *args): def query(model_name: Union[str, Any], *args) -> QueryType:
"""Helper to return a query handle.""" """Helper to return a query handle."""
_verify_instance()
if isinstance(model_name, str): if isinstance(model_name, str):
return Session().query(get_model(model_name), *args) return Session.query(get_model(model_name), *args)
return Session().query(model_name, *args) return Session.query(model_name, *args)
def get_model(model_name): def get_model(model_name: str) -> Any:
"""Get a model class.""" """Get a model class."""
from homeassistant.components.recorder import models from homeassistant.components.recorder import models
try:
return getattr(models, model_name) return getattr(models, model_name)
except AttributeError:
_LOGGER.error("Invalid model name %s", model_name)
return None
def log_error(e, retry_wait=0, rollback=True, def log_error(e: Exception, retry_wait: Optional[float]=0,
message="Error during query: %s"): rollback: Optional[bool]=True,
message: Optional[str]="Error during query: %s") -> None:
"""Log about SQLAlchemy errors in a sane manner.""" """Log about SQLAlchemy errors in a sane manner."""
import sqlalchemy.exc import sqlalchemy.exc
if not isinstance(e, sqlalchemy.exc.OperationalError): if not isinstance(e, sqlalchemy.exc.OperationalError):
_LOGGER.exception(e) _LOGGER.exception(str(e))
else: else:
_LOGGER.error(message, str(e)) _LOGGER.error(message, str(e))
if rollback: if rollback:
Session().rollback() Session.rollback()
if retry_wait: if retry_wait:
_LOGGER.info("Retrying in %s seconds", retry_wait) _LOGGER.info("Retrying in %s seconds", retry_wait)
time.sleep(retry_wait) time.sleep(retry_wait)
@ -145,19 +155,20 @@ class Recorder(threading.Thread):
"""A threaded recorder class.""" """A threaded recorder class."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, hass, purge_days, uri): def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) \
-> None:
"""Initialize the recorder.""" """Initialize the recorder."""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.hass = hass self.hass = hass
self.purge_days = purge_days self.purge_days = purge_days
self.queue = queue.Queue() self.queue = queue.Queue() # type: Any
self.quit_object = object() self.quit_object = object()
self.recording_start = dt_util.utcnow() self.recording_start = dt_util.utcnow()
self.db_url = uri self.db_url = uri
self.db_ready = threading.Event() self.db_ready = threading.Event()
self.engine = None self.engine = None # type: Any
self._run = None self._run = None # type: Any
def start_recording(event): def start_recording(event):
"""Start recording.""" """Start recording."""
@ -276,7 +287,7 @@ class Recorder(threading.Thread):
run.end = self.recording_start run.end = self.recording_start
_LOGGER.warning("Ended unfinished session (id=%s from %s)", _LOGGER.warning("Ended unfinished session (id=%s from %s)",
run.run_id, run.start) run.run_id, run.start)
Session().add(run) Session.add(run)
_LOGGER.warning("Found unfinished sessions") _LOGGER.warning("Found unfinished sessions")
@ -321,7 +332,7 @@ class Recorder(threading.Thread):
if self._commit(_purge_events): if self._commit(_purge_events):
_LOGGER.info("Purged events created before %s", purge_before) _LOGGER.info("Purged events created before %s", purge_before)
Session().expire_all() Session.expire_all()
# Execute sqlite vacuum command to free up space on disk # Execute sqlite vacuum command to free up space on disk
if self.engine.driver == 'sqlite': if self.engine.driver == 'sqlite':
@ -346,7 +357,8 @@ class Recorder(threading.Thread):
return False return False
def _verify_instance(): def _verify_instance() -> None:
"""Throw error if recorder not initialized.""" """Throw error if recorder not initialized."""
if _INSTANCE is None: if _INSTANCE is None:
raise RuntimeError("Recorder not initialized.") raise RuntimeError("Recorder not initialized.")
_INSTANCE.block_till_db_ready()

View file

@ -9,9 +9,8 @@ from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.core import Event, EventOrigin, State from homeassistant.core import Event, EventOrigin, State, split_entity_id
from homeassistant.remote import JSONEncoder from homeassistant.remote import JSONEncoder
from homeassistant.helpers.entity import split_entity_id
# SQLAlchemy Schema # SQLAlchemy Schema
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -62,7 +61,7 @@ class States(Base): # type: ignore
__tablename__ = 'states' __tablename__ = 'states'
state_id = Column(Integer, primary_key=True) state_id = Column(Integer, primary_key=True)
domain = Column(String(64)) domain = Column(String(64))
entity_id = Column(String(64)) entity_id = Column(String(255))
state = Column(String(255)) state = Column(String(255))
attributes = Column(Text) attributes = Column(Text)
event_id = Column(Integer, ForeignKey('events.event_id')) event_id = Column(Integer, ForeignKey('events.event_id'))

View file

@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS) from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.9.0'] REQUIREMENTS = ['pyRFXtrx==0.10.1']
DOMAIN = "rfxtrx" DOMAIN = "rfxtrx"
@ -66,6 +66,7 @@ def _valid_device(value, device_type):
key = device.get('packetid') key = device.get('packetid')
device.pop('packetid') device.pop('packetid')
key = str(key)
if not len(key) % 2 == 0: if not len(key) % 2 == 0:
key = '0' + key key = '0' + key
@ -130,7 +131,11 @@ def setup(hass, config):
# Log RFXCOM event # Log RFXCOM event
if not event.device.id_string: if not event.device.id_string:
return return
_LOGGER.info("Receive RFXCOM event from %s", event.device) _LOGGER.info("Receive RFXCOM event from "
"(Device_id: %s Class: %s Sub: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype)
# Callback to HA registered components. # Callback to HA registered components.
for subscriber in RECEIVED_EVT_SUBSCRIBERS: for subscriber in RECEIVED_EVT_SUBSCRIBERS:
@ -213,13 +218,14 @@ def get_new_device(event, config, device):
if not config[ATTR_AUTOMATIC_ADD]: if not config[ATTR_AUTOMATIC_ADD]:
return return
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
_LOGGER.info( _LOGGER.info(
"Automatic add %s rfxtrx device (Class: %s Sub: %s)", "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)",
device_id, device_id,
event.device.__class__.__name__, event.device.__class__.__name__,
event.device.subtype event.device.subtype,
pkt_id
) )
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
datas = {ATTR_STATE: False, ATTR_FIREEVENT: False} datas = {ATTR_STATE: False, ATTR_FIREEVENT: False}
signal_repetitions = config[CONF_SIGNAL_REPETITIONS] signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(pkt_id, event, datas, new_device = device(pkt_id, event, datas,
@ -236,7 +242,7 @@ def apply_received_command(event):
return return
_LOGGER.debug( _LOGGER.debug(
"EntityID: %s device_update. Command: %s", "Device_id: %s device_update. Command: %s",
device_id, device_id,
event.values['Command'] event.values['Command']
) )

View file

@ -0,0 +1,23 @@
move_up:
description: Move up all or specified roller shutter
fields:
entity_id:
description: Name(s) of roller shutter(s) to move up
example: 'rollershutter.living_room'
move_down:
description: Move down all or specified roller shutter
fields:
entity_id:
description: Name(s) of roller shutter(s) to move down
example: 'rollershutter.living_room'
stop:
description: Stop all or specified roller shutter
fields:
entity_id:
description: Name(s) of roller shutter(s) to stop
example: 'rollershutter.living_room'

View file

@ -40,10 +40,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
def __init__(self, value): def __init__(self, value):
"""Initialize the zwave rollershutter.""" """Initialize the zwave rollershutter."""
import libopenzwave
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._lozwmgr = libopenzwave.PyManager()
self._lozwmgr.create()
self._node = value.node self._node = value.node
self._current_position = None
dispatcher.connect( dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
@ -51,32 +55,53 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id or \ if self._value.value_id == value.value_id or \
self._value.node == value.node: self._value.node == value.node:
self.update_properties()
self.update_ha_state() self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value) _LOGGER.debug("Value changed on network %s", value)
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Position value
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Level':
self._current_position = value.data
@property @property
def current_position(self): def current_position(self):
"""Return the current position of Zwave roller shutter.""" """Return the current position of Zwave roller shutter."""
if self._value.data <= 5: if self._current_position is not None:
if self._current_position <= 5:
return 100 return 100
elif self._value.data >= 95: elif self._current_position >= 95:
return 0 return 0
else: else:
return 100 - self._value.data return 100 - self._current_position
def move_up(self, **kwargs): def move_up(self, **kwargs):
"""Move the roller shutter up.""" """Move the roller shutter up."""
self._node.set_dimmer(self._value.value_id, 100) for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open':
self._lozwmgr.pressButton(value.value_id)
break
def move_down(self, **kwargs): def move_down(self, **kwargs):
"""Move the roller shutter down.""" """Move the roller shutter down."""
self._node.set_dimmer(self._value.value_id, 0) for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Close':
self._lozwmgr.pressButton(value.value_id)
break
def stop(self, **kwargs): def stop(self, **kwargs):
"""Stop the roller shutter.""" """Stop the roller shutter."""
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_BINARY).values(): class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
# Rollershutter will toggle between UP (True), DOWN (False). if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
# It also stops the shutter if the same value is sent while moving. and value.label == 'Open':
value.data = value.data self._lozwmgr.releaseButton(value.value_id)
break break

View file

@ -14,7 +14,8 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_TOGGLE, STATE_ON, CONF_ALIAS) SERVICE_TOGGLE, STATE_ON, CONF_ALIAS)
from homeassistant.helpers.entity import ToggleEntity, split_entity_id from homeassistant.core import split_entity_id
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv

View file

@ -10,6 +10,7 @@ from datetime import timedelta
from homeassistant.const import TEMP_FAHRENHEIT from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.temperature import celsius_to_fahrenheit
# Update this requirement to upstream as soon as it supports Python 3. # Update this requirement to upstream as soon as it supports Python 3.
REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/' REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/'
@ -32,8 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_DHT import Adafruit_DHT
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
unit = hass.config.temperature_unit
available_sensors = { available_sensors = {
"DHT11": Adafruit_DHT.DHT11, "DHT11": Adafruit_DHT.DHT11,
"DHT22": Adafruit_DHT.DHT22, "DHT22": Adafruit_DHT.DHT22,
@ -58,7 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if variable not in SENSOR_TYPES: if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable) _LOGGER.error('Sensor type: "%s" does not exist', variable)
else: else:
dev.append(DHTSensor(data, variable, unit, name)) dev.append(
DHTSensor(data, variable, SENSOR_TYPES[variable][1], name))
except KeyError: except KeyError:
pass pass
@ -103,7 +104,8 @@ class DHTSensor(Entity):
if self.type == 'temperature': if self.type == 'temperature':
self._state = round(data['temperature'], 1) self._state = round(data['temperature'], 1)
if self.temp_unit == TEMP_FAHRENHEIT: if self.temp_unit == TEMP_FAHRENHEIT:
self._state = round(data['temperature'] * 1.8 + 32, 1) self._state = round(celsius_to_fahrenheit(data['temperature']),
1)
elif self.type == 'humidity': elif self.type == 'humidity':
self._state = round(data['humidity'], 1) self._state = round(data['humidity'], 1)

View file

@ -0,0 +1,104 @@
"""
Support for Fast.com internet speed testing sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.fastdotcom/
"""
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder
from homeassistant.components.sensor import DOMAIN
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_change
REQUIREMENTS = ['https://github.com/nkgilley/fast.com/archive/'
'master.zip#fastdotcom==0.0.1']
_LOGGER = logging.getLogger(__name__)
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
CONF_DAY = 'day'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Fast.com sensor."""
data = SpeedtestData(hass, config)
sensor = SpeedtestSensor(data)
add_devices([sensor])
def update(call=None):
"""Update service for manual updates."""
data.update(dt_util.now())
sensor.update()
hass.services.register(DOMAIN, 'update_fastdotcom', update)
# pylint: disable=too-few-public-methods
class SpeedtestSensor(Entity):
"""Implementation of a FAst.com sensor."""
def __init__(self, speedtest_data):
"""Initialize the sensor."""
self._name = 'Fast.com Speedtest'
self.speedtest_client = speedtest_data
self._state = None
self._unit_of_measurement = 'Mbps'
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Get the latest data and update the states."""
data = self.speedtest_client.data
if data is None:
entity_id = 'sensor.fastcom_speedtest'
states = recorder.get_model('States')
try:
last_state = recorder.execute(
recorder.query('States').filter(
(states.entity_id == entity_id) &
(states.last_changed == states.last_updated) &
(states.state != 'unknown')
).order_by(states.state_id.desc()).limit(1))
except TypeError:
return
if not last_state:
return
self._state = last_state[0].state
else:
self._state = data['download']
class SpeedtestData(object):
"""Get the latest data from fast.com."""
def __init__(self, hass, config):
"""Initialize the data object."""
self.data = None
track_time_change(hass, self.update,
second=config.get(CONF_SECOND, 0),
minute=config.get(CONF_MINUTE, 0),
hour=config.get(CONF_HOUR, None),
day=config.get(CONF_DAY, None))
def update(self, now):
"""Get the latest data from fast.com."""
from fastdotcom import fast_com
_LOGGER.info('Executing fast.com speedtest')
self.data = {'download': fast_com()}

View file

@ -10,7 +10,6 @@ import logging
import datetime import datetime
import time import time
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
@ -233,13 +232,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
authd_client.client.refresh_token() authd_client.client.refresh_token()
authd_client.system = authd_client.user_profile_get()["user"]["locale"] authd_client.system = authd_client.user_profile_get()["user"]["locale"]
if authd_client.system != 'en_GB':
if hass.config.units.is_metric:
authd_client.system = "metric"
else:
authd_client.system = "en_US"
dev = [] dev = []
for resource in config.get("monitored_resources", for resource in config.get("monitored_resources",
FITBIT_DEFAULT_RESOURCE_LIST): FITBIT_DEFAULT_RESOURCE_LIST):
dev.append(FitbitSensor(authd_client, config_path, resource, dev.append(FitbitSensor(authd_client, config_path, resource,
hass.config.temperature_unit == hass.config.units.is_metric))
TEMP_CELSIUS))
add_devices(dev) add_devices(dev)
else: else:

View file

@ -10,7 +10,7 @@ from requests.exceptions import ConnectionError as ConnectError, \
HTTPError, Timeout HTTPError, Timeout
from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor import DOMAIN
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if 'units' in config: if 'units' in config:
units = config['units'] units = config['units']
elif hass.config.temperature_unit == TEMP_CELSIUS: elif hass.config.units.is_metric:
units = 'si' units = 'si'
else: else:
units = 'us' units = 'us'

View file

@ -11,8 +11,7 @@ import voluptuous as vol
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_API_KEY, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -92,10 +91,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
options = config.get(CONF_OPTIONS) options = config.get(CONF_OPTIONS)
if options.get('units') is None: if options.get('units') is None:
if hass.config.temperature_unit is TEMP_CELSIUS: options['units'] = hass.config.units.name
options['units'] = 'metric'
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
options['units'] = 'imperial'
travel_mode = config.get(CONF_TRAVEL_MODE) travel_mode = config.get(CONF_TRAVEL_MODE)
mode = options.get(CONF_MODE) mode = options.get(CONF_MODE)

View file

@ -0,0 +1,111 @@
"""
Support for GPSD.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.gpsd/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN,
CONF_HOST, CONF_PORT, CONF_PLATFORM,
CONF_NAME)
REQUIREMENTS = ['gps3==0.33.2']
DEFAULT_NAME = 'GPS'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 2947
ATTR_GPS_TIME = 'gps_time'
ATTR_ELEVATION = 'elevation'
ATTR_SPEED = 'speed'
ATTR_CLIMB = 'climb'
ATTR_MODE = 'mode'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'gpsd',
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.string,
})
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the GPSD component."""
name = config.get(CONF_NAME, DEFAULT_NAME)
host = config.get(CONF_HOST, DEFAULT_HOST)
port = config.get(CONF_PORT, DEFAULT_PORT)
# Will hopefully be possible with the next gps3 update
# https://github.com/wadda/gps3/issues/11
# from gps3 import gps3
# try:
# gpsd_socket = gps3.GPSDSocket()
# gpsd_socket.connect(host=host, port=port)
# except GPSError:
# _LOGGER.warning('Not able to connect to GPSD')
# return False
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((host, port))
sock.shutdown(2)
_LOGGER.debug('Connection to GPSD possible')
except socket.error:
_LOGGER.error('Not able to connect to GPSD')
return False
add_devices([GpsdSensor(hass, name, host, port)])
class GpsdSensor(Entity):
"""Representation of a GPS receiver available via GPSD."""
def __init__(self, hass, name, host, port):
"""Initialize the GPSD sensor."""
from gps3.agps3threaded import AGPS3mechanism
self.hass = hass
self._name = name
self._host = host
self._port = port
self.agps_thread = AGPS3mechanism()
self.agps_thread.stream_data(host=self._host, port=self._port)
self.agps_thread.run_thread()
@property
def name(self):
"""Return the name."""
return self._name
# pylint: disable=no-member
@property
def state(self):
"""Return the state of GPSD."""
if self.agps_thread.data_stream.mode == 3:
return "3D Fix"
elif self.agps_thread.data_stream.mode == 2:
return "2D Fix"
else:
return STATE_UNKNOWN
@property
def state_attributes(self):
"""Return the state attributes of the GPS."""
return {
ATTR_LATITUDE: self.agps_thread.data_stream.lat,
ATTR_LONGITUDE: self.agps_thread.data_stream.lon,
ATTR_ELEVATION: self.agps_thread.data_stream.alt,
ATTR_GPS_TIME: self.agps_thread.data_stream.time,
ATTR_SPEED: self.agps_thread.data_stream.speed,
ATTR_CLIMB: self.agps_thread.data_stream.climb,
ATTR_MODE: self.agps_thread.data_stream.mode,
}

View file

@ -65,7 +65,7 @@ class MoldIndicator(Entity):
self._indoor_humidity_sensor = indoor_humidity_sensor self._indoor_humidity_sensor = indoor_humidity_sensor
self._outdoor_temp_sensor = outdoor_temp_sensor self._outdoor_temp_sensor = outdoor_temp_sensor
self._calib_factor = calib_factor self._calib_factor = calib_factor
self._is_metric = (hass.config.temperature_unit == TEMP_CELSIUS) self._is_metric = hass.config.units.is_metric
self._dewpoint = None self._dewpoint = None
self._indoor_temp = None self._indoor_temp = None
@ -109,7 +109,7 @@ class MoldIndicator(Entity):
# convert to celsius if necessary # convert to celsius if necessary
if unit == TEMP_FAHRENHEIT: if unit == TEMP_FAHRENHEIT:
return util.temperature.fahrenheit_to_celcius(temp) return util.temperature.fahrenheit_to_celsius(temp)
elif unit == TEMP_CELSIUS: elif unit == TEMP_CELSIUS:
return temp return temp
else: else:
@ -260,9 +260,9 @@ class MoldIndicator(Entity):
else: else:
return { return {
ATTR_DEWPOINT: ATTR_DEWPOINT:
util.temperature.celcius_to_fahrenheit( util.temperature.celsius_to_fahrenheit(
self._dewpoint), self._dewpoint),
ATTR_CRITICAL_TEMP: ATTR_CRITICAL_TEMP:
util.temperature.celcius_to_fahrenheit( util.temperature.celsius_to_fahrenheit(
self._crit_temp), self._crit_temp),
} }

View file

@ -95,6 +95,9 @@ class OctoPrintSensor(Entity):
"""Return the state of the sensor.""" """Return the state of the sensor."""
sensor_unit = self.unit_of_measurement sensor_unit = self.unit_of_measurement
if sensor_unit == TEMP_CELSIUS or sensor_unit == "%": if sensor_unit == TEMP_CELSIUS or sensor_unit == "%":
# API sometimes returns null and not 0
if self._state is None:
self._state = 0
return round(self._state, 2) return round(self._state, 2)
else: else:
return self._state return self._state

View file

@ -0,0 +1,74 @@
"""
Support for OhmConnect.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/sensor.ohmconnect/
"""
import logging
from datetime import timedelta
import xml.etree.ElementTree as ET
import requests
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the OhmConnect sensors."""
ohmid = config.get("id")
if ohmid is None:
_LOGGER.error("You must provide your OhmConnect ID!")
return False
add_devices([OhmconnectSensor(config.get("name", "OhmConnect Status"),
ohmid)])
class OhmconnectSensor(Entity):
"""Representation of a OhmConnect sensor."""
def __init__(self, name, ohmid):
"""Initialize the sensor."""
self._name = name
self._ohmid = ohmid
self._data = {}
self.update()
@property
def name(self):
"""The name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
if self._data.get("active") == "True":
return "Active"
else:
return "Inactive"
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {"Address": self._data.get("address"), "ID": self._ohmid}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from OhmConnect."""
try:
url = ("https://login.ohmconnect.com"
"/verify-ohm-hour/{}").format(self._ohmid)
response = requests.get(url, timeout=10)
root = ET.fromstring(response.text)
for child in root:
self._data[child.tag] = child.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to host/endpoint: %s", url)
self.data = {}

View file

@ -48,8 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
from pyowm import OWM from pyowm import OWM
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
unit = hass.config.temperature_unit
forecast = config.get('forecast') forecast = config.get('forecast')
owm = OWM(config.get(CONF_API_KEY, None)) owm = OWM(config.get(CONF_API_KEY, None))
@ -67,13 +66,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if variable not in SENSOR_TYPES: if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable) _LOGGER.error('Sensor type: "%s" does not exist', variable)
else: else:
dev.append(OpenWeatherMapSensor(data, variable, unit)) dev.append(OpenWeatherMapSensor(data, variable,
SENSOR_TYPES[variable][1]))
except KeyError: except KeyError:
pass pass
if forecast: if forecast:
SENSOR_TYPES['forecast'] = ['Forecast', None] SENSOR_TYPES['forecast'] = ['Forecast', None]
dev.append(OpenWeatherMapSensor(data, 'forecast', unit)) dev.append(OpenWeatherMapSensor(data, 'forecast',
SENSOR_TYPES['temperature'][1]))
add_devices(dev) add_devices(dev)

View file

@ -14,7 +14,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['plexapi==1.1.0'] REQUIREMENTS = ['plexapi==2.0.2']
CONF_SERVER = 'server' CONF_SERVER = 'server'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@ -54,15 +54,18 @@ class PlexSensor(Entity):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, name, plex_url, plex_user, plex_password, plex_server): def __init__(self, name, plex_url, plex_user, plex_password, plex_server):
"""Initialize the sensor.""" """Initialize the sensor."""
from plexapi.utils import NA
self._na_type = NA
self._name = name self._name = name
self._state = 0 self._state = 0
self._now_playing = [] self._now_playing = []
if plex_user and plex_password: if plex_user and plex_password:
from plexapi.myplex import MyPlexUser from plexapi.myplex import MyPlexAccount
user = MyPlexUser.signin(plex_user, plex_password) user = MyPlexAccount.signin(plex_user, plex_password)
server = plex_server if plex_server else user.resources()[0].name server = plex_server if plex_server else user.resources()[0].name
self._server = user.getResource(server).connect() self._server = user.resource(server).connect()
else: else:
from plexapi.server import PlexServer from plexapi.server import PlexServer
self._server = PlexServer(plex_url) self._server = PlexServer(plex_url)
@ -93,7 +96,11 @@ class PlexSensor(Entity):
def update(self): def update(self):
"""Update method for plex sensor.""" """Update method for plex sensor."""
sessions = self._server.sessions() sessions = self._server.sessions()
now_playing = [(s.user.title, "{0} ({1})".format(s.title, s.year)) now_playing = []
for s in sessions] for sess in sessions:
user = sess.user.title if sess.user is not self._na_type else ""
title = sess.title if sess.title is not self._na_type else ""
year = sess.year if sess.year is not self._na_type else ""
now_playing.append((user, "{0} ({1})".format(title, year)))
self._state = len(sessions) self._state = len(sessions)
self._now_playing = now_playing self._now_playing = now_playing

View file

@ -0,0 +1,94 @@
"""
Support for particulate matter sensors connected to a serial port.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.serial_pm/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_PLATFORM
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
REQUIREMENTS = ['pmsensor==0.2']
_LOGGER = logging.getLogger(__name__)
CONF_SERIAL_DEVICE = "serial_device"
CONF_BRAND = "brand"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): 'serial_pm',
vol.Optional(CONF_NAME, default=""): cv.string,
vol.Required(CONF_SERIAL_DEVICE): cv.string,
vol.Required(CONF_BRAND): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the available PM sensors."""
from pmsensor import serial_data_collector as pm
try:
coll = pm.PMDataCollector(config.get(CONF_SERIAL_DEVICE),
pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)])
except KeyError:
_LOGGER.error("Brand %s not supported\n supported brands: %s",
config.get(CONF_BRAND), pm.SUPPORTED_SENSORS.keys())
return
except OSError as err:
_LOGGER.error("Could not open serial connection to %s (%s)",
config.get(CONF_SERIAL_DEVICE), err)
return
dev = []
for pmname in coll.supported_values():
if config.get("name") != "":
name = "{} PM{}".format(config.get("name"), pmname)
else:
name = "PM{}".format(pmname)
dev.append(ParticulateMatterSensor(coll, name, pmname))
add_devices(dev)
class ParticulateMatterSensor(Entity):
"""Representation of an Particulate matter sensor."""
def __init__(self, pmDataCollector, name, pmname):
"""Initialize a new PM sensor."""
self._name = name
self._pmname = pmname
self._state = None
self._collector = pmDataCollector
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return "µg/m³"
def update(self):
"""Read from sensor and update the state."""
_LOGGER.debug("Reading data from PM sensor")
try:
self._state = self._collector.read_data()[self._pmname]
except KeyError:
_LOGGER.error("Could not read PM%s value", self._pmname)
def should_poll(self):
"""Sensor needs polling."""
return True

View file

@ -21,7 +21,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Temper sensors.""" """Setup the Temper sensors."""
from temperusb.temper import TemperHandler from temperusb.temper import TemperHandler
temp_unit = hass.config.temperature_unit temp_unit = hass.config.units.temperature_unit
name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME)
temper_devices = TemperHandler().get_devices() temper_devices = TemperHandler().get_devices()
add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx)) add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx))

View file

@ -51,7 +51,8 @@ class VeraSensor(VeraDevice, Entity):
def update(self): def update(self):
"""Update the state.""" """Update the state."""
if self.vera_device.category == "Temperature Sensor": if self.vera_device.category == "Temperature Sensor":
current_temp = self.vera_device.temperature self.current_value = self.vera_device.temperature
vera_temp_units = ( vera_temp_units = (
self.vera_device.vera_controller.temperature_units) self.vera_device.vera_controller.temperature_units)
@ -60,14 +61,6 @@ class VeraSensor(VeraDevice, Entity):
else: else:
self._temperature_units = TEMP_CELSIUS self._temperature_units = TEMP_CELSIUS
if self.hass:
temp = self.hass.config.temperature(
current_temp,
self._temperature_units)
current_temp, self._temperature_units = temp
self.current_value = current_temp
elif self.vera_device.category == "Light Sensor": elif self.vera_device.category == "Light Sensor":
self.current_value = self.vera_device.light self.current_value = self.vera_device.light
elif self.vera_device.category == "Humidity Sensor": elif self.vera_device.category == "Humidity Sensor":

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