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 =
homeassistant/__main__.py
homeassistant/scripts/*.py
homeassistant/helpers/typing.py
# omit pieces of code that rely on external devices being present
homeassistant/components/apcupsd.py
@ -88,6 +89,9 @@ omit =
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
homeassistant/components/knx.py
homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py
@ -100,6 +104,7 @@ omit =
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py
homeassistant/components/camera/generic.py
homeassistant/components/camera/mjpeg.py
@ -123,8 +128,9 @@ omit =
homeassistant/components/discovery.py
homeassistant/components/downloader.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/wink.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py
@ -192,23 +198,27 @@ omit =
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/openexchangerates.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.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/hello_world.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/home-assistant.log
@ -52,7 +54,8 @@ develop-eggs
lib
lib64
# Installer logs
# Logs
*.log
pip-log.txt
# Unit test / coverage reports
@ -91,3 +94,6 @@ ctags.tmp
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config
# Visual Studio Code
.vscode

View file

@ -10,8 +10,8 @@ homeassistant:
# Impacts weather/sunrise data
elevation: 665
# C for Celsius, F for Fahrenheit
temperature_unit: C
# 'metric' for Metric System, 'imperial' for imperial system
unit_system: metric
# Pick yours from here:
# 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-dom-15.2.1.min.js"></script>

View file

@ -419,8 +419,9 @@ definitions:
description: Longitude of Home Assistant server
location_name:
type: string
temperature_unit:
unit_system:
type: string
description: The system for measurement units
time_zone:
type: string
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
# useful when debugging shutdown/restart issues.
try:
nthreads = sum(thread.isAlive() and not thread.isDaemon()
nthreads = sum(thread.is_alive() and not thread.daemon
for thread in threading.enumerate())
if nthreads > 1:
sys.stderr.write(

View file

@ -11,12 +11,12 @@ from types import ModuleType
from typing import Any, Optional, Dict
import voluptuous as vol
from voluptuous.humanize import humanize_error
import homeassistant.components as core_components
from homeassistant.components import group, persistent_notification
import homeassistant.config as conf_util
import homeassistant.core as core
import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader
import homeassistant.util.package as pkg_util
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
@ -103,7 +103,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, config)
_log_exception(ex, domain, config)
return False
elif hasattr(component, 'PLATFORM_SCHEMA'):
@ -113,7 +113,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, domain, p_config)
_log_exception(ex, domain, p_config)
return False
# Not all platform components follow same pattern for platforms
@ -134,8 +134,8 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, '{}.{}'
.format(domain, p_name), p_validated)
_log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return False
platforms.append(p_validated)
@ -232,14 +232,14 @@ def from_config_dict(config: Dict[str, Any],
if config_dir is not None:
config_dir = os.path.abspath(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, {})
try:
conf_util.process_ha_core_config(hass, core_config)
except vol.Invalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
_log_exception(ex, 'homeassistant', core_config)
return None
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
config_dir = os.path.abspath(os.path.dirname(config_path))
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)
@ -371,6 +371,26 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
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."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))
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'))
return deps_dir

View file

@ -11,7 +11,6 @@ import itertools as it
import logging
import homeassistant.core as ha
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.loader import get_component
from homeassistant.const import (
@ -35,7 +34,7 @@ def is_on(hass, entity_id=None):
entity_ids = hass.states.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)
@ -95,7 +94,7 @@ def setup(hass, config):
# Group entity_ids by domain. groupby requires sorted data.
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:
# 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'
SCAN_INTERVAL = 30
ATTR_CHANGED_BY = 'changed_by'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -124,6 +125,11 @@ class AlarmControlPanel(Entity):
"""Regex for code format or None if no code is required."""
return None
@property
def changed_by(self):
"""Last change triggered by."""
return None
def alarm_disarm(self, code=None):
"""Send disarm command."""
raise NotImplementedError()
@ -145,5 +151,6 @@ class AlarmControlPanel(Entity):
"""Return the state attributes."""
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
}
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._state = STATE_UNKNOWN
self._digits = int(hub.config.get('code_digits', '4'))
self._changed_by = None
@property
def name(self):
@ -58,6 +59,11 @@ class VerisureAlarm(alarm.AlarmControlPanel):
"""The code format as regex."""
return '^\\d{%s}$' % self._digits
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
def update(self):
"""Update alarm status."""
hub.update_alarms()
@ -72,6 +78,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
_LOGGER.error(
'Unknown alarm state %s',
hub.alarm_status[self._id].status)
self._changed_by = hub.alarm_status[self._id].name
def alarm_disarm(self, code=None):
"""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)])
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
class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL."""
@ -52,19 +64,8 @@ class MjpegCamera(Camera):
def camera_image(self):
"""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:
return process_response(response)
return extract_image_from_mjpeg(response.iter_content(1024))
def mjpeg_stream(self, response):
"""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+)')
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
REQUIREMENTS = ['fuzzywuzzy==0.11.1']
def setup(hass, config):

View file

@ -21,10 +21,10 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_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"
REQUIREMENTS = ['python-nmap==0.6.0']
REQUIREMENTS = ['python-nmap==0.6.1']
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
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.7.0']
REQUIREMENTS = ['netdisco==0.7.1']
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__)
def register_built_in_panel(hass, component_name, title=None, icon=None,
url_name=None, config=None):
def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel."""
# pylint: disable=too-many-arguments
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
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,
url_name=None, url=None, config=None):
def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
sidebar_icon=None, url_path=None, url=None, config=None):
"""Register a panel for the frontend.
component_name: name of the web component
path: path to the HTML of the web component
md5: the md5 hash of the web component (for versioning, optional)
title: title to show in the sidebar (optional)
icon: icon to show next to title in sidebar (optional)
url_name: name to use in the url (defaults to component_name)
sidebar_title: title to show in the sidebar (optional)
sidebar_icon: icon to show next to title in sidebar (optional)
url_path: name to use in the url (defaults to component_name)
url: for the web component (for dev environment, optional)
config: config to be passed into the web component
Warning: this API will probably change. Use at own risk.
"""
# pylint: disable=too-many-arguments
if url_name is None:
url_name = component_name
if url_path is None:
url_path = component_name
if url_name in PANELS:
_LOGGER.warning('Overwriting component %s', url_name)
if url_path in PANELS:
_LOGGER.warning('Overwriting component %s', url_path)
if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s',
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()
data = {
'url_name': url_name,
'url_path': url_path,
'component_name': component_name,
}
if title:
data['title'] = title
if icon:
data['icon'] = icon
if sidebar_title:
data['title'] = sidebar_title
if sidebar_icon:
data['icon'] = sidebar_icon
if config is not None:
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)
data['url'] = fprinted_url
PANELS[url_name] = data
PANELS[url_path] = data
def setup(hass, config):
@ -195,6 +196,6 @@ class IndexView(HomeAssistantView):
resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
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')

View file

@ -8,6 +8,9 @@
<link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180'
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="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.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='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url %}
{% if panel_url -%}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif %}
{% endif -%}
<link rel='import' href='{{ icons_url }}' async>
<script>
var webComponentsSupported = (

View file

@ -1,16 +1,16 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = {
"core.js": "bc78f21f5280217aa2c78dfc5848134f",
"frontend.html": "6c52e8cb797bafa3124d936af5ce1fcc",
"mdi.html": "f6c6cc64c2ec38a80e91f801b41119b3",
"panels/ha-panel-dev-event.html": "20327fbd4fb0370aec9be4db26fd723f",
"panels/ha-panel-dev-info.html": "28e0a19ceb95aa714fd53228d9983a49",
"panels/ha-panel-dev-service.html": "85fd5b48600418bb5a6187539a623c38",
"panels/ha-panel-dev-state.html": "25d84d7b7aea779bb3bb3cd6c155f8d9",
"panels/ha-panel-dev-template.html": "d079abf61cff9690f828cafb0d29b7e7",
"panels/ha-panel-history.html": "7e051b5babf5653b689e0107ea608acb",
"panels/ha-panel-iframe.html": "7bdb564a8f37971d7b89b718935810a1",
"panels/ha-panel-logbook.html": "9b285357b0b2d82ee282e634f4e1cab2",
"panels/ha-panel-map.html": "dfe141a3fa5fd403be554def1dd039a9"
"core.js": "457d5acd123e7dc38947c07984b3a5e8",
"frontend.html": "829ee7cb591b8a63d7f22948a7aeb07a",
"mdi.html": "b399b5d3798f5b68b0a4fbaae3432d48",
"panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b",
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
"panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa",
"panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a",
"panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
"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)
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>
<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><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):
"""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
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
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
COMMAND_CLASS_BARRIER_OPERATOR = 0x66 # 102
_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]]
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
if value.type != zwave.TYPE_BOOL:
return
@ -62,8 +63,8 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
def close_door(self):
"""Close the garage door."""
self._value.node.set_switch(self._value.value_id, False)
self._value.data = False
def open_door(self):
"""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,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
from homeassistant.helpers.entity import (
Entity, generate_entity_id, split_entity_id)
from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
@ -101,7 +100,7 @@ def expand_entity_ids(hass, entity_ids):
try:
# 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:
found_ids.extend(

View file

@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.10"]
REQUIREMENTS = ["pyhomematic==0.1.11"]
HOMEMATIC = None
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_HEADERS, ALLOWED_CORS_HEADERS,
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.helpers.config_validation as cv
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_SERVER_HOST = "server_host"
@ -453,6 +453,10 @@ class HomeAssistantView(object):
"""Handle request to url."""
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
if request.method == "OPTIONS":
# For CORS preflight requests.
return self.options(request)
try:
handler = getattr(self, request.method.lower())
except AttributeError:
@ -473,16 +477,16 @@ class HomeAssistantView(object):
self.hass.wsgi.api_password):
authenticated = True
if authenticated:
_LOGGER.info('Successful login/request from %s',
request.remote_addr)
elif self.requires_auth and not authenticated:
if self.requires_auth and not authenticated:
_LOGGER.warning('Login attempt or request with an invalid'
'password from %s', request.remote_addr)
raise Unauthorized()
request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)',
request.path, request.remote_addr, authenticated)
result = handler(request, **values)
if isinstance(result, self.Response):

View file

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

View file

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

View file

@ -9,11 +9,12 @@ import logging
import socket
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
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip'
'#flux_led==0.3']
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip'
'#flux_led==0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "flux_led"
@ -37,7 +38,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
light_ips = []
for ipaddr, device_config in config["devices"].items():
device = {}
device['id'] = device_config[ATTR_NAME]
device['name'] = device_config[ATTR_NAME]
device['ipaddr'] = ipaddr
light = FluxLight(device)
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
scanner = flux_led.BulbScanner()
scanner.scan(timeout=20)
scanner.scan(timeout=10)
for device in scanner.getBulbInfo():
light = FluxLight(device)
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)
light_ips.append(ipaddr)
@ -69,7 +73,7 @@ class FluxLight(Light):
"""Initialize the light."""
import flux_led
self._name = device['id']
self._name = device['name']
self._ipaddr = device['ipaddr']
self.is_valid = True
self._bulb = None
@ -96,9 +100,27 @@ class FluxLight(Light):
"""Return true if device is on."""
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):
"""Turn the specified or all lights on."""
self._bulb.turnOn()
if not self.is_on:
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):
"""Turn the specified or all lights off."""

View file

@ -72,6 +72,11 @@ class Hyperion(Light):
"""Get the remote's active color."""
response = self.json_request({"command": "serverinfo"})
if response:
# workaround for outdated Hyperion
if "activeLedColor" not in response["info"]:
self._rgb_color = self._default_color
return
if response["info"]["activeLedColor"] == []:
self._rgb_color = [0, 0, 0]
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
https://home-assistant.io/components/light.lifx/
"""
# pylint: disable=missing-docstring
import colorsys
import logging
@ -16,7 +14,6 @@ from homeassistant.helpers.event import track_time_change
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['liffylights==0.9.4']
DEPENDENCIES = []
CONF_SERVER = "server" # server address configuration item
CONF_BROADCAST = "broadcast" # broadcast address configuration item
@ -94,11 +91,11 @@ class LIFX():
# pylint: disable=unused-argument
def poll(self, now):
"""Initialize the light."""
"""Polling for the light."""
self.probe()
def probe(self, address=None):
"""Initialize the light."""
"""Probe the light."""
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'
SCAN_INTERVAL = 30
ATTR_CHANGED_BY = 'changed_by'
GROUP_NAME_ALL_LOCKS = 'all locks'
ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format('all_locks')
@ -101,6 +102,11 @@ def setup(hass, config):
class LockDevice(Entity):
"""Representation of a lock."""
@property
def changed_by(self):
"""Last change triggered by."""
return None
# pylint: disable=no-self-use
@property
def code_format(self):
@ -127,6 +133,7 @@ class LockDevice(Entity):
return None
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by
}
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._state = STATE_UNKNOWN
self._digits = int(hub.config.get('code_digits', '4'))
self._changed_by = None
@property
def name(self):
@ -51,6 +52,11 @@ class VerisureDoorlock(LockDevice):
"""Return True if entity is available."""
return hub.available
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
@property
def code_format(self):
"""Return the required six digit code."""
@ -68,6 +74,7 @@ class VerisureDoorlock(LockDevice):
_LOGGER.error(
'Unknown lock state %s',
hub.lock_status[self._id].status)
self._changed_by = hub.lock_status[self._id].name
@property
def is_locked(self):

View file

@ -18,10 +18,8 @@ from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.core import State
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'frontend']
@ -196,6 +194,11 @@ def humanify(events):
event != last_sensor_event[to_state.entity_id]:
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(
event.time_fired,
name=to_state.name,

View file

@ -6,6 +6,7 @@ https://home-assistant.io/components/media_player/
"""
import logging
import os
import requests
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_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
@ -25,10 +27,13 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'media_player'
DEPENDENCIES = ['http']
SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}'
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
@ -286,6 +291,8 @@ def setup(hass, config):
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(MediaPlayerImageView(hass, component.entities))
component.setup(config)
descriptions = load_yaml_config_file(
@ -398,6 +405,11 @@ class MediaPlayerDevice(Entity):
"""State of the player."""
return STATE_UNKNOWN
@property
def access_token(self):
"""Access token for this media player."""
return str(id(self))
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
@ -633,7 +645,8 @@ class MediaPlayerDevice(Entity):
@property
def entity_picture(self):
"""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
def state_attributes(self):
@ -649,3 +662,39 @@ class MediaPlayerDevice(Entity):
}
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_component = pid.communicate()[0]
mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
pid_component).groups()[0]
return mac
match = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'),
pid_component)
if match is not None:
return match.groups()[0]
else:
return None
def _config_from_file(filename, config=None):

View file

@ -163,6 +163,11 @@ class LgTVDevice(MediaPlayerDevice):
"""Flag of media commands that are supported."""
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):
"""Turn off media player."""
self.send_command(1)

View file

@ -12,15 +12,16 @@ from urllib.parse import urlparse
import homeassistant.util as util
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN)
from homeassistant.loader import get_component
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_FORCED_SCANS = timedelta(seconds=1)
@ -30,7 +31,8 @@ PLEX_CONFIG_FILE = 'plex.conf'
_CONFIGURING = {}
_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):
@ -193,6 +195,9 @@ class PlexClient(MediaPlayerDevice):
# pylint: disable=too-many-public-methods, attribute-defined-outside-init
def __init__(self, device, plex_sessions, update_devices, update_sessions):
"""Initialize the Plex device."""
from plexapi.utils import NA
self.na_type = NA
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
@ -211,20 +216,17 @@ class PlexClient(MediaPlayerDevice):
@property
def name(self):
"""Return the name of the device."""
return self.device.name or DEVICE_DEFAULT_NAME
return self.device.title or DEVICE_DEFAULT_NAME
@property
def session(self):
"""Return the session, if any."""
if self.device.machineIdentifier not in self.plex_sessions:
return None
return self.plex_sessions[self.device.machineIdentifier]
return self.plex_sessions.get(self.device.machineIdentifier, None)
@property
def state(self):
"""Return the state of the device."""
if self.session:
if self.session and self.session.player:
state = self.session.player.state
if state == 'playing':
return STATE_PLAYING
@ -243,11 +245,30 @@ class PlexClient(MediaPlayerDevice):
self.update_devices(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
def media_content_id(self):
"""Content ID of current playing media."""
if self.session is not None:
return self.session.ratingKey
return self._convert_na_to_none(self.session.ratingKey)
@property
def media_content_type(self):
@ -259,65 +280,82 @@ class PlexClient(MediaPlayerDevice):
return MEDIA_TYPE_TVSHOW
elif media_type == 'movie':
return MEDIA_TYPE_VIDEO
elif media_type == 'track':
return MEDIA_TYPE_MUSIC
return None
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self.session is not None:
return self.session.duration
return self._convert_na_to_none(self.session.duration)
@property
def media_image_url(self):
"""Image url of current playing media."""
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
def media_title(self):
"""Title of current playing media."""
# find a string we can use as a title
if self.session is not None:
return self.session.title
return self._convert_na_to_none(self.session.title)
@property
def media_season(self):
"""Season of curent playing media (TV Show only)."""
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.seasons()[0].index
return self._convert_na_to_none(self.session.seasons()[0].index)
@property
def media_series_title(self):
"""The title of the series of current playing media (TV Show only)."""
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.grandparentTitle
return self._convert_na_to_none(self.session.grandparentTitle)
@property
def media_episode(self):
"""Episode of current playing media (TV Show only)."""
from plexapi.video import Show
if isinstance(self.session, Show):
return self.session.index
return self._convert_na_to_none(self.session.index)
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
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):
"""Send play command."""
self.device.play()
self.device.play(self._active_media_plexapi_type)
def media_pause(self):
"""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):
"""Send next track command."""
self.device.skipNext()
self.device.skipNext(self._active_media_plexapi_type)
def media_previous_track(self):
"""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
except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout):
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
pass
def get_source_list(self):
"""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,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
STATE_OFF, STATE_ON, TEMP_CELSIUS)
STATE_OFF, STATE_ON)
from homeassistant.helpers import validate_config, discovery
CONF_GATEWAYS = 'gateways'
@ -53,7 +53,7 @@ def setup(hass, config): # pylint: disable=too-many-locals
import mysensors.mysensors as mysensors
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)
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__)
def send_message(hass, message, title=None):
def send_message(hass, message, title=None, data=None):
"""Send a notification message."""
data = {
info = {
ATTR_MESSAGE: message
}
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):

View file

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

View file

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

View file

@ -6,14 +6,18 @@ https://home-assistant.io/components/notify.smtp/
"""
import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
def get_service(hass, config):
"""Get the mail notification service."""
@ -22,52 +26,21 @@ def get_service(hass, config):
_LOGGER):
return None
smtp_server = config.get('server', 'localhost')
port = int(config.get('port', '25'))
username = config.get('username', None)
password = config.get('password', None)
starttls = int(config.get('starttls', 0))
debug = config.get('debug', 0)
server = None
try:
server = smtplib.SMTP(smtp_server, port, timeout=5)
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
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)
mail_service = MailNotificationService(
config.get('server', 'localhost'),
int(config.get('port', '25')),
config.get('sender', None),
int(config.get('starttls', 0)),
config.get('username', None),
config.get('password', None),
config.get('recipient', None),
config.get('debug', 0))
if mail_service.connection_is_valid():
return mail_service
else:
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
class MailNotificationService(BaseNotificationService):
@ -99,17 +72,57 @@ class MailNotificationService(BaseNotificationService):
mail.login(self.username, self.password)
return mail
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
mail = self.connect()
subject = kwargs.get(ATTR_TITLE)
def connection_is_valid(self):
"""Check for valid config, verify connectivity."""
server = None
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['To'] = self.recipient
msg['From'] = self._sender
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):
try:
mail.sendmail(self._sender, self.recipient,
@ -122,3 +135,36 @@ class MailNotificationService(BaseNotificationService):
mail = self.connect()
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):
"""Setup iframe frontend panels."""
for url_name, info in config[DOMAIN].items():
for url_path, info in config[DOMAIN].items():
register_built_in_panel(
hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_name, {'url': info[CONF_URL]})
url_path, {'url': info[CONF_URL]})
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.event import track_state_change
from homeassistant.util.location import distance
from homeassistant.util.distance import convert
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
DEPENDENCIES = ['zone', 'device_tracker']
DOMAIN = 'proximity'
NOT_SET = 'not set'
# Default tolerance
DEFAULT_TOLERANCE = 1
# Default zone
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
ATTR_DIST_FROM = 'dist_to_zone'
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
@ -31,43 +44,41 @@ ATTR_NEAREST = 'nearest'
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
"""Get the zones and offsets from configuration.yaml."""
ignored_zones = []
if 'ignored_zones' in config[DOMAIN]:
for variable in config[DOMAIN]['ignored_zones']:
ignored_zones.append(variable)
def setup_proximity_component(hass, config):
"""Set up individual proximity component."""
# Get the devices from configuration.yaml.
if 'devices' not in config[DOMAIN]:
if 'devices' not in config:
_LOGGER.error('devices not found in config')
return False
ignored_zones = []
if 'ignored_zones' in config:
for variable in config['ignored_zones']:
ignored_zones.append(variable)
proximity_devices = []
for variable in config[DOMAIN]['devices']:
for variable in config['devices']:
proximity_devices.append(variable)
# 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.
proximity_zone = config[DOMAIN].get('zone', DEFAULT_PROXIMITY_ZONE)
proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
entity_id = DOMAIN + '.' + proximity_zone
proximity_zone = 'zone.' + proximity_zone
# Get the unit of measurement from configuration.yaml.
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()
# Set the default values.
dist_to_zone = 'not set'
dir_of_travel = 'not set'
nearest = 'not set'
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 = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
ignored_zones, proximity_devices, tolerance,
zone_id, unit_of_measure)
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
proximity.update_ha_state()
@ -78,13 +89,26 @@ def setup(hass, config): # pylint: disable=too-many-locals,too-many-statements
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
"""Representation of a Proximity."""
# pylint: disable=too-many-arguments
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
nearest, ignored_zones, proximity_devices, tolerance,
proximity_zone):
proximity_zone, unit_of_measure):
"""Initialize the proximity."""
self.hass = hass
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.tolerance = tolerance
self.proximity_zone = proximity_zone
self.unit_of_measure = unit_of_measure
@property
def name(self):
@ -109,7 +134,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return "km"
return self.unit_of_measure
@property
def state_attributes(self):
@ -183,15 +208,16 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
device_state.attributes['longitude'])
# 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
# closest.
closest_device = ''
dist_to_zone = 1000000
closest_device = None # type: str
dist_to_zone = None # type: float
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
dist_to_zone = distances_to_zone[device]

View file

@ -11,15 +11,18 @@ import logging
import queue
import threading
import time
from datetime import timedelta
from datetime import timedelta, datetime
from typing import Any, Union, Optional, List
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.core import HomeAssistant
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL)
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"
@ -44,15 +47,16 @@ CONFIG_SCHEMA = vol.Schema({
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_INSTANCE = None # type: Any
_LOGGER = logging.getLogger(__name__)
# These classes will be populated during setup()
# pylint: disable=invalid-name
Session = None
# pylint: disable=invalid-name,no-member
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.
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:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
finally:
Session().close()
Session.close()
return []
def run_information(point_in_time=None):
def run_information(point_in_time: Optional[datetime]=None):
"""Return information about current run.
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()
def setup(hass, config):
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Setup the recorder."""
# pylint: disable=global-statement
global _INSTANCE
@ -112,30 +116,36 @@ def setup(hass, config):
return True
def query(model_name, *args):
def query(model_name: Union[str, Any], *args) -> QueryType:
"""Helper to return a query handle."""
_verify_instance()
if isinstance(model_name, str):
return Session().query(get_model(model_name), *args)
return Session().query(model_name, *args)
return Session.query(get_model(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."""
from homeassistant.components.recorder import models
return getattr(models, model_name)
try:
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,
message="Error during query: %s"):
def log_error(e: Exception, retry_wait: Optional[float]=0,
rollback: Optional[bool]=True,
message: Optional[str]="Error during query: %s") -> None:
"""Log about SQLAlchemy errors in a sane manner."""
import sqlalchemy.exc
if not isinstance(e, sqlalchemy.exc.OperationalError):
_LOGGER.exception(e)
_LOGGER.exception(str(e))
else:
_LOGGER.error(message, str(e))
if rollback:
Session().rollback()
Session.rollback()
if retry_wait:
_LOGGER.info("Retrying in %s seconds", retry_wait)
time.sleep(retry_wait)
@ -145,19 +155,20 @@ class Recorder(threading.Thread):
"""A threaded recorder class."""
# 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."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.queue = queue.Queue()
self.queue = queue.Queue() # type: Any
self.quit_object = object()
self.recording_start = dt_util.utcnow()
self.db_url = uri
self.db_ready = threading.Event()
self.engine = None
self._run = None
self.engine = None # type: Any
self._run = None # type: Any
def start_recording(event):
"""Start recording."""
@ -276,7 +287,7 @@ class Recorder(threading.Thread):
run.end = self.recording_start
_LOGGER.warning("Ended unfinished session (id=%s from %s)",
run.run_id, run.start)
Session().add(run)
Session.add(run)
_LOGGER.warning("Found unfinished sessions")
@ -321,7 +332,7 @@ class Recorder(threading.Thread):
if self._commit(_purge_events):
_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
if self.engine.driver == 'sqlite':
@ -346,7 +357,8 @@ class Recorder(threading.Thread):
return False
def _verify_instance():
def _verify_instance() -> None:
"""Throw error if recorder not initialized."""
if _INSTANCE is None:
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
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.helpers.entity import split_entity_id
# SQLAlchemy Schema
# pylint: disable=invalid-name
@ -62,7 +61,7 @@ class States(Base): # type: ignore
__tablename__ = 'states'
state_id = Column(Integer, primary_key=True)
domain = Column(String(64))
entity_id = Column(String(64))
entity_id = Column(String(255))
state = Column(String(255))
attributes = Column(Text)
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.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.9.0']
REQUIREMENTS = ['pyRFXtrx==0.10.1']
DOMAIN = "rfxtrx"
@ -66,6 +66,7 @@ def _valid_device(value, device_type):
key = device.get('packetid')
device.pop('packetid')
key = str(key)
if not len(key) % 2 == 0:
key = '0' + key
@ -130,7 +131,11 @@ def setup(hass, config):
# Log RFXCOM event
if not event.device.id_string:
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.
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
@ -213,13 +218,14 @@ def get_new_device(event, config, device):
if not config[ATTR_AUTOMATIC_ADD]:
return
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
_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,
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}
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
new_device = device(pkt_id, event, datas,
@ -236,7 +242,7 @@ def apply_received_command(event):
return
_LOGGER.debug(
"EntityID: %s device_update. Command: %s",
"Device_id: %s device_update. Command: %s",
device_id,
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):
"""Initialize the zwave rollershutter."""
import libopenzwave
from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._lozwmgr = libopenzwave.PyManager()
self._lozwmgr.create()
self._node = value.node
self._current_position = None
dispatcher.connect(
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."""
if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties()
self.update_ha_state()
_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
def current_position(self):
"""Return the current position of Zwave roller shutter."""
if self._value.data <= 5:
return 100
elif self._value.data >= 95:
return 0
else:
return 100 - self._value.data
if self._current_position is not None:
if self._current_position <= 5:
return 100
elif self._current_position >= 95:
return 0
else:
return 100 - self._current_position
def move_up(self, **kwargs):
"""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):
"""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):
"""Stop the roller shutter."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_SWITCH_BINARY).values():
# Rollershutter will toggle between UP (True), DOWN (False).
# It also stops the shutter if the same value is sent while moving.
value.data = value.data
break
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
and value.label == 'Open':
self._lozwmgr.releaseButton(value.value_id)
break

View file

@ -14,7 +14,8 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
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
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.helpers.entity import Entity
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.
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
import Adafruit_DHT
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
available_sensors = {
"DHT11": Adafruit_DHT.DHT11,
"DHT22": Adafruit_DHT.DHT22,
@ -58,7 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(DHTSensor(data, variable, unit, name))
dev.append(
DHTSensor(data, variable, SENSOR_TYPES[variable][1], name))
except KeyError:
pass
@ -103,7 +104,8 @@ class DHTSensor(Entity):
if self.type == 'temperature':
self._state = round(data['temperature'], 1)
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':
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 time
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
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.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 = []
for resource in config.get("monitored_resources",
FITBIT_DEFAULT_RESOURCE_LIST):
dev.append(FitbitSensor(authd_client, config_path, resource,
hass.config.temperature_unit ==
TEMP_CELSIUS))
hass.config.units.is_metric))
add_devices(dev)
else:

View file

@ -10,7 +10,7 @@ from requests.exceptions import ConnectionError as ConnectError, \
HTTPError, Timeout
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.entity import Entity
from homeassistant.util import Throttle
@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if 'units' in config:
units = config['units']
elif hass.config.temperature_unit == TEMP_CELSIUS:
elif hass.config.units.is_metric:
units = 'si'
else:
units = 'us'

View file

@ -11,8 +11,7 @@ import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
CONF_API_KEY, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
from homeassistant.util import Throttle
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)
if options.get('units') is None:
if hass.config.temperature_unit is TEMP_CELSIUS:
options['units'] = 'metric'
elif hass.config.temperature_unit is TEMP_FAHRENHEIT:
options['units'] = 'imperial'
options['units'] = hass.config.units.name
travel_mode = config.get(CONF_TRAVEL_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._outdoor_temp_sensor = outdoor_temp_sensor
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._indoor_temp = None
@ -109,7 +109,7 @@ class MoldIndicator(Entity):
# convert to celsius if necessary
if unit == TEMP_FAHRENHEIT:
return util.temperature.fahrenheit_to_celcius(temp)
return util.temperature.fahrenheit_to_celsius(temp)
elif unit == TEMP_CELSIUS:
return temp
else:
@ -260,9 +260,9 @@ class MoldIndicator(Entity):
else:
return {
ATTR_DEWPOINT:
util.temperature.celcius_to_fahrenheit(
util.temperature.celsius_to_fahrenheit(
self._dewpoint),
ATTR_CRITICAL_TEMP:
util.temperature.celcius_to_fahrenheit(
util.temperature.celsius_to_fahrenheit(
self._crit_temp),
}

View file

@ -95,6 +95,9 @@ class OctoPrintSensor(Entity):
"""Return the state of the sensor."""
sensor_unit = self.unit_of_measurement
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)
else:
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
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
SENSOR_TYPES['temperature'][1] = hass.config.units.temperature_unit
forecast = config.get('forecast')
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:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(OpenWeatherMapSensor(data, variable, unit))
dev.append(OpenWeatherMapSensor(data, variable,
SENSOR_TYPES[variable][1]))
except KeyError:
pass
if forecast:
SENSOR_TYPES['forecast'] = ['Forecast', None]
dev.append(OpenWeatherMapSensor(data, 'forecast', unit))
dev.append(OpenWeatherMapSensor(data, 'forecast',
SENSOR_TYPES['temperature'][1]))
add_devices(dev)

View file

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

View file

@ -51,7 +51,8 @@ class VeraSensor(VeraDevice, Entity):
def update(self):
"""Update the state."""
if self.vera_device.category == "Temperature Sensor":
current_temp = self.vera_device.temperature
self.current_value = self.vera_device.temperature
vera_temp_units = (
self.vera_device.vera_controller.temperature_units)
@ -60,14 +61,6 @@ class VeraSensor(VeraDevice, Entity):
else:
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":
self.current_value = self.vera_device.light
elif self.vera_device.category == "Humidity Sensor":

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