Merge pull request #2654 from home-assistant/dev

0.25
This commit is contained in:
Paulus Schoutsen 2016-07-30 11:33:41 -07:00 committed by GitHub
commit 06a68d0c62
176 changed files with 3241 additions and 1340 deletions

View File

@ -91,6 +91,7 @@ omit =
homeassistant/components/knx.py homeassistant/components/knx.py
homeassistant/components/switch/knx.py homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py homeassistant/components/binary_sensor/knx.py
homeassistant/components/thermostat/knx.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
@ -129,21 +130,25 @@ omit =
homeassistant/components/joaoapps_join.py homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/x10.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/mpchc.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
@ -151,6 +156,7 @@ omit =
homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
@ -161,7 +167,6 @@ omit =
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py

5
.gitignore vendored
View File

@ -7,9 +7,10 @@ config/custom_components/*
!config/custom_components/example.py !config/custom_components/example.py
!config/custom_components/hello_world.py !config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py !config/custom_components/mqtt_example.py
!config/custom_components/react_panel
tests/config/deps tests/testing_config/deps
tests/config/home-assistant.log tests/testing_config/home-assistant.log
# Hide sublime text stuff # Hide sublime text stuff
*.sublime-project *.sublime-project

View File

@ -8,8 +8,13 @@ matrix:
env: TOXENV=requirements env: TOXENV=requirements
- python: "3.5" - python: "3.5"
env: TOXENV=lint env: TOXENV=lint
- python: "3.5"
env: TOXENV=typing
- python: "3.5" - python: "3.5"
env: TOXENV=py35 env: TOXENV=py35
allow_failures:
- python: "3.5"
env: TOXENV=typing
cache: cache:
directories: directories:
- $HOME/.cache/pip - $HOME/.cache/pip

View File

@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \
COPY requirements_all.txt requirements_all.txt COPY requirements_all.txt requirements_all.txt
# certifi breaks Debian based installs # certifi breaks Debian based installs
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
pip3 install mysqlclient psycopg2
# Copy source # Copy source
COPY . . COPY . .

View File

@ -67,7 +67,7 @@ Build home automation on top of your devices:
- Turn on the lights when people get home after sunset - Turn on the lights when people get home after sunset
- Turn on lights slowly during sunset to compensate for less light - Turn on lights slowly during sunset to compensate for less light
- Turn off all lights and devices when everybody leaves the house - Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/api/>`__ - Offers a `REST API <https://home-assistant.io/developers/rest_api/>`__
and can interface with MQTT for easy integration with other projects and can interface with MQTT for easy integration with other projects
like `OwnTracks <http://owntracks.org/>`__ like `OwnTracks <http://owntracks.org/>`__
- Allow sending notifications using - Allow sending notifications using

View File

@ -0,0 +1,30 @@
"""
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

@ -0,0 +1,415 @@
<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>
<!-- for development, replace with:
<script src="https://fb.me/react-15.2.1.js"></script>
<script src="https://fb.me/react-dom-15.2.1.js"></script>
-->
<!--
CSS taken from ReactJS TodoMVC example by Pete Hunt
http://todomvc.com/examples/react/
-->
<style>
.todoapp input[type="checkbox"] {
outline: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.todoapp .main {
position: relative;
border-top: 1px solid #e6e6e6;
}
.todoapp .todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todoapp .todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todoapp .todo-list li:last-child {
border-bottom: none;
}
.todoapp .todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.todoapp .todo-list li .toggle:focus {
border-left: 3px solid rgba(175, 47, 47, 0.35);
}
.todoapp .todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todoapp .todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todoapp .todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todoapp .todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todoapp .footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.todoapp .footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todoapp .todo-count {
float: left;
text-align: left;
font-weight: 300;
}
.todoapp .toggle-menu {
position: absolute;
right: 15px;
font-weight: 300;
color: rgba(175, 47, 47, 0.75);
}
.todoapp .filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.todoapp .filters li {
display: inline;
}
.todoapp .filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.todoapp .filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.todoapp .filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.todoapp .toggle-all,
.todoapp .todo-list li .toggle {
background: none;
}
.todoapp .todo-list li .toggle {
height: 40px;
}
.todoapp .toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.todoapp .footer {
height: 50px;
}
.todoapp .filters {
bottom: 10px;
}
}
</style>
<dom-module id='ha-panel-react'>
<template>
<style>
:host {
background: #f5f5f5;
display: block;
height: 100%;
overflow: auto;
}
.mount {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
</style>
<div id='mount' class='mount'></div>
</template>
</dom-module>
<script>
// Example uses ES6. Will only work in modern browsers
class TodoMVC extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: 'all',
// load initial value of entities
entities: this.props.hass.reactor.evaluate(
this.props.hass.entityGetters.visibleEntityMap),
};
}
componentDidMount() {
// register to entity updates
this._unwatchHass = this.props.hass.reactor.observe(
this.props.hass.entityGetters.visibleEntityMap,
entities => this.setState({entities}))
}
componentWillUnmount() {
// unregister to entity updates
this._unwatchHass();
}
handlePickFilter(filter, ev) {
ev.preventDefault();
this.setState({filter});
}
handleEntityToggle(entity, ev) {
this.props.hass.serviceActions.callService(
entity.domain, 'toggle', { entity_id: entity.entityId });
}
handleToggleMenu(ev) {
ev.preventDefault();
Polymer.Base.fire('open-menu', null, {node: ev.target});
}
entityRow(entity) {
const completed = entity.state === 'on';
return React.createElement(
'li', {
className: completed && 'completed',
key: entity.entityId,
},
React.createElement(
"div", { className: "view" },
React.createElement(
"input", {
checked: completed,
className: "toggle",
type: "checkbox",
onChange: ev => this.handleEntityToggle(entity, ev),
}),
React.createElement("label", null, entity.entityDisplay)));
}
filterRow(filter) {
return React.createElement(
"li", { key: filter },
React.createElement(
"a", {
href: "#",
className: this.state.filter === filter && "selected",
onClick: ev => this.handlePickFilter(filter, ev),
},
filter.substring(0, 1).toUpperCase() + filter.substring(1)
)
);
}
render() {
const { entities, filter } = this.state;
if (!entities) return null;
const filters = ['all', 'light', 'switch'];
const showEntities = filter === 'all' ?
entities.filter(ent => filters.includes(ent.domain)) :
entities.filter(ent => ent.domain == filter);
return React.createElement(
'div', { className: 'todoapp-wrapper' },
React.createElement(
"section", { className: "todoapp" },
React.createElement(
"div", null,
React.createElement(
"header", { className: "header" },
React.createElement("h1", null, this.props.title || "todos")
),
React.createElement(
"section", { className: "main" },
React.createElement(
"ul", { className: "todo-list" },
showEntities.valueSeq().map(ent => this.entityRow(ent)))
)
),
React.createElement(
"footer", { className: "footer" },
React.createElement(
"span", { className: "todo-count" },
showEntities.filter(ent => ent.state === 'off').size + " items left"
),
React.createElement(
"ul", { className: "filters" },
filters.map(filter => this.filterRow(filter))
),
!this.props.showMenu && React.createElement(
"a", {
className: "toggle-menu",
href: '#',
onClick: ev => this.handleToggleMenu(ev),
},
"Show menu"
)
)
));
}
}
Polymer({
is: 'ha-panel-react',
properties: {
// Home Assistant object
hass: {
type: Object,
},
// If should render in narrow mode
narrow: {
type: Boolean,
value: false,
},
// If sidebar is currently shown
showMenu: {
type: Boolean,
value: false,
},
// Home Assistant panel info
// panel.config contains config passed to register_panel serverside
panel: {
type: Object,
}
},
// This will make sure we forward changed properties to React
observers: [
'propsChanged(hass, narrow, showMenu, panel)',
],
// Mount React when element attached
attached: function () {
this.mount(this.hass, this.narrow, this.showMenu, this.panel);
},
// Called when properties change
propsChanged: function (hass, narrow, showMenu, panel) {
this.mount(hass, narrow, showMenu, panel);
},
// Render React. Debounce in case multiple properties change.
mount: function (hass, narrow, showMenu, panel) {
this.debounce('mount', function () {
ReactDOM.render(React.createElement(TodoMVC, {
hass: hass,
narrow: narrow,
showMenu: showMenu,
title: panel.config ? panel.config.title : null
}), this.$.mount);
}.bind(this));
},
// Unmount React node when panel no longer in use.
detached: function () {
ReactDOM.unmountComponentAtNode(this.$.mount);
},
});
</script>

View File

@ -8,6 +8,8 @@ import subprocess
import sys import sys
import threading import threading
from typing import Optional, List
from homeassistant.const import ( from homeassistant.const import (
__version__, __version__,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
@ -16,7 +18,7 @@ from homeassistant.const import (
) )
def validate_python(): def validate_python() -> None:
"""Validate we're running the right Python version.""" """Validate we're running the right Python version."""
major, minor = sys.version_info[:2] major, minor = sys.version_info[:2]
req_major, req_minor = REQUIRED_PYTHON_VER req_major, req_minor = REQUIRED_PYTHON_VER
@ -27,7 +29,7 @@ def validate_python():
sys.exit(1) sys.exit(1)
def ensure_config_path(config_dir): def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory.""" """Validate the configuration directory."""
import homeassistant.config as config_util import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'deps') lib_dir = os.path.join(config_dir, 'deps')
@ -56,7 +58,7 @@ def ensure_config_path(config_dir):
sys.exit(1) sys.exit(1)
def ensure_config_file(config_dir): def ensure_config_file(config_dir: str) -> str:
"""Ensure configuration file exists.""" """Ensure configuration file exists."""
import homeassistant.config as config_util import homeassistant.config as config_util
config_path = config_util.ensure_config_exists(config_dir) config_path = config_util.ensure_config_exists(config_dir)
@ -68,7 +70,7 @@ def ensure_config_file(config_dir):
return config_path return config_path
def get_arguments(): def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments.""" """Get parsed passed in arguments."""
import homeassistant.config as config_util import homeassistant.config as config_util
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
@ -125,12 +127,12 @@ def get_arguments():
arguments = parser.parse_args() arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner: if os.name != "posix" or arguments.debug or arguments.runner:
arguments.daemon = False setattr(arguments, 'daemon', False)
return arguments return arguments
def daemonize(): def daemonize() -> None:
"""Move current process to daemon process.""" """Move current process to daemon process."""
# Create first fork # Create first fork
pid = os.fork() pid = os.fork()
@ -155,7 +157,7 @@ def daemonize():
os.dup2(outfd.fileno(), sys.stderr.fileno()) os.dup2(outfd.fileno(), sys.stderr.fileno())
def check_pid(pid_file): def check_pid(pid_file: str) -> None:
"""Check that HA is not already running.""" """Check that HA is not already running."""
# Check pid file # Check pid file
try: try:
@ -177,7 +179,7 @@ def check_pid(pid_file):
sys.exit(1) sys.exit(1)
def write_pid(pid_file): def write_pid(pid_file: str) -> None:
"""Create a PID File.""" """Create a PID File."""
pid = os.getpid() pid = os.getpid()
try: try:
@ -187,7 +189,7 @@ def write_pid(pid_file):
sys.exit(1) sys.exit(1)
def closefds_osx(min_fd, max_fd): def closefds_osx(min_fd: int, max_fd: int) -> None:
"""Make sure file descriptors get closed when we restart. """Make sure file descriptors get closed when we restart.
We cannot call close on guarded fds, and we cannot easily test which fds We cannot call close on guarded fds, and we cannot easily test which fds
@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd):
pass pass
def cmdline(): def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance.""" """Collect path and arguments to re-execute the current hass instance."""
if sys.argv[0].endswith('/__main__.py'): if sys.argv[0].endswith('/__main__.py'):
modulepath = os.path.dirname(sys.argv[0]) modulepath = os.path.dirname(sys.argv[0])
@ -213,16 +215,17 @@ def cmdline():
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon'] return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
def setup_and_run_hass(config_dir, args): def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]:
"""Setup HASS and run.""" """Setup HASS and run."""
from homeassistant import bootstrap from homeassistant import bootstrap
# Run a simple daemon runner process on Windows to handle restarts # Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv: if os.name == 'nt' and '--runner' not in sys.argv:
args = cmdline() + ['--runner'] nt_args = cmdline() + ['--runner']
while True: while True:
try: try:
subprocess.check_call(args) subprocess.check_call(nt_args)
sys.exit(0) sys.exit(0)
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE: if exc.returncode != RESTART_EXIT_CODE:
@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args):
log_rotate_days=args.log_rotate_days) log_rotate_days=args.log_rotate_days)
if hass is None: if hass is None:
return return None
if args.open_ui: if args.open_ui:
def open_browser(event): def open_browser(event):
@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args):
return exit_code return exit_code
def try_to_restart(): def try_to_restart() -> None:
"""Attempt to clean up state and start a new homeassistant instance.""" """Attempt to clean up state and start a new homeassistant instance."""
# Things should be mostly shut down already at this point, now just try # Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind. # to clean up things that may have been left behind.
@ -303,7 +306,7 @@ def try_to_restart():
os.execv(args[0], args) os.execv(args[0], args)
def main(): def main() -> int:
"""Start Home Assistant.""" """Start Home Assistant."""
validate_python() validate_python()

View File

@ -7,6 +7,9 @@ import sys
from collections import defaultdict from collections import defaultdict
from threading import RLock from threading import RLock
from types import ModuleType
from typing import Any, Optional, Dict
import voluptuous as vol import voluptuous as vol
import homeassistant.components as core_components import homeassistant.components as core_components
@ -30,7 +33,8 @@ ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log' ERROR_LOG_FILENAME = 'home-assistant.log'
def setup_component(hass, domain, config=None): def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies.""" """Setup a component and all its dependencies."""
if domain in hass.config.components: if domain in hass.config.components:
return True return True
@ -53,7 +57,8 @@ def setup_component(hass, domain, config=None):
return True return True
def _handle_requirements(hass, component, name): def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool:
"""Install the requirements for a component.""" """Install the requirements for a component."""
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'): if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True return True
@ -67,9 +72,10 @@ def _handle_requirements(hass, component, name):
return True return True
def _setup_component(hass, domain, config): def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
"""Setup a component for Home Assistant.""" """Setup a component for Home Assistant."""
# pylint: disable=too-many-return-statements,too-many-branches # pylint: disable=too-many-return-statements,too-many-branches
# pylint: disable=too-many-statements
if domain in hass.config.components: if domain in hass.config.components:
return True return True
@ -147,9 +153,15 @@ def _setup_component(hass, domain, config):
_CURRENT_SETUP.append(domain) _CURRENT_SETUP.append(domain)
try: try:
if not component.setup(hass, config): result = component.setup(hass, config)
if result is False:
_LOGGER.error('component %s failed to initialize', domain) _LOGGER.error('component %s failed to initialize', domain)
return False return False
elif result is not True:
_LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain)
loader.set_component(domain, None)
return False
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain) _LOGGER.exception('Error during setup of component %s', domain)
return False return False
@ -169,7 +181,8 @@ def _setup_component(hass, domain, config):
return True return True
def prepare_setup_platform(hass, config, domain, platform_name): def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup.""" """Load a platform and makes sure dependencies are setup."""
_ensure_loader_prepared(hass) _ensure_loader_prepared(hass)
@ -202,9 +215,14 @@ def prepare_setup_platform(hass, config, domain, platform_name):
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments # pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True, def from_config_dict(config: Dict[str, Any],
verbose=False, skip_pip=False, hass: Optional[core.HomeAssistant]=None,
log_rotate_days=None): config_dir: Optional[str]=None,
enable_log: bool=True,
verbose: bool=False,
skip_pip: bool=False,
log_rotate_days: Any=None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a config dict. """Try to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies. Dynamically loads required components and its dependencies.
@ -266,8 +284,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
return hass return hass
def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, def from_config_file(config_path: str,
log_rotate_days=None): hass: Optional[core.HomeAssistant]=None,
verbose: bool=False,
skip_pip: bool=True,
log_rotate_days: Any=None):
"""Read the configuration file and try to start all the functionality. """Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given, Will add functionality to 'hass' parameter if given,
@ -292,7 +313,8 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
skip_pip=skip_pip) skip_pip=skip_pip)
def enable_logging(hass, verbose=False, log_rotate_days=None): def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None:
"""Setup the logging.""" """Setup the logging."""
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
@ -343,12 +365,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
'Unable to setup error log %s (access denied)', err_log_path) 'Unable to setup error log %s (access denied)', err_log_path)
def _ensure_loader_prepared(hass): def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
"""Ensure Home Assistant loader is prepared.""" """Ensure Home Assistant loader is prepared."""
if not loader.PREPARED: if not loader.PREPARED:
loader.prepare(hass) loader.prepare(hass)
def _mount_local_lib_path(config_dir): def _mount_local_lib_path(config_dir: str) -> None:
"""Add local library to Python Path.""" """Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps')) sys.path.insert(0, os.path.join(config_dir, 'deps'))

View File

@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/
""" """
import logging import logging
import homeassistant.util.dt as dt_util
from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED)
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice) BinarySensorDevice)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice):
self._state = False self._state = False
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""

View File

@ -13,14 +13,15 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class # These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = { SENSOR_TYPES = {
"opened": "opening", "opened": "opening",
"brightness": "light", "brightness": "light",
"vibration": "vibration", "vibration": "vibration",
"loudness": "sound" "loudness": "sound",
"liquid_detected": "moisture"
} }
@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
return self.wink.vibration_boolean() return self.wink.vibration_boolean()
elif self.capability == "brightness": elif self.capability == "brightness":
return self.wink.brightness_boolean() return self.wink.brightness_boolean()
elif self.capability == "liquid_detected":
return self.wink.liquid_boolean()
else: else:
return self.wink.state() return self.wink.state()

View File

@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
def value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_ha_state() self.update_ha_state()

View File

@ -13,7 +13,8 @@ ATTR_URL = 'url'
ATTR_URL_DEFAULT = 'https://www.google.com' ATTR_URL_DEFAULT = 'https://www.google.com'
SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ SERVICE_BROWSE_URL_SCHEMA = vol.Schema({
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url, # pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(),
}) })

View File

@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/ https://home-assistant.io/components/device_tracker.icloud/
""" """
import logging import logging
import re import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
from homeassistant.util import slugify
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT,
PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.8.3'] REQUIREMENTS = ['pyicloud==0.9.1']
CONF_INTERVAL = 'interval' CONF_INTERVAL = 'interval'
DEFAULT_INTERVAL = 8 KEEPALIVE_INTERVAL = 4
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): vol.Coerce(str),
vol.Required(CONF_PASSWORD): vol.Coerce(str),
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int),
vol.Range(min=1))
})
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
@ -23,63 +34,67 @@ def setup_scanner(hass, config, see):
from pyicloud import PyiCloudService from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException from pyicloud.exceptions import PyiCloudFailedLoginException
from pyicloud.exceptions import PyiCloudNoDevicesException from pyicloud.exceptions import PyiCloudNoDevicesException
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
# Get the username and password from the configuration. username = config[CONF_USERNAME]
username = config.get(CONF_USERNAME) password = config[CONF_PASSWORD]
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify a username and password')
return False
try: try:
_LOGGER.info('Logging into iCloud Account') _LOGGER.info('Logging into iCloud Account')
# Attempt the login to iCloud # Attempt the login to iCloud
api = PyiCloudService(username, api = PyiCloudService(username, password, verify=True)
password,
verify=True)
except PyiCloudFailedLoginException as error: except PyiCloudFailedLoginException as error:
_LOGGER.exception('Error logging into iCloud Service: %s', error) _LOGGER.exception('Error logging into iCloud Service: %s', error)
return False return False
def keep_alive(now): def keep_alive(now):
"""Keep authenticating iCloud connection.""" """Keep authenticating iCloud connection.
The session timeouts if we are not using it so we
have to re-authenticate & this will send an email.
"""
api.authenticate() api.authenticate()
_LOGGER.info("Authenticate against iCloud") _LOGGER.info("Authenticate against iCloud")
track_utc_time_change(hass, keep_alive, second=0) seen_devices = {}
def update_icloud(now): def update_icloud(now):
"""Authenticate against iCloud and scan for devices.""" """Authenticate against iCloud and scan for devices."""
try: try:
# The session timeouts if we are not using it so we keep_alive(None)
# have to re-authenticate. This will send an email.
api.authenticate()
# Loop through every device registered with the iCloud account # Loop through every device registered with the iCloud account
for device in api.devices: for device in api.devices:
status = device.status() status = device.status()
dev_id = slugify(status['name'].replace(' ', '', 99))
# An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id))
if entity is None and dev_id in seen_devices:
continue
seen_devices[dev_id] = True
location = device.location() location = device.location()
# If the device has a location add it. If not do nothing # If the device has a location add it. If not do nothing
if location: if location:
see( see(
dev_id=re.sub(r"(\s|\W|')", dev_id=dev_id,
'',
status['name']),
host_name=status['name'], host_name=status['name'],
gps=(location['latitude'], location['longitude']), gps=(location['latitude'], location['longitude']),
battery=status['batteryLevel']*100, battery=status['batteryLevel']*100,
gps_accuracy=location['horizontalAccuracy'] gps_accuracy=location['horizontalAccuracy']
) )
else:
# No location found for the device so continue
continue
except PyiCloudNoDevicesException: except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!') _LOGGER.info('No iCloud Devices found!')
track_utc_time_change( hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud)
hass, update_icloud,
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)), update_minutes = list(range(0, 60, config[CONF_INTERVAL]))
second=0 # Schedule keepalives between the updates
) keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL)
if x not in update_minutes)
track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes)
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes)
return True return True

View File

@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery" DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.7'] REQUIREMENTS = ['netdisco==0.7.0']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds
@ -30,6 +30,7 @@ SERVICE_HANDLERS = {
'roku': ('media_player', 'roku'), 'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'), 'sonos': ('media_player', 'sonos'),
'logitech_mediaserver': ('media_player', 'squeezebox'), 'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'),
} }

View File

@ -24,7 +24,8 @@ ATTR_URL = "url"
ATTR_SUBDIR = "subdir" ATTR_SUBDIR = "subdir"
SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): vol.Url, # pylint: disable=no-value-for-parameter
vol.Required(ATTR_URL): vol.Url(),
vol.Optional(ATTR_SUBDIR): cv.string, vol.Optional(ATTR_SUBDIR): cv.string,
}) })

View File

@ -1,37 +1,130 @@
"""Handle the frontend for Home Assistant.""" """Handle the frontend for Home Assistant."""
import hashlib
import logging
import os import os
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.components import api from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version from .version import FINGERPRINTS
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api'] DEPENDENCIES = ['api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
PANELS = {}
# To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, title=None, icon=None,
url_name=None, config=None):
"""Register a built-in panel."""
# pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development:
url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name))
else:
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)
def register_panel(hass, component_name, path, md5=None, title=None, icon=None,
url_name=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)
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_name in PANELS:
_LOGGER.warning('Overwriting component %s', url_name)
if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s',
component_name, path)
return
if md5 is None:
with open(path) as fil:
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
data = {
'url_name': url_name,
'component_name': component_name,
}
if title:
data['title'] = title
if icon:
data['icon'] = icon
if config is not None:
data['config'] = config
if url is not None:
data['url'] = url
else:
url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url
PANELS[url_name] = data
def setup(hass, config): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
hass.wsgi.register_view(IndexView)
hass.wsgi.register_view(BootstrapView) hass.wsgi.register_view(BootstrapView)
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
if hass.wsgi.development: if hass.wsgi.development:
sw_path = "home-assistant-polymer/build/service_worker.js" sw_path = "home-assistant-polymer/build/service_worker.js"
else: else:
sw_path = "service_worker.js" sw_path = "service_worker.js"
hass.wsgi.register_static_path( hass.wsgi.register_static_path("/service_worker.js",
"/service_worker.js", os.path.join(STATIC_PATH, sw_path), 0)
os.path.join(www_static_path, sw_path), hass.wsgi.register_static_path("/robots.txt",
0 os.path.join(STATIC_PATH, "robots.txt"))
) hass.wsgi.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path(
"/robots.txt",
os.path.join(www_static_path, "robots.txt")
)
hass.wsgi.register_static_path("/static", www_static_path)
hass.wsgi.register_static_path("/local", hass.config.path('www')) hass.wsgi.register_static_path("/local", hass.config.path('www'))
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'):
register_built_in_panel(hass, panel)
def register_frontend_index(event):
"""Register the frontend index urls.
Done when Home Assistant is started so that all panels are known.
"""
hass.wsgi.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
return True return True
@ -48,6 +141,7 @@ class BootstrapView(HomeAssistantView):
'states': self.hass.states.all(), 'states': self.hass.states.all(),
'events': api.events_json(self.hass), 'events': api.events_json(self.hass),
'services': api.services_json(self.hass), 'services': api.services_json(self.hass),
'panels': PANELS,
}) })
@ -57,16 +151,15 @@ class IndexView(HomeAssistantView):
url = '/' url = '/'
name = "frontend:index" name = "frontend:index"
requires_auth = False requires_auth = False
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState', extra_urls = ['/states', '/states/<entity:entity_id>']
'/devEvent', '/devInfo', '/devTemplate',
'/states', '/states/<entity:entity_id>']
def __init__(self, hass): def __init__(self, hass, extra_urls):
"""Initialize the frontend view.""" """Initialize the frontend view."""
super().__init__(hass) super().__init__(hass)
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment( self.templates = Environment(
loader=FileSystemLoader( loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/') os.path.join(os.path.dirname(__file__), 'templates/')
@ -76,32 +169,32 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None): def get(self, request, entity_id=None):
"""Serve the index view.""" """Serve the index view."""
if self.hass.wsgi.development: if self.hass.wsgi.development:
core_url = '/static/home-assistant-polymer/build/_core_compiled.js' core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html' ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
map_url = ('/static/home-assistant-polymer/src/layouts/'
'partial-map.html')
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
'dev-tools.html')
else: else:
core_url = '/static/core-{}.js'.format(version.CORE) core_url = '/static/core-{}.js'.format(
ui_url = '/static/frontend-{}.html'.format(version.UI) FINGERPRINTS['core.js'])
map_url = '/static/partial-map-{}.html'.format(version.MAP) ui_url = '/static/frontend-{}.html'.format(
dev_url = '/static/dev-tools-{}.html'.format(version.DEV) FINGERPRINTS['frontend.html'])
if request.path == '/':
panel = 'states'
else:
panel = request.path.split('/')[1]
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
# auto login if no password was set # auto login if no password was set
if self.hass.config.api.api_password is None: no_auth = 'false' if self.hass.config.api.api_password else 'true'
auth = 'true'
else:
auth = 'false'
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = self.templates.get_template('index.html') template = self.templates.get_template('index.html')
# pylint is wrong # pylint is wrong
# pylint: disable=no-member # pylint: disable=no-member
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION) icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url)
return self.Response(resp, mimetype='text/html') return self.Response(resp, mimetype='text/html')

View File

@ -1,2 +0,0 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "758957b7ea989d6beca60e218ea7f7dd"

View File

@ -5,19 +5,26 @@
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json'> <link rel='manifest' href='/static/manifest.json'>
<link rel='icon' href='/static/favicon.ico'> <link rel='icon' href='/static/icons/favicon.ico'>
<link rel='apple-touch-icon' sizes='180x180' <link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'> href='/static/icons/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'> <meta name='apple-mobile-web-app-capable' content='yes'>
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/> <meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/> <meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/> <meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/> <meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
<meta name="msapplication-TileColor" content="#3fbbf4ff"/> <meta name="msapplication-TileColor" content="#3fbbf4ff"/>
<meta name='mobile-web-app-capable' content='yes'> <meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, user-scalable=no'> <meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'> <meta name='theme-color' content='#03a9f4'>
<style> <style>
body {
font-family: 'Roboto', 'Noto', sans-serif;
font-weight: 300;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
#ha-init-skeleton { #ha-init-skeleton {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
@ -65,23 +72,23 @@
.getElementById('ha-init-skeleton') .getElementById('ha-init-skeleton')
.classList.add('error'); .classList.add('error');
}; };
window.noAuth = {{ auth }}; window.noAuth = {{ no_auth }};
window.deferredLoading = { window.Polymer = {lazyRegister: true, useNativeCSSProperties: true, dom: 'shady'};
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
</script> </script>
</head> </head>
<body fullbleed> <body>
<div id='ha-init-skeleton'> <div id='ha-init-skeleton'>
<img src='/static/favicon-192x192.png' height='192'> <img src='/static/icons/favicon-192x192.png' height='192'>
<paper-spinner active></paper-spinner> <paper-spinner active></paper-spinner>
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a> Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
</div> </div>
<home-assistant icons='{{ icons }}'></home-assistant> <home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #} {# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script src='{{ core_url }}'></script> <script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()' async> <link rel='import' href='{{ ui_url }}' onerror='initError()'>
{% if panel_url %}
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
{% endif %}
<link rel='import' href='{{ icons_url }}' async> <link rel='import' href='{{ icons_url }}' async>
<script> <script>
var webComponentsSupported = ( var webComponentsSupported = (
@ -89,11 +96,11 @@
'import' in document.createElement('link') && 'import' in document.createElement('link') &&
'content' in document.createElement('template')); 'content' in document.createElement('template'));
if (!webComponentsSupported) { if (!webComponentsSupported) {
var script = document.createElement('script') var e = document.createElement('script');
script.async = true e.async = true;
script.onerror = initError; e.onerror = initError;
script.src = '/static/webcomponents-lite.min.js' e.src = '/static/webcomponents-lite.min.js';
document.head.appendChild(script) document.head.appendChild(e);
} }
</script> </script>
</body> </body>

View File

@ -1,5 +1,16 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f" FINGERPRINTS = {
MAP = "c922306de24140afd14f857f927bf8f0" "core.js": "bc78f21f5280217aa2c78dfc5848134f",
DEV = "b7079ac3121b95b9856e5603a6d8a263" "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"
}

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

@ -1 +1 @@
Subproject commit 718384f22aa0a689190a4d3f41b5e9ed091c80a3 Subproject commit 697f9397de357cec9662626575fc01d6f921ef22

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -7,22 +7,22 @@
"background_color": "#FFFFFF", "background_color": "#FFFFFF",
"icons": [ "icons": [
{ {
"src": "/static/favicon-192x192.png", "src": "/static/icons/favicon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/static/favicon-384x384.png", "src": "/static/icons/favicon-384x384.png",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/static/favicon-512x512.png", "src": "/static/icons/favicon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/static/favicon-1024x1024.png", "src": "/static/icons/favicon-1024x1024.png",
"sizes": "1024x1024", "sizes": "1024x1024",
"type": "image/png" "type": "image/png"
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +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>

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

@ -0,0 +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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.components.wink import WinkDevice from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -44,7 +44,6 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
from pydispatch import dispatcher from pydispatch import dispatcher
ZWaveDeviceEntity.__init__(self, value, DOMAIN) ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._state = value.data self._state = value.data
dispatcher.connect( dispatcher.connect(
self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
@ -53,7 +52,7 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id:
self._state = value.data self._state = value.data
self.update_ha_state(True) self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value) _LOGGER.debug("Value changed on network %s", value)
@property @property

View File

@ -12,8 +12,8 @@ import voluptuous as vol
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNKNOWN, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
ATTR_ASSUMED_STATE, ) STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
from homeassistant.helpers.entity import ( from homeassistant.helpers.entity import (
Entity, generate_entity_id, split_entity_id) Entity, generate_entity_id, split_entity_id)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
@ -64,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema({
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME),
(STATE_OPEN, STATE_CLOSED)] (STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)]
def _get_group_on_off(state): def _get_group_on_off(state):
@ -304,8 +304,9 @@ class Group(Entity):
if gr_on is None: if gr_on is None:
return return
if tr_state is None or (gr_state == gr_on and if tr_state is None or ((gr_state == gr_on and
tr_state.state == gr_off): tr_state.state == gr_off) or
tr_state.state not in (gr_on, gr_off)):
if states is None: if states is None:
states = self._tracking_states states = self._tracking_states

View File

@ -4,13 +4,13 @@ Provide pre-made queries on top of the recorder component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history/ https://home-assistant.io/components/history/
""" """
import re
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script from homeassistant.components import recorder, script
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history' DOMAIN = 'history'
@ -19,9 +19,6 @@ DEPENDENCIES = ['recorder', 'http']
SIGNIFICANT_DOMAINS = ('thermostat',) SIGNIFICANT_DOMAINS = ('thermostat',)
IGNORE_DOMAINS = ('zone', 'scene',) IGNORE_DOMAINS = ('zone', 'scene',)
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
def last_5_states(entity_id): def last_5_states(entity_id):
"""Return the last 5 states for entity_id.""" """Return the last 5 states for entity_id."""
@ -153,6 +150,7 @@ def setup(hass, config):
"""Setup the history hooks.""" """Setup the history hooks."""
hass.wsgi.register_view(Last5StatesView) hass.wsgi.register_view(Last5StatesView)
hass.wsgi.register_view(HistoryPeriodView) hass.wsgi.register_view(HistoryPeriodView)
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True return True
@ -173,14 +171,14 @@ class HistoryPeriodView(HomeAssistantView):
url = '/api/history/period' url = '/api/history/period'
name = 'api:history:view-period' name = 'api:history:view-period'
extra_urls = ['/api/history/period/<date:date>'] extra_urls = ['/api/history/period/<datetime:datetime>']
def get(self, request, date=None): def get(self, request, datetime=None):
"""Return history over a period of time.""" """Return history over a period of time."""
one_day = timedelta(days=1) one_day = timedelta(days=1)
if date: if datetime:
start_time = dt_util.as_utc(dt_util.start_of_local_day(date)) start_time = dt_util.as_utc(datetime)
else: else:
start_time = dt_util.utcnow() - one_day start_time = dt_util.utcnow() - one_day

View File

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

View File

@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = "http" DOMAIN = "http"
REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10") REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10")
CONF_API_PASSWORD = "api_password" CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
@ -216,9 +216,29 @@ def routing_map(hass):
"""Convert date to url value.""" """Convert date to url value."""
return value.isoformat() return value.isoformat()
class DateTimeValidator(BaseConverter):
"""Validate datetimes in urls formatted per ISO 8601."""
regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \
r'\.\d+([+-][0-2]\d:[0-5]\d|Z)'
def to_python(self, value):
"""Validate and convert date."""
parsed = dt_util.parse_datetime(value)
if parsed is None:
raise ValidationError()
return parsed
def to_url(self, value):
"""Convert date to url value."""
return value.isoformat()
return Map(converters={ return Map(converters={
'entity': EntityValidator, 'entity': EntityValidator,
'date': DateValidator, 'date': DateValidator,
'datetime': DateTimeValidator,
}) })

View File

@ -98,9 +98,10 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
def value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id or \
self._value.node == value.node:
self.update_properties() self.update_properties()
self.update_ha_state(True) self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value) _LOGGER.debug("Value changed on network %s", value)
def update_properties(self): def update_properties(self):
@ -135,7 +136,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33: if value.command_class == 112 and value.index == 33:
self._current_swing_mode = value.data self._current_swing_mode = value.data
self._swing_list = [0, 1] self._swing_list = list(value.data_items)
_LOGGER.debug("self._swing_list=%s", self._swing_list) _LOGGER.debug("self._swing_list=%s", self._swing_list)
@property @property
@ -235,5 +236,5 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
for value in self._node.get_values( for value in self._node.get_values(
class_id=COMMAND_CLASS_CONFIGURATION).values(): class_id=COMMAND_CLASS_CONFIGURATION).values():
if value.command_class == 112 and value.index == 33: if value.command_class == 112 and value.index == 33:
value.data = int(swing_mode) value.data = bytes(swing_mode, 'utf-8')
break break

View File

@ -33,6 +33,7 @@ CONF_PASSWORD = 'password'
CONF_SSL = 'ssl' CONF_SSL = 'ssl'
CONF_VERIFY_SSL = 'verify_ssl' CONF_VERIFY_SSL = 'verify_ssl'
CONF_BLACKLIST = 'blacklist' CONF_BLACKLIST = 'blacklist'
CONF_TAGS = 'tags'
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
@ -56,6 +57,7 @@ def setup(hass, config):
verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool, verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool,
DEFAULT_VERIFY_SSL) DEFAULT_VERIFY_SSL)
blacklist = conf.get(CONF_BLACKLIST, []) blacklist = conf.get(CONF_BLACKLIST, [])
tags = conf.get(CONF_TAGS, {})
try: try:
influx = InfluxDBClient(host=host, port=port, username=username, influx = InfluxDBClient(host=host, port=port, username=username,
@ -99,6 +101,9 @@ def setup(hass, config):
} }
] ]
for tag in tags:
json_body[0]['tags'][tag] = tags[tag]
try: try:
influx.write_points(json_body) influx.write_points(json_body)
except exceptions.InfluxDBClientError: except exceptions.InfluxDBClientError:

View File

@ -34,7 +34,7 @@ SERVICE_SELECT_VALUE = 'select_value'
SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({ SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Required(ATTR_VALUE): vol.Coerce(float),
}) })
@ -152,7 +152,7 @@ class InputSlider(Entity):
def select_value(self, value): def select_value(self, value):
"""Select new value.""" """Select new value."""
num_value = int(value) num_value = float(value)
if num_value < self._minimum or num_value > self._maximum: if num_value < self._minimum or num_value > self._maximum:
_LOGGER.warning('Invalid value: %s (range %s - %s)', _LOGGER.warning('Invalid value: %s (range %s - %s)',
num_value, self._minimum, self._maximum) num_value, self._minimum, self._maximum)

View File

@ -19,26 +19,19 @@ DOMAIN = 'joaoapps_join'
CONF_DEVICE_ID = 'device_id' CONF_DEVICE_ID = 'device_id'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [{
vol.Required(CONF_DEVICE_ID): cv.string, vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string vol.Optional(CONF_API_KEY): cv.string
}) }])
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
def setup(hass, config): def register_device(hass, device_id, api_key, name):
"""Setup Join services.""" """Method to register services for each join device listed."""
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms, from pyjoin import (ring_device, set_wallpaper, send_sms,
send_file, send_url, send_notification) send_file, send_url, send_notification)
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
api_key = config[DOMAIN].get(CONF_API_KEY)
name = config[DOMAIN].get(CONF_NAME)
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
def ring_service(service): def ring_service(service):
"""Service to ring devices.""" """Service to ring devices."""
@ -69,7 +62,6 @@ def setup(hass, config):
sms_text=service.data.get('message'), sms_text=service.data.get('message'),
api_key=api_key) api_key=api_key)
name = name.lower().replace(" ", "_") + "_" if name else ""
hass.services.register(DOMAIN, name + 'ring', ring_service) hass.services.register(DOMAIN, name + 'ring', ring_service)
hass.services.register(DOMAIN, name + 'set_wallpaper', hass.services.register(DOMAIN, name + 'set_wallpaper',
set_wallpaper_service) set_wallpaper_service)
@ -77,4 +69,19 @@ def setup(hass, config):
hass.services.register(DOMAIN, name + 'send_file', send_file_service) hass.services.register(DOMAIN, name + 'send_file', send_file_service)
hass.services.register(DOMAIN, name + 'send_url', send_url_service) hass.services.register(DOMAIN, name + 'send_url', send_url_service)
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service) hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
def setup(hass, config):
"""Setup Join services."""
from pyjoin import get_devices
for device in config[DOMAIN]:
device_id = device.get(CONF_DEVICE_ID)
api_key = device.get(CONF_API_KEY)
name = device.get(CONF_NAME)
name = name.lower().replace(" ", "_") + "_" if name else ""
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
register_device(hass, device_id, api_key, name)
return True return True

View File

@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
DOMAIN = "knx" DOMAIN = "knx"
REQUIREMENTS = ['knxip==0.3.0'] REQUIREMENTS = ['knxip==0.3.2']
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received" EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
@ -45,7 +45,12 @@ def setup(hass, config):
KNXTUNNEL = KNXIPTunnel(host, port) KNXTUNNEL = KNXIPTunnel(host, port)
try: try:
KNXTUNNEL.connect() res = KNXTUNNEL.connect()
_LOGGER.debug("Res = %s", res)
if not res:
_LOGGER.exception("Could not connect to KNX/IP interface %s", host)
return False
except KNXException as ex: except KNXException as ex:
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex) _LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
KNXTUNNEL = None KNXTUNNEL = None
@ -74,7 +79,10 @@ class KNXConfig(object):
self.config = config self.config = config
self.should_poll = config.get("poll", True) self.should_poll = config.get("poll", True)
if config.get("address"):
self._address = parse_group_address(config.get("address")) self._address = parse_group_address(config.get("address"))
else:
self._address = None
if self.config.get("state_address"): if self.config.get("state_address"):
self._state_address = parse_group_address( self._state_address = parse_group_address(
self.config.get("state_address")) self.config.get("state_address"))
@ -198,7 +206,7 @@ class KNXGroupAddress(Entity):
return False return False
class KNXMultiAddressDevice(KNXGroupAddress): class KNXMultiAddressDevice(Entity):
"""Representation of devices connected to a multiple KNX group address. """Representation of devices connected to a multiple KNX group address.
This is needed for devices like dimmers or shutter actuators as they have This is needed for devices like dimmers or shutter actuators as they have
@ -218,18 +226,21 @@ class KNXMultiAddressDevice(KNXGroupAddress):
""" """
from knxip.core import parse_group_address, KNXException from knxip.core import parse_group_address, KNXException
super().__init__(self, hass, config) self._config = config
self._state = False
self.config = config self._data = None
_LOGGER.debug("Initalizing KNX multi address device")
# parse required addresses # parse required addresses
for name in required: for name in required:
_LOGGER.info(name)
paramname = name + "_address" paramname = name + "_address"
addr = self._config.config.get(paramname) addr = self._config.config.get(paramname)
if addr is None: if addr is None:
_LOGGER.exception("Required KNX group address %s missing", _LOGGER.exception("Required KNX group address %s missing",
paramname) paramname)
raise KNXException("Group address missing in configuration") raise KNXException("Group address for %s missing "
"in configuration", paramname)
addr = parse_group_address(addr) addr = parse_group_address(addr)
self.names[addr] = name self.names[addr] = name
@ -244,23 +255,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
_LOGGER.exception("Cannot parse group address %s", addr) _LOGGER.exception("Cannot parse group address %s", addr)
self.names[addr] = name self.names[addr] = name
def handle_frame(frame): @property
"""Handle an incoming KNX frame. def name(self):
"""The entity's display name."""
return self._config.name
Handle an incoming frame and update our status if it contains @property
information relating to this device. def config(self):
""" """The entity's configuration."""
addr = frame.data[0] return self._config
if addr in self.names: @property
self.values[addr] = frame.data[1] def should_poll(self):
self.update_ha_state() """Return the state of the polling, if needed."""
return self._config.should_poll
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame) @property
def cache(self):
def group_write_address(self, name, value): """The name given to the entity."""
"""Write to the group address with the given name.""" return self._config.config.get("cache", True)
KNXTUNNEL.group_write(self.address, [value])
def has_attribute(self, name): def has_attribute(self, name):
"""Check if the attribute with the given name is defined. """Check if the attribute with the given name is defined.
@ -277,7 +290,7 @@ class KNXMultiAddressDevice(KNXGroupAddress):
from knxip.core import KNXException from knxip.core import KNXException
addr = None addr = None
for attributename, attributeaddress in self.names.items(): for attributeaddress, attributename in self.names.items():
if attributename == name: if attributename == name:
addr = attributeaddress addr = attributeaddress
@ -293,3 +306,25 @@ class KNXMultiAddressDevice(KNXGroupAddress):
return False return False
return res return res
def set_value(self, name, value):
"""Set the value of a given named attribute."""
from knxip.core import KNXException
addr = None
for attributeaddress, attributename in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
return False
try:
KNXTUNNEL.group_write(addr, value)
except KNXException:
_LOGGER.exception("Unable to write to KNX address: %s",
addr)
return False
return True

View File

@ -0,0 +1,109 @@
"""
Support for Flux lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.flux_led/
"""
import logging
import socket
import voluptuous as vol
from homeassistant.components.light import Light
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip'
'#flux_led==0.3']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "flux_led"
ATTR_NAME = 'name'
DEVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_NAME): cv.string,
})
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): DOMAIN,
vol.Optional('devices', default={}): {cv.string: DEVICE_SCHEMA},
vol.Optional('automatic_add', default=False): cv.boolean,
}, extra=vol.ALLOW_EXTRA)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the Flux lights."""
import flux_led
lights = []
light_ips = []
for ipaddr, device_config in config["devices"].items():
device = {}
device['id'] = device_config[ATTR_NAME]
device['ipaddr'] = ipaddr
light = FluxLight(device)
if light.is_valid:
lights.append(light)
light_ips.append(ipaddr)
if not config['automatic_add']:
add_devices_callback(lights)
return
# Find the bulbs on the LAN
scanner = flux_led.BulbScanner()
scanner.scan(timeout=20)
for device in scanner.getBulbInfo():
light = FluxLight(device)
ipaddr = device['ipaddr']
if light.is_valid and ipaddr not in light_ips:
lights.append(light)
light_ips.append(ipaddr)
add_devices_callback(lights)
class FluxLight(Light):
"""Representation of a Flux light."""
# pylint: disable=too-many-arguments
def __init__(self, device):
"""Initialize the light."""
import flux_led
self._name = device['id']
self._ipaddr = device['ipaddr']
self.is_valid = True
self._bulb = None
try:
self._bulb = flux_led.WifiLedBulb(self._ipaddr)
except socket.error:
self.is_valid = False
_LOGGER.error("Failed to connect to bulb %s, %s",
self._ipaddr, self._name)
@property
def unique_id(self):
"""Return the ID of this light."""
return "{}.{}".format(
self.__class__, self._ipaddr)
@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._bulb.isOn()
def turn_on(self, **kwargs):
"""Turn the specified or all lights on."""
self._bulb.turnOn()
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""
self._bulb.turnOff()
def update(self):
"""Synchronize state with bulb."""
self._bulb.refreshState()

View File

@ -19,24 +19,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup a Hyperion server remote.""" """Setup a Hyperion server remote."""
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
port = config.get("port", 19444) port = config.get("port", 19444)
device = Hyperion(config.get('name', host), host, port) default_color = config.get("default_color", [255, 255, 255])
device = Hyperion(config.get('name', host), host, port, default_color)
if device.setup(): if device.setup():
add_devices_callback([device]) add_devices_callback([device])
return True return True
else:
return False return False
class Hyperion(Light): class Hyperion(Light):
"""Representation of a Hyperion remote.""" """Representation of a Hyperion remote."""
def __init__(self, name, host, port): def __init__(self, name, host, port, default_color):
"""Initialize the light.""" """Initialize the light."""
self._host = host self._host = host
self._port = port self._port = port
self._name = name self._name = name
self._is_available = True self._default_color = default_color
self._rgb_color = [255, 255, 255] self._rgb_color = [0, 0, 0]
@property @property
def name(self): def name(self):
@ -50,38 +50,43 @@ class Hyperion(Light):
@property @property
def is_on(self): def is_on(self):
"""Return true if the device is online.""" """Return true if not black."""
return self._is_available return self._rgb_color != [0, 0, 0]
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the lights on.""" """Turn the lights on."""
if self._is_available:
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR] self._rgb_color = kwargs[ATTR_RGB_COLOR]
else:
self._rgb_color = self._default_color
self.json_request({"command": "color", "priority": 128, self.json_request({"command": "color", "priority": 128,
"color": self._rgb_color}) "color": self._rgb_color})
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Disconnect the remote.""" """Disconnect all remotes."""
self.json_request({"command": "clearall"}) self.json_request({"command": "clearall"})
self._rgb_color = [0, 0, 0]
def update(self): def update(self):
"""Ping the remote.""" """Get the remote's active color."""
# just see if the remote port is open response = self.json_request({"command": "serverinfo"})
self._is_available = self.json_request() if response:
if response["info"]["activeLedColor"] == []:
self._rgb_color = [0, 0, 0]
else:
self._rgb_color =\
response["info"]["activeLedColor"][0]["RGB Value"]
def setup(self): def setup(self):
"""Get the hostname of the remote.""" """Get the hostname of the remote."""
response = self.json_request({"command": "serverinfo"}) response = self.json_request({"command": "serverinfo"})
if response: if response:
if self._name == self._host:
self._name = response["info"]["hostname"] self._name = response["info"]["hostname"]
return True return True
return False return False
def json_request(self, request=None, wait_for_response=False): def json_request(self, request, wait_for_response=False):
"""Communicate with the JSON server.""" """Communicate with the JSON server."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5) sock.settimeout(5)
@ -92,11 +97,6 @@ class Hyperion(Light):
sock.close() sock.close()
return False return False
if not request:
# No communication needed, simple presence detection returns True
sock.close()
return True
sock.send(bytearray(json.dumps(request) + "\n", "utf-8")) sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
try: try:
buf = sock.recv(4096) buf = sock.recv(4096)

View File

@ -6,30 +6,16 @@ https://home-assistant.io/components/light.qwikswitch/
""" """
import logging import logging
import homeassistant.components.qwikswitch as qwikswitch import homeassistant.components.qwikswitch as qwikswitch
from homeassistant.components.light import Light
DEPENDENCIES = ['qwikswitch'] DEPENDENCIES = ['qwikswitch']
class QSLight(qwikswitch.QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Store add_devices for the light components.""" """Add lights from the main Qwikswitch component."""
if discovery_info is None or 'qsusb_id' not in discovery_info: if discovery_info is None:
logging.getLogger(__name__).error( logging.getLogger(__name__).error('Configure Qwikswitch Component.')
'Configure main Qwikswitch component')
return False return False
qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']] add_devices(qwikswitch.QSUSB['light'])
return True
for item in qsusb.ha_devices:
if item['type'] not in ['dim', 'rel']:
continue
dev = QSLight(item, qsusb)
add_devices([dev])
qsusb.ha_objects[item['id']] = dev

View File

@ -6,10 +6,8 @@ https://home-assistant.io/components/light.vera/
""" """
import logging import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.light import ATTR_BRIGHTNESS, Light from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.const import ( from homeassistant.const import (
ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED,
STATE_OFF, STATE_ON) STATE_OFF, STATE_ON)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
@ -56,31 +54,6 @@ class VeraLight(VeraDevice, Light):
self._state = STATE_OFF self._state = STATE_OFF
self.update_ha_state() self.update_ha_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable:
armed = self.vera_device.is_armed
attr[ATTR_ARMED] = 'True' if armed else 'False'
if self.vera_device.is_trippable:
last_tripped = self.vera_device.last_trip
if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat()
else:
attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.is_tripped
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
@property @property
def is_on(self): def is_on(self):
"""Return true if device is on.""" """Return true if device is on."""

View File

@ -14,7 +14,7 @@ from homeassistant.util import color as color_util
from homeassistant.util.color import \ from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View File

@ -0,0 +1,80 @@
"""
Support for X10 lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.x10/
"""
import logging
from subprocess import check_output, CalledProcessError, STDOUT
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
_LOGGER = logging.getLogger(__name__)
def x10_command(command):
"""Execute X10 command and check output."""
return check_output(["heyu"] + command.split(' '), stderr=STDOUT)
def get_status():
"""Get on/off status for all x10 units in default housecode."""
output = check_output("heyu info | grep monitored", shell=True)
return output.decode('utf-8').split(' ')[-1].strip('\n()')
def get_unit_status(code):
"""Get on/off status for given unit."""
unit = int(code[1])
return get_status()[16 - int(unit)] == '1'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the x10 Light platform."""
try:
x10_command("info")
except CalledProcessError as err:
_LOGGER.error(err.output)
return False
add_devices(X10Light(light) for light in config['lights'])
class X10Light(Light):
"""Representation of an X10 Light."""
def __init__(self, light):
"""Initialize an X10 Light."""
self._name = light['name']
self._id = light['id']
self._is_on = False
self._brightness = 0
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self._brightness
@property
def is_on(self):
"""Return true if light is on."""
return self._is_on
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
x10_command("on " + self._id)
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
self._is_on = True
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
x10_command("off " + self._id)
self._is_on = False
def update(self):
"""Fetch new state data for this light."""
self._is_on = get_unit_status(self._id)

View File

@ -8,7 +8,6 @@ import logging
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, DOMAIN, Light ATTR_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave from homeassistant.components import zwave
@ -107,24 +106,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
def _value_changed(self, value): def _value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id != value.value_id: if self._value.value_id == value.value_id or \
return self._value.node == value.node:
if self._refreshing:
self._refreshing = False
self.update_properties() self.update_properties()
else:
def _refresh_value():
"""Used timer callback for delayed value refresh."""
self._refreshing = True
self._value.refresh()
if self._timer is not None and self._timer.isAlive():
self._timer.cancel()
self._timer = Timer(2, _refresh_value)
self._timer.start()
self.update_ha_state() self.update_ha_state()
@property @property

View File

@ -8,7 +8,7 @@ import logging
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.components.vera import ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
@ -32,16 +32,6 @@ class VeraLock(VeraDevice, LockDevice):
self._state = None self._state = None
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def lock(self, **kwargs): def lock(self, **kwargs):
"""Lock the device.""" """Lock the device."""
self.vera_device.lock() self.vera_device.lock()

View File

@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice
from homeassistant.components.wink import WinkDevice from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -46,7 +46,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
def _value_changed(self, value): def _value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id or \
self._value.node == value.node:
self._state = value.data self._state = value.data
self.update_ha_state() self.update_ha_state()

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/logbook/ https://home-assistant.io/components/logbook/
""" """
import logging import logging
import re
from datetime import timedelta from datetime import timedelta
from itertools import groupby from itertools import groupby
@ -14,6 +13,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun from homeassistant.components import recorder, sun
from homeassistant.components.frontend import register_built_in_panel
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START, from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
@ -24,9 +24,7 @@ from homeassistant.helpers import template
from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook" DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http'] DEPENDENCIES = ['recorder', 'frontend']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -75,6 +73,9 @@ def setup(hass, config):
hass.wsgi.register_view(LogbookView) hass.wsgi.register_view(LogbookView)
register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type')
hass.services.register(DOMAIN, 'log', log_message, hass.services.register(DOMAIN, 'log', log_message,
schema=LOG_MESSAGE_SCHEMA) schema=LOG_MESSAGE_SCHEMA)
return True return True
@ -85,16 +86,11 @@ class LogbookView(HomeAssistantView):
url = '/api/logbook' url = '/api/logbook'
name = 'api:logbook' name = 'api:logbook'
extra_urls = ['/api/logbook/<date:date>'] extra_urls = ['/api/logbook/<datetime:datetime>']
def get(self, request, date=None): def get(self, request, datetime=None):
"""Retrieve logbook entries.""" """Retrieve logbook entries."""
if date: start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day())
start_day = dt_util.start_of_local_day(date)
else:
start_day = dt_util.start_of_local_day()
start_day = dt_util.as_utc(start_day)
end_day = start_day + timedelta(days=1) end_day = start_day + timedelta(days=1)
events = recorder.get_model('Events') events = recorder.get_model('Events')

View File

@ -0,0 +1,172 @@
"""Support for the DirecTV recievers."""
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING)
REQUIREMENTS = ['directpy==0.1']
DEFAULT_PORT = 8080
SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK
KNOWN_HOSTS = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the DirecTV platform."""
hosts = []
if discovery_info and discovery_info in KNOWN_HOSTS:
return
if discovery_info is not None:
hosts.append([
'DirecTV_' + discovery_info[1],
discovery_info[0],
DEFAULT_PORT
])
elif CONF_HOST in config:
hosts.append([
config.get(CONF_NAME, 'DirecTV Receiver'),
config[CONF_HOST], DEFAULT_PORT
])
dtvs = []
for host in hosts:
dtvs.append(DirecTvDevice(*host))
KNOWN_HOSTS.append(host)
add_devices(dtvs)
return True
class DirecTvDevice(MediaPlayerDevice):
"""Representation of a DirecTV reciever on the network."""
# pylint: disable=abstract-method
# pylint: disable=too-many-public-methods
def __init__(self, name, host, port):
"""Initialize the device."""
from DirectPy import DIRECTV
self.dtv = DIRECTV(host, port)
self._name = name
self._is_standby = True
self._current = None
def update(self):
"""Retrieve latest state."""
self._is_standby = self.dtv.get_standby()
if self._is_standby:
self._current = None
else:
self._current = self.dtv.get_tuned()
@property
def name(self):
"""Return the name of the device."""
return self._name
# MediaPlayerDevice properties and methods
@property
def state(self):
"""Return the state of the device."""
if self._is_standby:
return STATE_OFF
# haven't determined a way to see if the content is paused
else:
return STATE_PLAYING
@property
def media_content_id(self):
"""Content ID of current playing media."""
if self._is_standby:
return None
else:
return self._current['programId']
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self._is_standby:
return None
else:
return self._current['duration']
@property
def media_title(self):
"""Title of current playing media."""
if self._is_standby:
return None
else:
return self._current['title']
@property
def media_series_title(self):
"""Title of current episode of TV show."""
if self._is_standby:
return None
else:
if 'episodeTitle' in self._current:
return self._current['episodeTitle']
else:
return None
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_DTV
@property
def media_content_type(self):
"""Content type of current playing media."""
if 'episodeTitle' in self._current:
return MEDIA_TYPE_TVSHOW
else:
return MEDIA_TYPE_VIDEO
@property
def media_channel(self):
"""Channel current playing media."""
if self._is_standby:
return None
else:
chan = "{} ({})".format(self._current['callsign'],
self._current['major'])
return chan
def turn_on(self):
"""Turn on the reciever."""
self.dtv.key_press('poweron')
def turn_off(self):
"""Turn off the reciever."""
self.dtv.key_press('poweroff')
def media_play(self):
"""Send play commmand."""
self.dtv.key_press('play')
def media_pause(self):
"""Send pause commmand."""
self.dtv.key_press('pause')
def media_stop(self):
"""Send stop commmand."""
self.dtv.key_press('stop')
def media_previous_track(self):
"""Send rewind commmand."""
self.dtv.key_press('rew')
def media_next_track(self):
"""Send fast forward commmand."""
self.dtv.key_press('ffwd')

View File

@ -0,0 +1,153 @@
"""
Support to interface with the MPC-HC Web API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.mpchc/
"""
import logging
import re
import requests
from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
_LOGGER = logging.getLogger(__name__)
SUPPORT_MPCHC = SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MPC-HC platform."""
name = config.get("name", "MPC-HC")
url = '{}:{}'.format(config.get('host'), config.get('port', '13579'))
if config.get('host') is None:
_LOGGER.error("Missing NPC-HC host address in config")
return False
add_devices([MpcHcDevice(name, url)])
# pylint: disable=abstract-method
class MpcHcDevice(MediaPlayerDevice):
"""Representation of a MPC-HC server."""
def __init__(self, name, url):
"""Initialize the MPC-HC device."""
self._name = name
self._url = url
self.update()
def update(self):
"""Get the latest details."""
self._player_variables = dict()
try:
response = requests.get("{}/variables.html".format(self._url),
data=None, timeout=3)
mpchc_variables = re.findall(r'<p id="(.+?)">(.+?)</p>',
response.text)
self._player_variables = dict()
for var in mpchc_variables:
self._player_variables[var[0]] = var[1].lower()
except requests.exceptions.RequestException:
_LOGGER.error("Could not connect to MPC-HC at: %s", self._url)
def _send_command(self, command_id):
"""Send a command to MPC-HC via its window message ID."""
try:
params = {"wm_command": command_id}
requests.get("{}/command.html".format(self._url),
params=params, timeout=3)
except requests.exceptions.RequestException:
_LOGGER.error("Could not send command %d to MPC-HC at: %s",
command_id, self._url)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
state = self._player_variables.get('statestring', None)
if state is None:
return STATE_OFF
if state == 'playing':
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
else:
return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
return self._player_variables.get('file', None)
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return int(self._player_variables.get('volumelevel', 0)) / 100.0
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._player_variables.get('muted', '0') == '1'
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
duration = self._player_variables.get('durationstring',
"00:00:00").split(':')
return \
int(duration[0]) * 3600 + \
int(duration[1]) * 60 + \
int(duration[2])
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_MPCHC
def volume_up(self):
"""Volume up the media player."""
self._send_command(907)
def volume_down(self):
"""Volume down media player."""
self._send_command(908)
def mute_volume(self, mute):
"""Mute the volume."""
self._send_command(909)
def media_play(self):
"""Send play command."""
self._send_command(887)
def media_pause(self):
"""Send pause command."""
self._send_command(888)
def media_stop(self):
"""Send stop command."""
self._send_command(890)
def media_next_track(self):
"""Send next track command."""
self._send_command(921)
def media_previous_track(self):
"""Send previous track command."""
self._send_command(920)

View File

@ -0,0 +1,118 @@
"""
Support for interfacing with Russound via RNET Protocol.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.russound_rnet/
"""
import logging
from homeassistant.components.media_player import (
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
REQUIREMENTS = [
'https://github.com/laf/russound/archive/0.1.6.zip'
'#russound==0.1.6']
ZONES = 'zones'
SOURCES = 'sources'
SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Russound RNET platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
keypad = config.get('keypad', '70')
if host is None or port is None:
_LOGGER.error('Invalid config. Expected %s and %s',
CONF_HOST, CONF_PORT)
return False
from russound import russound
russ = russound.Russound(host, port)
russ.connect(keypad)
sources = []
for source in config[SOURCES]:
sources.append(source['name'])
if russ.is_connected():
for zone_id, extra in config[ZONES].items():
add_devices([RussoundRNETDevice(hass, russ, sources, zone_id,
extra)])
else:
_LOGGER.error('Not connected to %s:%s', host, port)
# pylint: disable=abstract-method, too-many-public-methods,
# pylint: disable=too-many-instance-attributes, too-many-arguments
class RussoundRNETDevice(MediaPlayerDevice):
"""Representation of a Russound RNET device."""
def __init__(self, hass, russ, sources, zone_id, extra):
"""Initialise the Russound RNET device."""
self._name = extra['name']
self._russ = russ
self._state = STATE_OFF
self._sources = sources
self._zone_id = zone_id
self._volume = 0
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_RUSSOUND
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._volume = volume * 100
self._russ.set_volume('1', self._zone_id, self._volume)
def turn_on(self):
"""Turn the media player on."""
self._russ.set_power('1', self._zone_id, '1')
self._state = STATE_ON
def turn_off(self):
"""Turn off media player."""
self._russ.set_power('1', self._zone_id, '0')
self._state = STATE_OFF
def mute_volume(self, mute):
"""Send mute command."""
self._russ.toggle_mute('1', self._zone_id)
def select_source(self, source):
"""Set the input source."""
if source in self._sources:
index = self._sources.index(source)+1
self._russ.set_source('1', self._zone_id, index)
@property
def source_list(self):
"""List of available input sources."""
return self._sources

View File

@ -6,8 +6,9 @@ https://home-assistant.io/components/media_player.sonos/
""" """
import datetime import datetime
import logging import logging
import socket
from os import path from os import path
import socket
import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
@ -15,8 +16,10 @@ from homeassistant.components.media_player import (
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice) SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF) STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF,
ATTR_ENTITY_ID)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['SoCo==0.11.1'] REQUIREMENTS = ['SoCo==0.11.1']
@ -43,16 +46,28 @@ SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV' SUPPORT_SOURCE_TV = 'TV'
SUPPORT_SOURCE_RADIO = 'Radio' SUPPORT_SOURCE_RADIO = 'Radio'
SONOS_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
})
# List of devices that have been registered
DEVICES = []
# pylint: disable=unused-argument, too-many-locals # pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Sonos platform.""" """Setup the Sonos platform."""
import soco import soco
global DEVICES
if discovery_info: if discovery_info:
player = soco.SoCo(discovery_info) player = soco.SoCo(discovery_info)
if player.is_visible: if player.is_visible:
add_devices([SonosDevice(hass, player)]) device = SonosDevice(hass, player)
add_devices([device])
if not DEVICES:
register_services(hass)
DEVICES.append(device)
return True return True
return False return False
@ -74,61 +89,73 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning('No Sonos speakers found.') _LOGGER.warning('No Sonos speakers found.')
return False return False
devices = [SonosDevice(hass, p) for p in players] DEVICES = [SonosDevice(hass, p) for p in players]
add_devices(devices) add_devices(DEVICES)
register_services(hass)
_LOGGER.info('Added %s Sonos speakers', len(players)) _LOGGER.info('Added %s Sonos speakers', len(players))
return True
def _apply_service(service, service_func, *service_func_args):
def register_services(hass):
"""Register all services for sonos devices."""
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
_group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_UNJOIN,
_unjoin_service,
descriptions.get(SERVICE_UNJOIN),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
_snapshot_service,
descriptions.get(SERVICE_SNAPSHOT),
schema=SONOS_SCHEMA)
hass.services.register(DOMAIN, SERVICE_RESTORE,
_restore_service,
descriptions.get(SERVICE_RESTORE),
schema=SONOS_SCHEMA)
def _apply_service(service, service_func, *service_func_args):
"""Internal func for applying a service.""" """Internal func for applying a service."""
entity_id = service.data.get('entity_id') entity_ids = service.data.get('entity_id')
if entity_id: if entity_ids:
_devices = [device for device in devices _devices = [device for device in DEVICES
if device.entity_id == entity_id] if device.entity_id in entity_ids]
else: else:
_devices = devices _devices = DEVICES
for device in _devices: for device in _devices:
service_func(device, *service_func_args) service_func(device, *service_func_args)
device.update_ha_state(True) device.update_ha_state(True)
def group_players_service(service):
def _group_players_service(service):
"""Group media players, use player as coordinator.""" """Group media players, use player as coordinator."""
_apply_service(service, SonosDevice.group_players) _apply_service(service, SonosDevice.group_players)
def unjoin_service(service):
def _unjoin_service(service):
"""Unjoin the player from a group.""" """Unjoin the player from a group."""
_apply_service(service, SonosDevice.unjoin) _apply_service(service, SonosDevice.unjoin)
def snapshot_service(service):
def _snapshot_service(service):
"""Take a snapshot.""" """Take a snapshot."""
_apply_service(service, SonosDevice.snapshot) _apply_service(service, SonosDevice.snapshot)
def restore_service(service):
def _restore_service(service):
"""Restore a snapshot.""" """Restore a snapshot."""
_apply_service(service, SonosDevice.restore) _apply_service(service, SonosDevice.restore)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS,
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
hass.services.register(DOMAIN, SERVICE_UNJOIN,
unjoin_service,
descriptions.get(SERVICE_UNJOIN))
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
snapshot_service,
descriptions.get(SERVICE_SNAPSHOT))
hass.services.register(DOMAIN, SERVICE_RESTORE,
restore_service,
descriptions.get(SERVICE_RESTORE))
return True
def only_if_coordinator(func): def only_if_coordinator(func):
"""Decorator for coordinator. """Decorator for coordinator.

View File

@ -22,7 +22,7 @@ def get_service(hass, config):
"""Get the GNTP notification service.""" """Get the GNTP notification service."""
if config.get('app_icon') is None: if config.get('app_icon') is None:
icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend",
"www_static", "favicon-192x192.png") "www_static", "icons", "favicon-192x192.png")
app_icon = open(icon_file, 'rb').read() app_icon = open(icon_file, 'rb').read()
else: else:
app_icon = config.get('app_icon') app_icon = config.get('app_icon')

View File

@ -1,59 +0,0 @@
"""
Google Voice SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.google_voice/
"""
import logging
from homeassistant.components.notify import (
ATTR_TARGET, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/w1ll1am23/pygooglevoice-sms/archive/'
'7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#'
'pygooglevoice-sms==0.0.1']
def get_service(hass, config):
"""Get the Google Voice SMS notification service."""
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_USERNAME,
CONF_PASSWORD]},
_LOGGER):
return None
return GoogleVoiceSMSNotificationService(config[CONF_USERNAME],
config[CONF_PASSWORD])
# pylint: disable=too-few-public-methods
class GoogleVoiceSMSNotificationService(BaseNotificationService):
"""Implement the notification service for the Google Voice SMS service."""
def __init__(self, username, password):
"""Initialize the service."""
from googlevoicesms import Voice
self.voice = Voice()
self.username = username
self.password = password
def send_message(self, message="", **kwargs):
"""Send SMS to specified target user cell."""
targets = kwargs.get(ATTR_TARGET)
if not targets:
_LOGGER.info('At least 1 target is required')
return
if not isinstance(targets, list):
targets = [targets]
self.voice.login(self.username, self.password)
for target in targets:
self.voice.send_sms(target, message)
self.voice.logout()

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService) ATTR_TITLE, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
REQUIREMENTS = ['sendgrid>=1.6.0,<1.7.0'] REQUIREMENTS = ['sendgrid==3.0.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,27 +24,50 @@ def get_service(hass, config):
api_key = config['api_key'] api_key = config['api_key']
sender = config['sender'] sender = config['sender']
recipient = config['recipient'] recipient = config['recipient']
return SendgridNotificationService(api_key, sender, recipient) return SendgridNotificationService(api_key, sender, recipient)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class SendgridNotificationService(BaseNotificationService): class SendgridNotificationService(BaseNotificationService):
"""Implement the notification service for email via Sendgrid.""" """Implementation the notification service for email via Sendgrid."""
def __init__(self, api_key, sender, recipient): def __init__(self, api_key, sender, recipient):
"""Initialize the service.""" """Initialize the service."""
from sendgrid import SendGridAPIClient
self.api_key = api_key self.api_key = api_key
self.sender = sender self.sender = sender
self.recipient = recipient self.recipient = recipient
from sendgrid import SendGridClient self._sg = SendGridAPIClient(apikey=self.api_key)
self._sg = SendGridClient(self.api_key)
def send_message(self, message='', **kwargs): def send_message(self, message='', **kwargs):
"""Send an email to a user via SendGrid.""" """Send an email to a user via SendGrid."""
subject = kwargs.get(ATTR_TITLE) subject = kwargs.get(ATTR_TITLE)
from sendgrid import Mail data = {
mail = Mail(from_email=self.sender, to=self.recipient, "personalizations": [
html=message, text=message, subject=subject) {
self._sg.send(mail) "to": [
{
"email": self.recipient
}
],
"subject": subject
}
],
"from": {
"email": self.sender
},
"content": [
{
"type": "text/plain",
"value": message
}
]
}
response = self._sg.client.mail.send.post(request_body=data)
if response.status_code is not 202:
_LOGGER.error('Unable to send notification with SendGrid')

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
REQUIREMENTS = ['slacker==0.9.21'] REQUIREMENTS = ['slacker==0.9.24']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -8,43 +8,69 @@ import io
import logging import logging
import urllib import urllib
import requests import requests
from requests.auth import HTTPBasicAuth import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService) ATTR_TITLE, ATTR_DATA, BaseNotificationService)
from homeassistant.const import CONF_API_KEY from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION,
from homeassistant.helpers import validate_config ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.3.3'] REQUIREMENTS = ['python-telegram-bot==5.0.0']
ATTR_PHOTO = "photo" ATTR_PHOTO = "photo"
ATTR_FILE = "file"
ATTR_URL = "url"
ATTR_CAPTION = "caption" ATTR_CAPTION = "caption"
ATTR_USERNAME = "username"
ATTR_PASSWORD = "password" CONF_CHAT_ID = 'chat_id'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "telegram",
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_CHAT_ID): cv.string,
})
def get_service(hass, config): def get_service(hass, config):
"""Get the Telegram notification service.""" """Get the Telegram notification service."""
import telegram import telegram
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_API_KEY, 'chat_id']},
_LOGGER):
return None
try: try:
bot = telegram.Bot(token=config[CONF_API_KEY]) chat_id = config.get(CONF_CHAT_ID)
api_key = config.get(CONF_API_KEY)
bot = telegram.Bot(token=api_key)
username = bot.getMe()['username'] username = bot.getMe()['username']
_LOGGER.info("Telegram bot is '%s'.", username) _LOGGER.info("Telegram bot is '%s'.", username)
except urllib.error.HTTPError: except urllib.error.HTTPError:
_LOGGER.error("Please check your access token.") _LOGGER.error("Please check your access token.")
return None return None
return TelegramNotificationService(config[CONF_API_KEY], config['chat_id']) return TelegramNotificationService(api_key, chat_id)
def load_data(url=None, file=None, username=None, password=None):
"""Load photo/document into ByteIO/File container from a source."""
try:
if url is not None:
# load photo from url
if username is not None and password is not None:
req = requests.get(url, auth=(username, password), timeout=15)
else:
req = requests.get(url, timeout=15)
return io.BytesIO(req.content)
elif file is not None:
# load photo from file
return open(file, "rb")
else:
_LOGGER.warning("Can't load photo no photo found in params!")
except (OSError, IOError, requests.exceptions.RequestException):
_LOGGER.error("Can't load photo into ByteIO")
return None
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -64,7 +90,18 @@ class TelegramNotificationService(BaseNotificationService):
import telegram import telegram
title = kwargs.get(ATTR_TITLE) title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA, {}) data = kwargs.get(ATTR_DATA)
# exists data for send a photo/location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO, None)
photos = photos if isinstance(photos, list) else [photos]
for photo_data in photos:
self.send_photo(photo_data)
return
elif data is not None and ATTR_LOCATION in data:
return self.send_location(data.get(ATTR_LOCATION))
# send message # send message
try: try:
@ -74,41 +111,30 @@ class TelegramNotificationService(BaseNotificationService):
_LOGGER.exception("Error sending message.") _LOGGER.exception("Error sending message.")
return return
def send_photo(self, data):
"""Send a photo."""
import telegram
caption = data.pop(ATTR_CAPTION, None)
# send photo # send photo
if ATTR_PHOTO in data:
# if not a list
if not isinstance(data[ATTR_PHOTO], list):
photos = [data[ATTR_PHOTO]]
else:
photos = data[ATTR_PHOTO]
try: try:
for photo_data in photos: photo = load_data(**data)
caption = photo_data.get(ATTR_CAPTION, None)
# file is a url
if ATTR_URL in photo_data:
# use http authenticate
if ATTR_USERNAME in photo_data and\
ATTR_PASSWORD in photo_data:
req = requests.get(
photo_data[ATTR_URL],
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
photo_data[ATTR_PASSWORD])
)
else:
req = requests.get(photo_data[ATTR_URL])
file_id = io.BytesIO(req.content)
elif ATTR_FILE in photo_data:
file_id = open(photo_data[ATTR_FILE], "rb")
else:
_LOGGER.error("No url or path is set for photo!")
continue
self.bot.sendPhoto(chat_id=self._chat_id, self.bot.sendPhoto(chat_id=self._chat_id,
photo=file_id, caption=caption) photo=photo, caption=caption)
except telegram.error.TelegramError:
except (OSError, IOError, telegram.error.TelegramError,
urllib.error.HTTPError):
_LOGGER.exception("Error sending photo.") _LOGGER.exception("Error sending photo.")
return return
def send_location(self, gps):
"""Send a location."""
import telegram
latitude = float(gps.get(ATTR_LATITUDE, 0.0))
longitude = float(gps.get(ATTR_LONGITUDE, 0.0))
# send location
try:
self.bot.sendLocation(chat_id=self._chat_id,
latitude=latitude, longitude=longitude)
except telegram.error.TelegramError:
_LOGGER.exception("Error sending location.")
return

View File

@ -0,0 +1,31 @@
"""Add an iframe panel to Home Assistant."""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'panel_iframe'
DEPENDENCIES = ['frontend']
CONF_TITLE = 'title'
CONF_ICON = 'icon'
CONF_URL = 'url'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: {
vol.Optional(CONF_TITLE): cv.string,
vol.Optional(CONF_ICON): cv.icon,
# pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Url(),
}})}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Setup iframe frontend panels."""
for url_name, 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]})
return True

View File

@ -5,18 +5,29 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/qwikswitch/ https://home-assistant.io/components/qwikswitch/
""" """
import logging import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP import voluptuous as vol
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.discovery import load_platform
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.discovery import load_platform
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
from homeassistant.components.switch import SwitchDevice
DOMAIN = 'qwikswitch'
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip' REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
'#pyqwikswitch==0.4'] '#pyqwikswitch==0.4']
DEPENDENCIES = []
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'qwikswitch' CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
QSUSB = None CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required('url', default='http://127.0.0.1:2020'): vol.Coerce(str),
vol.Optional('dimmer_adjust', default=1): CV_DIM_VALUE,
vol.Optional('button_events'): vol.Coerce(str)
})}, extra=vol.ALLOW_EXTRA)
QSUSB = {}
class QSToggleEntity(object): class QSToggleEntity(object):
@ -42,6 +53,7 @@ class QSToggleEntity(object):
self._value = qsitem[PQS_VALUE] self._value = qsitem[PQS_VALUE]
self._qsusb = qsusb self._qsusb = qsusb
self._dim = qsitem[PQS_TYPE] == QSType.dimmer self._dim = qsitem[PQS_TYPE] == QSType.dimmer
QSUSB[self._id] = self
@property @property
def brightness(self): def brightness(self):
@ -87,51 +99,70 @@ class QSToggleEntity(object):
self.update_value(0) self.update_value(0)
class QSSwitch(QSToggleEntity, SwitchDevice):
"""Switch based on a Qwikswitch relay module."""
pass
class QSLight(QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
pass
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
def setup(hass, config): def setup(hass, config):
"""Setup the QSUSB component.""" """Setup the QSUSB component."""
from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD,
QS_TYPE, PQS_VALUE, PQS_TYPE, QSType) PQS_VALUE, PQS_TYPE, QSType)
# Override which cmd's in /&listen packets will fire events # Override which cmd's in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS)) cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS))
cmd_buttons = cmd_buttons.split(',') cmd_buttons = cmd_buttons.split(',')
try: url = config[DOMAIN]['url']
url = config[DOMAIN].get('url', 'http://127.0.0.1:2020') dimmer_adjust = config[DOMAIN]['dimmer_adjust']
dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1'))
qsusb = QSUsb(url, _LOGGER, dimmer_adjust) qsusb = QSUsb(url, _LOGGER, dimmer_adjust)
# Ensure qsusb terminates threads correctly def _stop(event):
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, """Stop the listener queue and clean up."""
lambda event: qsusb.stop()) nonlocal qsusb
except ValueError as val_err: qsusb.stop()
_LOGGER.error(str(val_err)) qsusb = None
return False
qsusb.ha_devices = qsusb.devices()
qsusb.ha_objects = {}
# Identify switches & remove ' Switch' postfix in name
for item in qsusb.ha_devices:
if item[PQS_TYPE] == QSType.relay and \
item[QS_NAME].lower().endswith(' switch'):
item[QS_TYPE] = 'switch'
item[QS_NAME] = item[QS_NAME][:-7]
global QSUSB global QSUSB
if QSUSB is None:
QSUSB = {} QSUSB = {}
QSUSB[id(qsusb)] = qsusb _LOGGER.info("Waiting for long poll to QSUSB to time out")
# Load sub-components for qwikswitch hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop)
# Discover all devices in QSUSB
devices = qsusb.devices()
QSUSB['switch'] = []
QSUSB['light'] = []
for item in devices:
if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower()
.endswith(' switch')):
item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix
QSUSB['switch'].append(QSSwitch(item, qsusb))
elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]:
QSUSB['light'].append(QSLight(item, qsusb))
else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", item)
# Load platforms
for comp_name in ('switch', 'light'): for comp_name in ('switch', 'light'):
load_platform(hass, comp_name, 'qwikswitch', if len(QSUSB[comp_name]) > 0:
{'qsusb_id': id(qsusb)}, config) load_platform(hass, comp_name, 'qwikswitch', {}, config)
def qs_callback(item): def qs_callback(item):
"""Typically a button press or update signal.""" """Typically a button press or update signal."""
if qsusb is None: # Shutting down
_LOGGER.info("Done")
return
# If button pressed, fire a hass event # If button pressed, fire a hass event
if item.get(QS_CMD, '') in cmd_buttons: if item.get(QS_CMD, '') in cmd_buttons:
hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id')) hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id'))
@ -142,9 +173,13 @@ def setup(hass, config):
if qsreply is False: if qsreply is False:
return return
for item in qsreply: for item in qsreply:
if item[QS_ID] in qsusb.ha_objects: if item[QS_ID] in QSUSB:
qsusb.ha_objects[item[QS_ID]].update_value( QSUSB[item[QS_ID]].update_value(
round(min(item[PQS_VALUE], 100) * 2.55)) round(min(item[PQS_VALUE], 100) * 2.55))
def _start(event):
"""Start listening."""
qsusb.listen(callback=qs_callback, timeout=30) qsusb.listen(callback=qs_callback, timeout=30)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start)
return True return True

View File

@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int), vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)), vol.Range(min=1)),
vol.Optional(CONF_DB_URL): vol.Url(''), # pylint: disable=no-value-for-parameter
vol.Optional(CONF_DB_URL): vol.Url(),
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -90,8 +91,12 @@ def run_information(point_in_time=None):
def setup(hass, config): def setup(hass, config):
"""Setup the recorder.""" """Setup the recorder."""
# pylint: disable=global-statement # pylint: disable=global-statement
# pylint: disable=too-many-locals
global _INSTANCE global _INSTANCE
if _INSTANCE is not None:
_LOGGER.error('Only a single instance allowed.')
return False
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS) purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None) db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
@ -129,7 +134,7 @@ def log_error(e, retry_wait=0, rollback=True,
if rollback: if rollback:
Session().rollback() Session().rollback()
if retry_wait: if retry_wait:
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT) _LOGGER.info("Retrying in %s seconds", retry_wait)
time.sleep(retry_wait) time.sleep(retry_wait)
@ -164,8 +169,6 @@ class Recorder(threading.Thread):
from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.models import Events, States
import sqlalchemy.exc import sqlalchemy.exc
global _INSTANCE
while True: while True:
try: try:
self._setup_connection() self._setup_connection()
@ -176,8 +179,12 @@ class Recorder(threading.Thread):
message="Error during connection setup: %s") message="Error during connection setup: %s")
if self.purge_days is not None: if self.purge_days is not None:
track_point_in_utc_time(self.hass, def purge_ticker(event):
lambda now: self._purge_old_data(), """Rerun purge every second day."""
self._purge_old_data()
track_point_in_utc_time(self.hass, purge_ticker,
dt_util.utcnow() + timedelta(days=2))
track_point_in_utc_time(self.hass, purge_ticker,
dt_util.utcnow() + timedelta(minutes=5)) dt_util.utcnow() + timedelta(minutes=5))
while True: while True:
@ -186,42 +193,26 @@ class Recorder(threading.Thread):
if event == self.quit_object: if event == self.quit_object:
self._close_run() self._close_run()
self._close_connection() self._close_connection()
# pylint: disable=global-statement
global _INSTANCE
_INSTANCE = None _INSTANCE = None
self.queue.task_done() self.queue.task_done()
return return
elif event.event_type == EVENT_TIME_CHANGED: if event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done() self.queue.task_done()
continue continue
session = Session()
dbevent = Events.from_event(event) dbevent = Events.from_event(event)
session.add(dbevent) self._commit(dbevent)
for _ in range(0, RETRIES):
try:
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
if event.event_type != EVENT_STATE_CHANGED: if event.event_type != EVENT_STATE_CHANGED:
self.queue.task_done() self.queue.task_done()
continue continue
session = Session()
dbstate = States.from_event(event) dbstate = States.from_event(event)
for _ in range(0, RETRIES):
try:
dbstate.event_id = dbevent.event_id dbstate.event_id = dbevent.event_id
session.add(dbstate) self._commit(dbstate)
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
self.queue.task_done() self.queue.task_done()
@ -268,6 +259,7 @@ class Recorder(threading.Thread):
def _close_connection(self): def _close_connection(self):
"""Close the connection.""" """Close the connection."""
# pylint: disable=global-statement
global Session global Session
self.engine.dispose() self.engine.dispose()
self.engine = None self.engine = None
@ -289,16 +281,12 @@ class Recorder(threading.Thread):
start=self.recording_start, start=self.recording_start,
created=dt_util.utcnow() created=dt_util.utcnow()
) )
session = Session() self._commit(self._run)
session.add(self._run)
session.commit()
def _close_run(self): def _close_run(self):
"""Save end time for current run.""" """Save end time for current run."""
self._run.end = dt_util.utcnow() self._run.end = dt_util.utcnow()
session = Session() self._commit(self._run)
session.add(self._run)
session.commit()
self._run = None self._run = None
def _purge_old_data(self): def _purge_old_data(self):
@ -312,17 +300,24 @@ class Recorder(threading.Thread):
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days) purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before) def _purge_states(session):
deleted_rows = Session().query(Events).filter( deleted_rows = session.query(States) \
(Events.created < purge_before)).delete(synchronize_session=False) .filter((States.created < purge_before)) \
_LOGGER.debug("Deleted %s events", deleted_rows) .delete(synchronize_session=False)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = Session().query(States).filter(
(States.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows) _LOGGER.debug("Deleted %s states", deleted_rows)
Session().commit() if self._commit(_purge_states):
_LOGGER.info("Purged states created before %s", purge_before)
def _purge_events(session):
deleted_rows = session.query(Events) \
.filter((Events.created < purge_before)) \
.delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
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 # Execute sqlite vacuum command to free up space on disk
@ -330,6 +325,23 @@ class Recorder(threading.Thread):
_LOGGER.info("Vacuuming SQLite to free space") _LOGGER.info("Vacuuming SQLite to free space")
self.engine.execute("VACUUM") self.engine.execute("VACUUM")
@staticmethod
def _commit(work):
"""Commit & retry work: Either a model or in a function."""
import sqlalchemy.exc
session = Session()
for _ in range(0, RETRIES):
try:
if callable(work):
work(session)
else:
session.add(work)
session.commit()
return True
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
return False
def _verify_instance(): def _verify_instance():
"""Throw error if recorder not initialized.""" """Throw error if recorder not initialized."""

View File

@ -20,7 +20,7 @@ Base = declarative_base()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class Events(Base): class Events(Base): # type: ignore
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
"""Event history data.""" """Event history data."""
@ -55,7 +55,7 @@ class Events(Base):
return None return None
class States(Base): class States(Base): # type: ignore
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
"""State change history.""" """State change history."""
@ -114,7 +114,7 @@ class States(Base):
return None return None
class RecorderRuns(Base): class RecorderRuns(Base): # type: ignore
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
"""Representation of recorder run.""" """Representation of recorder run."""

View File

@ -100,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema({
DEVICE_SCHEMA_SENSOR = vol.Schema({ DEVICE_SCHEMA_SENSOR = vol.Schema({
vol.Optional(ATTR_NAME, default=None): cv.string, vol.Optional(ATTR_NAME, default=None): cv.string,
vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean,
vol.Optional(ATTR_DATA_TYPE, default=[]): vol.Optional(ATTR_DATA_TYPE, default=[]):
vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]),
}) })

View File

@ -10,7 +10,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.components.wink import WinkDevice from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -49,14 +49,20 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
def value_changed(self, value): def value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id == value.value_id: if self._value.value_id == value.value_id or \
self.update_ha_state(True) self._value.node == value.node:
self.update_ha_state()
_LOGGER.debug("Value changed on network %s", value) _LOGGER.debug("Value changed on network %s", value)
@property @property
def current_position(self): def current_position(self):
"""Return the current position of Zwave roller shutter.""" """Return the current position of Zwave roller shutter."""
return self._value.data if self._value.data <= 5:
return 100
elif self._value.data >= 95:
return 0
else:
return 100 - self._value.data
def move_up(self, **kwargs): def move_up(self, **kwargs):
"""Move the roller shutter up.""" """Move the roller shutter up."""

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