mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Merge remote-tracking branch 'upstream/master' into scheduler
* upstream/master: (104 commits) Fire a time_changed event every second Update example config with correct wink config Wink API is weird. If you delete a device from their API, they dont delete it. They just "hide" it Update the frontend with the new icon for sensor Minor refactor of build_frontend script to support linux, and not just mac Update script installs latest dependencies Fix flaky device scanner test Increased environment validation upon start Fix group names for switch, light and devices Disable pylint unused-argument check Fix device scanner test Better update schedules for cast and devicetracker Tweaks to the configurator UI Add tests, fix styling Add initial version of configurator component Fix tabs being selectable by clicking on header Data binding fix: Update instead of replace states New: State.last_updated represents creation date Update sensor icon for now Updates to resolve flake8 errors ...
This commit is contained in:
commit
631251f1f7
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -4,3 +4,9 @@
|
||||
[submodule "homeassistant/external/pywemo"]
|
||||
path = homeassistant/external/pywemo
|
||||
url = https://github.com/balloob/pywemo.git
|
||||
[submodule "homeassistant/external/netdisco"]
|
||||
path = homeassistant/external/netdisco
|
||||
url = https://github.com/balloob/netdisco.git
|
||||
[submodule "homeassistant/external/noop"]
|
||||
path = homeassistant/external/noop
|
||||
url = https://github.com/balloob/noop.git
|
||||
|
@ -7,6 +7,6 @@ install:
|
||||
script:
|
||||
- flake8 homeassistant --exclude bower_components,external
|
||||
- pylint homeassistant
|
||||
- coverage run --source=homeassistant -m unittest discover ha_test
|
||||
- coverage run --source=homeassistant --omit "homeassistant/external/*" -m unittest discover tests
|
||||
after_success:
|
||||
- coveralls
|
||||
|
@ -3,6 +3,4 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
|
||||
VOLUME /config
|
||||
|
||||
EXPOSE 8123
|
||||
|
||||
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]
|
||||
|
15
README.md
15
README.md
@ -33,26 +33,27 @@ If you run into issues while using Home Assistant or during development of a com
|
||||
|
||||
## Installation instructions / Quick-start guide
|
||||
|
||||
Running Home Assistant requires that python3 and the package requests are installed.
|
||||
|
||||
Run the following code to get up and running with the minimum setup:
|
||||
Running Home Assistant requires that python3 and the package requests are installed. Run the following code to install and start Home Assistant:
|
||||
|
||||
```python
|
||||
git clone --recursive https://github.com/balloob/home-assistant.git
|
||||
cd home-assistant
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
python3 -m homeassistant
|
||||
python3 -m homeassistant --open-ui
|
||||
```
|
||||
|
||||
This will start the Home Assistant server and create an initial configuration file in `config/home-assistant.conf` that is setup for demo mode. It will launch its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123). The default password is 'password'.
|
||||
The last command will start the Home Assistant server and launch its webinterface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists.
|
||||
|
||||
If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command.
|
||||
|
||||
If you're using Docker, you can use
|
||||
|
||||
```bash
|
||||
docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro -p 8123:8123 balloob/home-assistant
|
||||
docker run -d --name="home-assistant" -v /path/to/homeassistant/config:/config -v /etc/localtime:/etc/localtime:ro --net=host balloob/home-assistant
|
||||
```
|
||||
|
||||
After you have launched the Docker image, navigate to its web interface on [http://127.0.0.1:8123](http://127.0.0.1:8123).
|
||||
|
||||
After you got the demo mode running it is time to enable some real components and get started. An example configuration file has been provided in [/config/home-assistant.conf.example](https://github.com/balloob/home-assistant/blob/master/config/home-assistant.conf.example).
|
||||
|
||||
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.
|
||||
|
@ -34,7 +34,6 @@ SERVICE_FLASH = 'flash'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
""" Setup example component. """
|
||||
|
||||
|
@ -11,6 +11,10 @@ api_password=mypass
|
||||
[light]
|
||||
platform=hue
|
||||
|
||||
[wink]
|
||||
# Get your token at https://winkbearertoken.appspot.com
|
||||
access_token=YOUR_TOKEN
|
||||
|
||||
[device_tracker]
|
||||
# The following types are available: netgear, tomato, luci, nmap_tracker
|
||||
platform=netgear
|
||||
@ -33,9 +37,19 @@ platform=wemo
|
||||
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
|
||||
# hosts=192.168.1.9,192.168.1.12
|
||||
|
||||
[thermostat]
|
||||
platform=nest
|
||||
# Required: username and password that are used to login to the Nest thermostat.
|
||||
username=myemail@mydomain.com
|
||||
password=mypassword
|
||||
|
||||
[downloader]
|
||||
download_dir=downloads
|
||||
|
||||
[notify]
|
||||
platform=pushbullet
|
||||
api_key=ABCDEFGHJKLMNOPQRSTUVXYZ
|
||||
|
||||
[device_sun_light_trigger]
|
||||
# Optional: specify a specific light/group of lights that has to be turned on
|
||||
light_group=group.living_room
|
||||
@ -65,3 +79,26 @@ unknown_light=group.living_room
|
||||
[browser]
|
||||
|
||||
[keyboard]
|
||||
|
||||
[automation]
|
||||
platform=state
|
||||
alias=Sun starts shining
|
||||
|
||||
state_entity_id=sun.sun
|
||||
# Next two are optional, omit to match all
|
||||
state_from=below_horizon
|
||||
state_to=above_horizon
|
||||
|
||||
execute_service=light.turn_off
|
||||
service_entity_id=group.living_room
|
||||
|
||||
[automation 2]
|
||||
platform=time
|
||||
alias=Beer o Clock
|
||||
|
||||
time_hours=16
|
||||
time_minutes=0
|
||||
time_seconds=0
|
||||
|
||||
execute_service=notify.notify
|
||||
execute_service_data={"message":"It's 4, time for beer!"}
|
||||
|
@ -27,7 +27,7 @@ import homeassistant.util as util
|
||||
DOMAIN = "homeassistant"
|
||||
|
||||
# How often time_changed event should fire
|
||||
TIMER_INTERVAL = 10 # seconds
|
||||
TIMER_INTERVAL = 1 # seconds
|
||||
|
||||
# How long we wait for the result of a service call
|
||||
SERVICE_CALL_LIMIT = 10 # seconds
|
||||
@ -52,6 +52,13 @@ class HomeAssistant(object):
|
||||
self.services = ServiceRegistry(self.bus, pool)
|
||||
self.states = StateMachine(self.bus)
|
||||
|
||||
# List of loaded components
|
||||
self.components = []
|
||||
|
||||
# Remote.API object pointing at local API
|
||||
self.local_api = None
|
||||
|
||||
# Directory that holds the configuration
|
||||
self.config_dir = os.path.join(os.getcwd(), 'config')
|
||||
|
||||
def get_config_path(self, path):
|
||||
@ -219,10 +226,10 @@ def _process_match_param(parameter):
|
||||
""" Wraps parameter in a list if it is not one and returns it. """
|
||||
if parameter is None or parameter == MATCH_ALL:
|
||||
return MATCH_ALL
|
||||
elif isinstance(parameter, list):
|
||||
return parameter
|
||||
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
|
||||
return (parameter,)
|
||||
else:
|
||||
return [parameter]
|
||||
return tuple(parameter)
|
||||
|
||||
|
||||
def _matcher(subject, pattern):
|
||||
@ -412,26 +419,36 @@ class EventBus(object):
|
||||
|
||||
|
||||
class State(object):
|
||||
""" Object to represent a state within the state machine. """
|
||||
"""
|
||||
Object to represent a state within the state machine.
|
||||
|
||||
__slots__ = ['entity_id', 'state', 'attributes', 'last_changed']
|
||||
entity_id: the entity that is represented.
|
||||
state: the state of the entity
|
||||
attributes: extra information on entity and state
|
||||
last_changed: last time the state was changed, not the attributes.
|
||||
last_updated: last time this object was updated.
|
||||
"""
|
||||
|
||||
__slots__ = ['entity_id', 'state', 'attributes',
|
||||
'last_changed', 'last_updated']
|
||||
|
||||
def __init__(self, entity_id, state, attributes=None, last_changed=None):
|
||||
if not ENTITY_ID_PATTERN.match(entity_id):
|
||||
raise InvalidEntityFormatError((
|
||||
"Invalid entity id encountered: {}. "
|
||||
"Format should be <domain>.<entity>").format(entity_id))
|
||||
"Format should be <domain>.<object_id>").format(entity_id))
|
||||
|
||||
self.entity_id = entity_id
|
||||
self.state = state
|
||||
self.attributes = attributes or {}
|
||||
self.last_updated = dt.datetime.now()
|
||||
|
||||
# Strip microsecond from last_changed else we cannot guarantee
|
||||
# state == State.from_dict(state.as_dict())
|
||||
# This behavior occurs because to_dict uses datetime_to_str
|
||||
# which does not preserve microseconds
|
||||
self.last_changed = util.strip_microseconds(
|
||||
last_changed or dt.datetime.now())
|
||||
last_changed or self.last_updated)
|
||||
|
||||
def copy(self):
|
||||
""" Creates a copy of itself. """
|
||||
@ -467,17 +484,17 @@ class State(object):
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.__class__ == other.__class__ and
|
||||
self.entity_id == other.entity_id and
|
||||
self.state == other.state and
|
||||
self.attributes == other.attributes)
|
||||
|
||||
def __repr__(self):
|
||||
if self.attributes:
|
||||
return "<state {}:{} @ {}>".format(
|
||||
self.state, util.repr_helper(self.attributes),
|
||||
util.datetime_to_str(self.last_changed))
|
||||
else:
|
||||
return "<state {} @ {}>".format(
|
||||
self.state, util.datetime_to_str(self.last_changed))
|
||||
attr = "; {}".format(util.repr_helper(self.attributes)) \
|
||||
if self.attributes else ""
|
||||
|
||||
return "<state {}={}{} @ {}>".format(
|
||||
self.entity_id, self.state, attr,
|
||||
util.datetime_to_str(self.last_changed))
|
||||
|
||||
|
||||
class StateMachine(object):
|
||||
@ -513,15 +530,12 @@ class StateMachine(object):
|
||||
def get_since(self, point_in_time):
|
||||
"""
|
||||
Returns all states that have been changed since point_in_time.
|
||||
|
||||
Note: States keep track of last_changed -without- microseconds.
|
||||
Therefore your point_in_time will also be stripped of microseconds.
|
||||
"""
|
||||
point_in_time = util.strip_microseconds(point_in_time)
|
||||
|
||||
with self._lock:
|
||||
return [state for state in self._states.values()
|
||||
if state.last_changed >= point_in_time]
|
||||
if state.last_updated >= point_in_time]
|
||||
|
||||
def is_state(self, entity_id, state):
|
||||
""" Returns True if entity exists and is specified state. """
|
||||
@ -538,20 +552,28 @@ class StateMachine(object):
|
||||
def set(self, entity_id, new_state, attributes=None):
|
||||
""" Set the state of an entity, add entity if it does not exist.
|
||||
|
||||
Attributes is an optional dict to specify attributes of this state. """
|
||||
Attributes is an optional dict to specify attributes of this state.
|
||||
|
||||
If you just update the attributes and not the state, last changed will
|
||||
not be affected.
|
||||
"""
|
||||
|
||||
new_state = str(new_state)
|
||||
attributes = attributes or {}
|
||||
|
||||
with self._lock:
|
||||
old_state = self._states.get(entity_id)
|
||||
|
||||
is_existing = old_state is not None
|
||||
same_state = is_existing and old_state.state == new_state
|
||||
same_attr = is_existing and old_state.attributes == attributes
|
||||
|
||||
# If state did not exist or is different, set it
|
||||
if not old_state or \
|
||||
old_state.state != new_state or \
|
||||
old_state.attributes != attributes:
|
||||
if not (same_state and same_attr):
|
||||
last_changed = old_state.last_changed if same_state else None
|
||||
|
||||
state = self._states[entity_id] = \
|
||||
State(entity_id, new_state, attributes)
|
||||
State(entity_id, new_state, attributes, last_changed)
|
||||
|
||||
event_data = {'entity_id': entity_id, 'new_state': state}
|
||||
|
||||
@ -574,9 +596,9 @@ class StateMachine(object):
|
||||
|
||||
# Ensure it is a lowercase list with entity ids we want to match on
|
||||
if isinstance(entity_ids, str):
|
||||
entity_ids = [entity_ids.lower()]
|
||||
entity_ids = (entity_ids.lower(),)
|
||||
else:
|
||||
entity_ids = [entity_id.lower() for entity_id in entity_ids]
|
||||
entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)
|
||||
|
||||
@ft.wraps(action)
|
||||
def state_listener(event):
|
||||
|
@ -1,36 +1,23 @@
|
||||
""" Starts home assistant. """
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import importlib
|
||||
|
||||
try:
|
||||
from homeassistant import bootstrap
|
||||
|
||||
except ImportError:
|
||||
# This is to add support to load Home Assistant using
|
||||
# `python3 homeassistant` instead of `python3 -m homeassistant`
|
||||
def validate_python():
|
||||
""" Validate we're running the right Python version. """
|
||||
major, minor = sys.version_info[:2]
|
||||
|
||||
# Insert the parent directory of this file into the module search path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from homeassistant import bootstrap
|
||||
if major < 3 or (major == 3 and minor < 4):
|
||||
print("Home Assistant requires atleast Python 3.4")
|
||||
sys.exit()
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. Will create demo config if no config found. """
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
metavar='path_to_config_dir',
|
||||
default="config",
|
||||
help="Directory that contains the Home Assistant configuration")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate that all core dependencies are installed
|
||||
def validate_dependencies():
|
||||
""" Validate all dependencies that HA uses. """
|
||||
import_fail = False
|
||||
|
||||
for module in ['requests']:
|
||||
@ -44,11 +31,42 @@ def main():
|
||||
if import_fail:
|
||||
print(("Install dependencies by running: "
|
||||
"pip3 install -r requirements.txt"))
|
||||
exit()
|
||||
sys.exit()
|
||||
|
||||
|
||||
def ensure_path_and_load_bootstrap():
|
||||
""" Ensure sys load path is correct and load Home Assistant bootstrap. """
|
||||
try:
|
||||
from homeassistant import bootstrap
|
||||
|
||||
except ImportError:
|
||||
# This is to add support to load Home Assistant using
|
||||
# `python3 homeassistant` instead of `python3 -m homeassistant`
|
||||
|
||||
# Insert the parent directory of this file into the module search path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from homeassistant import bootstrap
|
||||
|
||||
return bootstrap
|
||||
|
||||
|
||||
def validate_git_submodules():
|
||||
""" Validate the git submodules are cloned. """
|
||||
try:
|
||||
# pylint: disable=no-name-in-module, unused-variable
|
||||
from homeassistant.external.noop import WORKING # noqa
|
||||
except ImportError:
|
||||
print("Repository submodules have not been initialized")
|
||||
print("Please run: git submodule update --init --recursive")
|
||||
sys.exit()
|
||||
|
||||
|
||||
def ensure_config_path(config_dir):
|
||||
""" Gets the path to the configuration file.
|
||||
Creates one if it not exists. """
|
||||
|
||||
# Test if configuration directory exists
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
|
||||
if not os.path.isdir(config_dir):
|
||||
print(('Fatal Error: Unable to find specified configuration '
|
||||
'directory {} ').format(config_dir))
|
||||
@ -60,15 +78,72 @@ def main():
|
||||
if not os.path.isfile(config_path):
|
||||
try:
|
||||
with open(config_path, 'w') as conf:
|
||||
conf.write("[http]\n")
|
||||
conf.write("api_password=password\n\n")
|
||||
conf.write("[demo]\n")
|
||||
conf.write("[http]\n\n")
|
||||
conf.write("[discovery]\n\n")
|
||||
except IOError:
|
||||
print(('Fatal Error: No configuration file found and unable '
|
||||
'to write a default one to {}').format(config_path))
|
||||
sys.exit()
|
||||
|
||||
hass = bootstrap.from_config_file(config_path)
|
||||
return config_path
|
||||
|
||||
|
||||
def get_arguments():
|
||||
""" Get parsed passed in arguments. """
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
metavar='path_to_config_dir',
|
||||
default="config",
|
||||
help="Directory that contains the Home Assistant configuration")
|
||||
parser.add_argument(
|
||||
'--demo-mode',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in demo mode')
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
help='Open the webinterface in a browser')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
""" Starts Home Assistant. """
|
||||
validate_python()
|
||||
validate_dependencies()
|
||||
|
||||
bootstrap = ensure_path_and_load_bootstrap()
|
||||
|
||||
validate_git_submodules()
|
||||
|
||||
args = get_arguments()
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
config_path = ensure_config_path(config_dir)
|
||||
|
||||
if args.demo_mode:
|
||||
from homeassistant.components import http, demo
|
||||
|
||||
# Demo mode only requires http and demo components.
|
||||
hass = bootstrap.from_config_dict({
|
||||
http.DOMAIN: {},
|
||||
demo.DOMAIN: {}
|
||||
})
|
||||
else:
|
||||
hass = bootstrap.from_config_file(config_path)
|
||||
|
||||
if args.open_ui:
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
|
||||
def open_browser(event):
|
||||
""" Open the webinterface in a browser. """
|
||||
if hass.local_api is not None:
|
||||
import webbrowser
|
||||
webbrowser.open(hass.local_api.base_url)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
hass.start()
|
||||
hass.block_till_stopped()
|
||||
|
||||
|
@ -17,6 +17,38 @@ from collections import defaultdict
|
||||
import homeassistant
|
||||
import homeassistant.loader as loader
|
||||
import homeassistant.components as core_components
|
||||
import homeassistant.components.group as group
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_component(hass, domain, config=None):
|
||||
""" Setup a component for Home Assistant. """
|
||||
if config is None:
|
||||
config = defaultdict(dict)
|
||||
|
||||
component = loader.get_component(domain)
|
||||
|
||||
try:
|
||||
if component.setup(hass, config):
|
||||
hass.components.append(component.DOMAIN)
|
||||
|
||||
_LOGGER.info("component %s initialized", domain)
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if group.DOMAIN not in component.DEPENDENCIES:
|
||||
hass.pool.add_worker()
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
_LOGGER.error("component %s failed to initialize", domain)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Error during setup of component %s", domain)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
@ -29,7 +61,7 @@ def from_config_dict(config, hass=None):
|
||||
if hass is None:
|
||||
hass = homeassistant.HomeAssistant()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
enable_logging(hass)
|
||||
|
||||
loader.prepare(hass)
|
||||
|
||||
@ -42,42 +74,21 @@ def from_config_dict(config, hass=None):
|
||||
if ' ' not in key and key != homeassistant.DOMAIN)
|
||||
|
||||
if not core_components.setup(hass, config):
|
||||
logger.error(("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted."))
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted.")
|
||||
|
||||
return hass
|
||||
|
||||
logger.info("Home Assistant core initialized")
|
||||
_LOGGER.info("Home Assistant core initialized")
|
||||
|
||||
# Setup the components
|
||||
|
||||
# We assume that all components that load before the group component loads
|
||||
# are components that poll devices. As their tasks are IO based, we will
|
||||
# add an extra worker for each of them.
|
||||
add_worker = True
|
||||
|
||||
for domain in loader.load_order_components(components):
|
||||
component = loader.get_component(domain)
|
||||
|
||||
try:
|
||||
if component.setup(hass, config):
|
||||
logger.info("component %s initialized", domain)
|
||||
|
||||
add_worker = add_worker and domain != "group"
|
||||
|
||||
if add_worker:
|
||||
hass.pool.add_worker()
|
||||
|
||||
else:
|
||||
logger.error("component %s failed to initialize", domain)
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.exception("Error during setup of component %s", domain)
|
||||
setup_component(hass, domain, config)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
def from_config_file(config_path, hass=None, enable_logging=True):
|
||||
def from_config_file(config_path, hass=None):
|
||||
"""
|
||||
Reads the configuration file and tries to start all the required
|
||||
functionality. Will add functionality to 'hass' parameter if given,
|
||||
@ -89,32 +100,6 @@ def from_config_file(config_path, hass=None, enable_logging=True):
|
||||
# Set config dir to directory holding config file
|
||||
hass.config_dir = os.path.abspath(os.path.dirname(config_path))
|
||||
|
||||
if enable_logging:
|
||||
# Setup the logging for home assistant.
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.get_config_path("home-assistant.log")
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containgin directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
||||
(not err_path_exists and os.access(hass.config_dir, os.W_OK)):
|
||||
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
|
||||
err_handler.setLevel(logging.WARNING)
|
||||
err_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||
datefmt='%H:%M %d-%m-%y'))
|
||||
logging.getLogger('').addHandler(err_handler)
|
||||
|
||||
else:
|
||||
logging.getLogger(__name__).error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
||||
# Read config
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
@ -128,3 +113,30 @@ def from_config_file(config_path, hass=None, enable_logging=True):
|
||||
config_dict[section][key] = val
|
||||
|
||||
return from_config_dict(config_dict, hass)
|
||||
|
||||
|
||||
def enable_logging(hass):
|
||||
""" Setup the logging for home assistant. """
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
err_log_path = hass.get_config_path("home-assistant.log")
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containing directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
||||
(not err_path_exists and os.access(hass.config_dir, os.W_OK)):
|
||||
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
|
||||
err_handler.setLevel(logging.WARNING)
|
||||
err_handler.setFormatter(
|
||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||
datefmt='%H:%M %d-%m-%y'))
|
||||
logging.getLogger('').addHandler(err_handler)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
|
@ -70,7 +70,6 @@ def turn_off(hass, entity_id=None, **service_data):
|
||||
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
""" Setup general services related to homeassistant. """
|
||||
|
||||
|
71
homeassistant/components/automation/__init__.py
Normal file
71
homeassistant/components/automation/__init__.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""
|
||||
homeassistant.components.automation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Allows to setup simple automation rules via the config file.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.util import convert, split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
|
||||
DOMAIN = "automation"
|
||||
|
||||
DEPENDENCIES = ["group"]
|
||||
|
||||
CONF_ALIAS = "alias"
|
||||
CONF_SERVICE = "execute_service"
|
||||
CONF_SERVICE_ENTITY_ID = "service_entity_id"
|
||||
CONF_SERVICE_DATA = "service_data"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up automation. """
|
||||
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
platform = get_component('automation.{}'.format(p_type))
|
||||
|
||||
if platform is None:
|
||||
_LOGGER.error("Unknown automation platform specified: %s", p_type)
|
||||
continue
|
||||
|
||||
if platform.register(hass, p_config, _get_action(hass, p_config)):
|
||||
_LOGGER.info(
|
||||
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _get_action(hass, config):
|
||||
""" Return an action based on a config. """
|
||||
|
||||
def action():
|
||||
""" Action to be executed. """
|
||||
_LOGGER.info("Executing rule %s", config.get(CONF_ALIAS, ""))
|
||||
|
||||
if CONF_SERVICE in config:
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
|
||||
service_data = convert(
|
||||
config.get(CONF_SERVICE_DATA), json.loads, {})
|
||||
|
||||
if not isinstance(service_data, dict):
|
||||
_LOGGER.error(
|
||||
"%s should be a serialized JSON object", CONF_SERVICE_DATA)
|
||||
service_data = {}
|
||||
|
||||
if CONF_SERVICE_ENTITY_ID in config:
|
||||
service_data[ATTR_ENTITY_ID] = \
|
||||
config[CONF_SERVICE_ENTITY_ID].split(",")
|
||||
|
||||
hass.services.call(domain, service, service_data)
|
||||
|
||||
return action
|
36
homeassistant/components/automation/state.py
Normal file
36
homeassistant/components/automation/state.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""
|
||||
homeassistant.components.automation.state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers state listening automation rules.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import MATCH_ALL
|
||||
|
||||
|
||||
CONF_ENTITY_ID = "state_entity_id"
|
||||
CONF_FROM = "state_from"
|
||||
CONF_TO = "state_to"
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
|
||||
if entity_id is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing configuration key %s", CONF_ENTITY_ID)
|
||||
return False
|
||||
|
||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
""" Listens for state changes and calls action. """
|
||||
action()
|
||||
|
||||
hass.states.track_change(
|
||||
entity_id, state_automation_listener, from_state, to_state)
|
||||
|
||||
return True
|
28
homeassistant/components/automation/time.py
Normal file
28
homeassistant/components/automation/time.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""
|
||||
homeassistant.components.automation.time
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Offers time listening automation rules.
|
||||
"""
|
||||
from homeassistant.util import convert
|
||||
|
||||
CONF_HOURS = "time_hours"
|
||||
CONF_MINUTES = "time_minutes"
|
||||
CONF_SECONDS = "time_seconds"
|
||||
|
||||
|
||||
def register(hass, config, action):
|
||||
""" Listen for state changes based on `config`. """
|
||||
hours = convert(config.get(CONF_HOURS), int)
|
||||
minutes = convert(config.get(CONF_MINUTES), int)
|
||||
seconds = convert(config.get(CONF_SECONDS), int)
|
||||
|
||||
def time_automation_listener(now):
|
||||
""" Listens for time changes and calls action. """
|
||||
action()
|
||||
|
||||
hass.track_time_change(
|
||||
time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
|
||||
return True
|
@ -11,7 +11,6 @@ DEPENDENCIES = []
|
||||
SERVICE_BROWSE_URL = "browse_url"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
""" Listen for browse_url events and open
|
||||
the url in the default webbrowser. """
|
||||
|
@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts.
|
||||
"""
|
||||
import logging
|
||||
|
||||
try:
|
||||
import pychromecast
|
||||
except ImportError:
|
||||
# Ignore, we will raise appropriate error later
|
||||
pass
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import extract_entity_ids
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
||||
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
|
||||
CONF_HOSTS)
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
|
||||
|
||||
|
||||
DOMAIN = 'chromecast'
|
||||
@ -105,12 +111,35 @@ def media_prev_track(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-branches
|
||||
def setup(hass, config):
|
||||
""" Listen for chromecast events. """
|
||||
logger = logging.getLogger(__name__)
|
||||
def setup_chromecast(casts, host):
|
||||
""" Tries to convert host to Chromecast object and set it up. """
|
||||
|
||||
# Check if already setup
|
||||
if any(cast.host == host for cast in casts.values()):
|
||||
return
|
||||
|
||||
try:
|
||||
cast = pychromecast.PyChromecast(host)
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(
|
||||
util.slugify(cast.device.friendly_name)),
|
||||
casts.keys())
|
||||
|
||||
casts[entity_id] = cast
|
||||
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
# pylint: disable=unused-argument,too-many-locals
|
||||
""" Listen for chromecast events. """
|
||||
logger = logging.getLogger(__name__)
|
||||
discovery = get_component('discovery')
|
||||
|
||||
try:
|
||||
# pylint: disable=redefined-outer-name
|
||||
import pychromecast
|
||||
except ImportError:
|
||||
logger.exception(("Failed to import pychromecast. "
|
||||
@ -119,33 +148,23 @@ def setup(hass, config):
|
||||
|
||||
return False
|
||||
|
||||
if CONF_HOSTS in config[DOMAIN]:
|
||||
hosts = config[DOMAIN][CONF_HOSTS].split(",")
|
||||
casts = {}
|
||||
|
||||
# If no hosts given, scan for chromecasts
|
||||
else:
|
||||
# If discovery component not loaded, scan ourselves
|
||||
if discovery.DOMAIN not in hass.components:
|
||||
logger.info("Scanning for Chromecasts")
|
||||
hosts = pychromecast.discover_chromecasts()
|
||||
|
||||
casts = {}
|
||||
for host in hosts:
|
||||
setup_chromecast(casts, host)
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
cast = pychromecast.PyChromecast(host)
|
||||
def chromecast_discovered(service, info):
|
||||
""" Called when a Chromecast has been discovered. """
|
||||
logger.info("New Chromecast discovered: %s", info[0])
|
||||
setup_chromecast(casts, info[0])
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(
|
||||
util.slugify(cast.device.friendly_name)),
|
||||
casts.keys())
|
||||
|
||||
casts[entity_id] = cast
|
||||
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
pass
|
||||
|
||||
if not casts:
|
||||
logger.error("Could not find Chromecasts")
|
||||
return False
|
||||
discovery.listen(
|
||||
hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
|
||||
|
||||
def update_chromecast_state(entity_id, chromecast):
|
||||
""" Retrieve state of Chromecast and update statemachine. """
|
||||
@ -192,12 +211,13 @@ def setup(hass, config):
|
||||
|
||||
hass.states.set(entity_id, state, state_attr)
|
||||
|
||||
def update_chromecast_states(time): # pylint: disable=unused-argument
|
||||
def update_chromecast_states(time):
|
||||
""" Updates all chromecast states. """
|
||||
logger.info("Updating Chromecast status")
|
||||
if casts:
|
||||
logger.info("Updating Chromecast status")
|
||||
|
||||
for entity_id, cast in casts.items():
|
||||
update_chromecast_state(entity_id, cast)
|
||||
for entity_id, cast in casts.items():
|
||||
update_chromecast_state(entity_id, cast)
|
||||
|
||||
def _service_to_entities(service):
|
||||
""" Helper method to get entities from service. """
|
||||
@ -277,7 +297,7 @@ def setup(hass, config):
|
||||
pychromecast.play_youtube_video(video_id, cast.host)
|
||||
update_chromecast_state(entity_id, cast)
|
||||
|
||||
hass.track_time_change(update_chromecast_states)
|
||||
hass.track_time_change(update_chromecast_states, second=range(0, 60, 15))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
||||
turn_off_service)
|
||||
|
190
homeassistant/components/configurator.py
Normal file
190
homeassistant/components/configurator.py
Normal file
@ -0,0 +1,190 @@
|
||||
"""
|
||||
homeassistant.components.configurator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A component to allow pieces of code to request configuration from the user.
|
||||
|
||||
Initiate a request by calling the `request_config` method with a callback.
|
||||
This will return a request id that has to be used for future calls.
|
||||
A callback has to be provided to `request_config` which will be called when
|
||||
the user has submitted configuration information.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import generate_entity_id
|
||||
from homeassistant.const import EVENT_TIME_CHANGED
|
||||
|
||||
DOMAIN = "configurator"
|
||||
DEPENDENCIES = []
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
SERVICE_CONFIGURE = "configure"
|
||||
|
||||
STATE_CONFIGURE = "configure"
|
||||
STATE_CONFIGURED = "configured"
|
||||
|
||||
ATTR_CONFIGURE_ID = "configure_id"
|
||||
ATTR_DESCRIPTION = "description"
|
||||
ATTR_DESCRIPTION_IMAGE = "description_image"
|
||||
ATTR_SUBMIT_CAPTION = "submit_caption"
|
||||
ATTR_FIELDS = "fields"
|
||||
ATTR_ERRORS = "errors"
|
||||
|
||||
_REQUESTS = {}
|
||||
_INSTANCES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
hass, name, callback, description=None, description_image=None,
|
||||
submit_caption=None, fields=None):
|
||||
""" Create a new request for config.
|
||||
Will return an ID to be used for sequent calls. """
|
||||
|
||||
instance = _get_instance(hass)
|
||||
|
||||
request_id = instance.request_config(
|
||||
name, callback,
|
||||
description, description_image, submit_caption, fields)
|
||||
|
||||
_REQUESTS[request_id] = instance
|
||||
|
||||
return request_id
|
||||
|
||||
|
||||
def notify_errors(request_id, error):
|
||||
""" Add errors to a config request. """
|
||||
try:
|
||||
_REQUESTS[request_id].notify_errors(request_id, error)
|
||||
except KeyError:
|
||||
# If request_id does not exist
|
||||
pass
|
||||
|
||||
|
||||
def request_done(request_id):
|
||||
""" Mark a config request as done. """
|
||||
try:
|
||||
_REQUESTS.pop(request_id).request_done(request_id)
|
||||
except KeyError:
|
||||
# If request_id does not exist
|
||||
pass
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Set up Configurator. """
|
||||
return True
|
||||
|
||||
|
||||
def _get_instance(hass):
|
||||
""" Get an instance per hass object. """
|
||||
try:
|
||||
return _INSTANCES[hass]
|
||||
except KeyError:
|
||||
_INSTANCES[hass] = Configurator(hass)
|
||||
|
||||
if DOMAIN not in hass.components:
|
||||
hass.components.append(DOMAIN)
|
||||
|
||||
return _INSTANCES[hass]
|
||||
|
||||
|
||||
class Configurator(object):
|
||||
"""
|
||||
Class to keep track of current configuration requests.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
self.hass = hass
|
||||
self._cur_id = 0
|
||||
self._requests = {}
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def request_config(
|
||||
self, name, callback,
|
||||
description, description_image, submit_caption, fields):
|
||||
""" Setup a request for configuration. """
|
||||
|
||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
|
||||
|
||||
if fields is None:
|
||||
fields = []
|
||||
|
||||
request_id = self._generate_unique_id()
|
||||
|
||||
self._requests[request_id] = (entity_id, fields, callback)
|
||||
|
||||
data = {
|
||||
ATTR_CONFIGURE_ID: request_id,
|
||||
ATTR_FIELDS: fields,
|
||||
}
|
||||
|
||||
data.update({
|
||||
key: value for key, value in [
|
||||
(ATTR_DESCRIPTION, description),
|
||||
(ATTR_DESCRIPTION_IMAGE, description_image),
|
||||
(ATTR_SUBMIT_CAPTION, submit_caption),
|
||||
] if value is not None
|
||||
})
|
||||
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURE, data)
|
||||
|
||||
return request_id
|
||||
|
||||
def notify_errors(self, request_id, error):
|
||||
""" Update the state with errors. """
|
||||
if not self._validate_request_id(request_id):
|
||||
return
|
||||
|
||||
entity_id = self._requests[request_id][0]
|
||||
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
new_data = state.attributes
|
||||
new_data[ATTR_ERRORS] = error
|
||||
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
|
||||
|
||||
def request_done(self, request_id):
|
||||
""" Remove the config request. """
|
||||
if not self._validate_request_id(request_id):
|
||||
return
|
||||
|
||||
entity_id = self._requests.pop(request_id)[0]
|
||||
|
||||
# If we remove the state right away, it will not be included with
|
||||
# the result fo the service call (current design limitation).
|
||||
# Instead, we will set it to configured to give as feedback but delete
|
||||
# it shortly after so that it is deleted when the client updates.
|
||||
self.hass.states.set(entity_id, STATE_CONFIGURED)
|
||||
|
||||
def deferred_remove(event):
|
||||
""" Remove the request state. """
|
||||
self.hass.states.remove(entity_id)
|
||||
|
||||
self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
|
||||
|
||||
def handle_service_call(self, call):
|
||||
""" Handle a configure service call. """
|
||||
request_id = call.data.get(ATTR_CONFIGURE_ID)
|
||||
|
||||
if not self._validate_request_id(request_id):
|
||||
return
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
entity_id, fields, callback = self._requests[request_id]
|
||||
|
||||
# field validation goes here?
|
||||
|
||||
callback(call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
""" Generates a unique configurator id. """
|
||||
self._cur_id += 1
|
||||
return "{}-{}".format(id(self), self._cur_id)
|
||||
|
||||
def _validate_request_id(self, request_id):
|
||||
""" Validate that the request belongs to this instance. """
|
||||
return request_id in self._requests
|
@ -5,16 +5,21 @@ homeassistant.components.demo
|
||||
Sets up a demo environment that mimics interaction with devices
|
||||
"""
|
||||
import random
|
||||
import time
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.helpers import extract_entity_ids
|
||||
from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF,
|
||||
ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE)
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
STATE_ON, STATE_OFF, TEMP_CELCIUS,
|
||||
ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_LATITUDE, CONF_LONGITUDE)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
|
||||
from homeassistant.util import split_entity_id
|
||||
ATTR_XY_COLOR, ATTR_RGB_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
|
||||
from homeassistant.components.thermostat import (
|
||||
ATTR_CURRENT_TEMPERATURE, ATTR_AWAY_MODE)
|
||||
from homeassistant.util import split_entity_id, color_RGB_to_xy
|
||||
|
||||
DOMAIN = "demo"
|
||||
|
||||
@ -24,6 +29,7 @@ DEPENDENCIES = []
|
||||
def setup(hass, config):
|
||||
""" Setup a demo environment. """
|
||||
group = loader.get_component('group')
|
||||
configurator = loader.get_component('configurator')
|
||||
|
||||
config.setdefault(ha.DOMAIN, {})
|
||||
config.setdefault(DOMAIN, {})
|
||||
@ -48,8 +54,25 @@ def setup(hass, config):
|
||||
domain, _ = split_entity_id(entity_id)
|
||||
|
||||
if domain == "light":
|
||||
data = {ATTR_BRIGHTNESS: 200,
|
||||
ATTR_XY_COLOR: random.choice(light_colors)}
|
||||
rgb_color = service.data.get(ATTR_RGB_COLOR)
|
||||
|
||||
if rgb_color:
|
||||
color = color_RGB_to_xy(
|
||||
rgb_color[0], rgb_color[1], rgb_color[2])
|
||||
|
||||
else:
|
||||
cur_state = hass.states.get(entity_id)
|
||||
|
||||
# Use current color if available
|
||||
if cur_state and cur_state.attributes.get(ATTR_XY_COLOR):
|
||||
color = cur_state.attributes.get(ATTR_XY_COLOR)
|
||||
else:
|
||||
color = random.choice(light_colors)
|
||||
|
||||
data = {
|
||||
ATTR_BRIGHTNESS: service.data.get(ATTR_BRIGHTNESS, 200),
|
||||
ATTR_XY_COLOR: color
|
||||
}
|
||||
else:
|
||||
data = None
|
||||
|
||||
@ -124,7 +147,7 @@ def setup(hass, config):
|
||||
})
|
||||
|
||||
# Setup chromecast
|
||||
hass.states.set("chromecast.Living_Rm", "Netflix",
|
||||
hass.states.set("chromecast.Living_Rm", "Plex",
|
||||
{'friendly_name': 'Living Room',
|
||||
ATTR_ENTITY_PICTURE:
|
||||
'http://graph.facebook.com/KillBillMovie/picture'})
|
||||
@ -141,4 +164,38 @@ def setup(hass, config):
|
||||
'unit_of_measurement': '%'
|
||||
})
|
||||
|
||||
# Nest demo
|
||||
hass.states.set("thermostat.Nest", "23",
|
||||
{
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELCIUS,
|
||||
ATTR_CURRENT_TEMPERATURE: '18',
|
||||
ATTR_AWAY_MODE: STATE_OFF
|
||||
})
|
||||
|
||||
configurator_ids = []
|
||||
|
||||
def hue_configuration_callback(data):
|
||||
""" Fake callback, mark config as done. """
|
||||
time.sleep(2)
|
||||
|
||||
# First time it is called, pretend it failed.
|
||||
if len(configurator_ids) == 1:
|
||||
configurator.notify_errors(
|
||||
configurator_ids[0],
|
||||
"Failed to register, please try again.")
|
||||
|
||||
configurator_ids.append(0)
|
||||
else:
|
||||
configurator.request_done(configurator_ids[0])
|
||||
|
||||
request_id = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips Hue "
|
||||
"with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
|
||||
configurator_ids.append(request_id)
|
||||
|
||||
return True
|
||||
|
@ -66,7 +66,6 @@ def setup(hass, config):
|
||||
else:
|
||||
return None
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def schedule_light_on_sun_rise(entity, old_state, new_state):
|
||||
"""The moment sun sets we want to have all the lights on.
|
||||
We will schedule to have each light start after one another
|
||||
|
@ -24,9 +24,8 @@ DEPENDENCIES = []
|
||||
|
||||
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
|
||||
|
||||
GROUP_NAME_ALL_DEVICES = 'all_devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format(
|
||||
GROUP_NAME_ALL_DEVICES)
|
||||
GROUP_NAME_ALL_DEVICES = 'all devices'
|
||||
ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
@ -111,26 +110,24 @@ class DeviceTracker(object):
|
||||
""" Triggers update of the device states. """
|
||||
self.update_devices(now)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
dev_group = group.Group(
|
||||
hass, GROUP_NAME_ALL_DEVICES, user_defined=False)
|
||||
|
||||
def reload_known_devices_service(service):
|
||||
""" Reload known devices file. """
|
||||
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
|
||||
|
||||
self._read_known_devices_file()
|
||||
|
||||
self.update_devices(datetime.now())
|
||||
|
||||
if self.tracked:
|
||||
group.setup_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES,
|
||||
self.device_entity_ids, False)
|
||||
dev_group.update_tracked_entity_ids(self.device_entity_ids)
|
||||
|
||||
reload_known_devices_service(None)
|
||||
|
||||
if self.invalid_known_devices_file:
|
||||
return
|
||||
|
||||
hass.track_time_change(update_device_state)
|
||||
hass.track_time_change(
|
||||
update_device_state, second=range(0, 60, 12))
|
||||
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||
|
@ -17,7 +17,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Luci scanner. """
|
||||
if not validate_config(config,
|
||||
@ -101,7 +100,12 @@ class LuciDeviceScanner(object):
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
if result:
|
||||
self.last_results = [x['HW address'] for x in result]
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
# Check if the Flags for each device contain
|
||||
# NUD_REACHABLE and if so, add it to last_results
|
||||
if int(device_entry['Flags'], 16) & 0x2:
|
||||
self.last_results.append(device_entry['HW address'])
|
||||
|
||||
return True
|
||||
|
||||
|
@ -14,7 +14,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Netgear scanner. """
|
||||
if not validate_config(config,
|
||||
@ -22,7 +21,10 @@ def get_scanner(hass, config):
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
scanner = NetgearDeviceScanner(config[DOMAIN])
|
||||
info = config[DOMAIN]
|
||||
|
||||
scanner = NetgearDeviceScanner(
|
||||
info[CONF_HOST], info[CONF_USERNAME], info[CONF_PASSWORD])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
@ -30,10 +32,7 @@ def get_scanner(hass, config):
|
||||
class NetgearDeviceScanner(object):
|
||||
""" This class queries a Netgear wireless router using the SOAP-api. """
|
||||
|
||||
def __init__(self, config):
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
|
||||
def __init__(self, host, username, password):
|
||||
self.last_results = []
|
||||
|
||||
try:
|
||||
@ -54,32 +53,27 @@ class NetgearDeviceScanner(object):
|
||||
self.lock = threading.Lock()
|
||||
|
||||
_LOGGER.info("Logging in")
|
||||
if self._api.login():
|
||||
self.success_init = True
|
||||
self._update_info()
|
||||
|
||||
self.success_init = self._api.login()
|
||||
|
||||
if self.success_init:
|
||||
self._update_info()
|
||||
else:
|
||||
_LOGGER.error("Failed to Login")
|
||||
|
||||
self.success_init = False
|
||||
|
||||
def scan_devices(self):
|
||||
""" Scans for new devices and return a
|
||||
list containing found device ids. """
|
||||
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
return (device.mac for device in self.last_results)
|
||||
|
||||
def get_device_name(self, mac):
|
||||
""" Returns the name of the given device or None if we don't know. """
|
||||
|
||||
filter_named = [device.name for device in self.last_results
|
||||
if device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
try:
|
||||
return next(device.name for device in self.last_results
|
||||
if device.mac == mac)
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
@ -92,4 +86,4 @@ class NetgearDeviceScanner(object):
|
||||
with self.lock:
|
||||
_LOGGER.info("Scanning")
|
||||
|
||||
self.last_results = self._api.get_attached_devices()
|
||||
self.last_results = self._api.get_attached_devices() or []
|
||||
|
@ -20,7 +20,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Nmap scanner. """
|
||||
if not validate_config(config, {DOMAIN: [CONF_HOSTS]},
|
||||
|
@ -20,7 +20,6 @@ CONF_HTTP_ID = "http_id"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_scanner(hass, config):
|
||||
""" Validates config and returns a Tomato scanner. """
|
||||
if not validate_config(config,
|
||||
|
86
homeassistant/components/discovery.py
Normal file
86
homeassistant/components/discovery.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""
|
||||
Starts a service to scan in intervals for new devices.
|
||||
|
||||
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
|
||||
|
||||
Knows which components handle certain types, will make sure they are
|
||||
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
||||
|
||||
"""
|
||||
import logging
|
||||
import threading
|
||||
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
from homeassistant.external.netdisco.netdisco import DiscoveryService
|
||||
import homeassistant.external.netdisco.netdisco.const as services
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED,
|
||||
ATTR_SERVICE, ATTR_DISCOVERED)
|
||||
|
||||
DOMAIN = "discovery"
|
||||
DEPENDENCIES = []
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
services.BELKIN_WEMO: "switch",
|
||||
services.GOOGLE_CAST: "chromecast",
|
||||
services.PHILIPS_HUE: "light",
|
||||
}
|
||||
|
||||
|
||||
def listen(hass, service, callback):
|
||||
"""
|
||||
Setup listener for discovery of specific service.
|
||||
Service can be a string or a list/tuple.
|
||||
"""
|
||||
|
||||
if isinstance(service, str):
|
||||
service = (service,)
|
||||
else:
|
||||
service = tuple(service)
|
||||
|
||||
def discovery_event_listener(event):
|
||||
""" Listens for discovery events. """
|
||||
if event.data[ATTR_SERVICE] in service:
|
||||
callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED])
|
||||
|
||||
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Starts a discovery service. """
|
||||
|
||||
# Disable zeroconf logging, it spams
|
||||
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
def new_service_listener(service, info):
|
||||
""" Called when a new service is found. """
|
||||
with lock:
|
||||
component = SERVICE_HANDLERS.get(service)
|
||||
|
||||
logger.info("Found new service: %s %s", service, info)
|
||||
|
||||
if component and component not in hass.components:
|
||||
bootstrap.setup_component(hass, component, config)
|
||||
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: service,
|
||||
ATTR_DISCOVERED: info
|
||||
})
|
||||
|
||||
def start_discovery(event):
|
||||
""" Start discovering. """
|
||||
netdisco = DiscoveryService(SCAN_INTERVAL)
|
||||
netdisco.add_listener(new_service_listener)
|
||||
netdisco.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_discovery)
|
||||
|
||||
return True
|
@ -5,12 +5,11 @@ homeassistant.components.groups
|
||||
Provides functionality to group devices that can be turned on or off.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import homeassistant as ha
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
|
||||
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
|
||||
|
||||
DOMAIN = "group"
|
||||
DEPENDENCIES = []
|
||||
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||
|
||||
_GROUPS = {}
|
||||
|
||||
|
||||
def _get_group_on_off(state):
|
||||
""" Determine the group on/off states based on a state. """
|
||||
@ -94,89 +91,97 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
def setup(hass, config):
|
||||
""" Sets up all groups found definded in the configuration. """
|
||||
for name, entity_ids in config.get(DOMAIN, {}).items():
|
||||
entity_ids = entity_ids.split(",")
|
||||
|
||||
setup_group(hass, name, entity_ids)
|
||||
setup_group(hass, name, entity_ids.split(","))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup_group(hass, name, entity_ids, user_defined=True):
|
||||
""" Sets up a group state that is the combined state of
|
||||
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
||||
logger = logging.getLogger(__name__)
|
||||
class Group(object):
|
||||
""" Tracks a group of entity ids. """
|
||||
def __init__(self, hass, name, entity_ids=None, user_defined=True):
|
||||
self.hass = hass
|
||||
self.name = name
|
||||
self.user_defined = user_defined
|
||||
|
||||
# In case an iterable is passed in
|
||||
entity_ids = list(entity_ids)
|
||||
self.entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
hass.states.entity_ids(DOMAIN))
|
||||
|
||||
if not entity_ids:
|
||||
logger.error(
|
||||
'Error setting up group %s: no entities passed in to track', name)
|
||||
self.tracking = []
|
||||
self.group_on, self.group_off = None, None
|
||||
|
||||
return False
|
||||
|
||||
# Loop over the given entities to:
|
||||
# - determine which group type this is (on_off, device_home)
|
||||
# - determine which states exist and have groupable states
|
||||
# - determine the current state of the group
|
||||
warnings = []
|
||||
group_ids = []
|
||||
group_on, group_off = None, None
|
||||
group_state = False
|
||||
|
||||
for entity_id in entity_ids:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
# Try to determine group type if we didn't yet
|
||||
if group_on is None and state:
|
||||
group_on, group_off = _get_group_on_off(state.state)
|
||||
|
||||
if group_on is None:
|
||||
# We did not find a matching group_type
|
||||
warnings.append(
|
||||
"Entity {} has ungroupable state '{}'".format(
|
||||
name, state.state))
|
||||
|
||||
continue
|
||||
|
||||
# Check if entity exists
|
||||
if not state:
|
||||
warnings.append("Entity {} does not exist".format(entity_id))
|
||||
|
||||
# Check if entity is invalid state
|
||||
elif state.state != group_off and state.state != group_on:
|
||||
|
||||
warnings.append("State of {} is {} (expected: {} or {})".format(
|
||||
entity_id, state.state, group_off, group_on))
|
||||
|
||||
# We have a valid group state
|
||||
if entity_ids is not None:
|
||||
self.update_tracked_entity_ids(entity_ids)
|
||||
else:
|
||||
group_ids.append(entity_id)
|
||||
self.force_update()
|
||||
|
||||
# Keep track of the group state to init later on
|
||||
group_state = group_state or state.state == group_on
|
||||
@property
|
||||
def state(self):
|
||||
""" Return the current state from the group. """
|
||||
return self.hass.states.get(self.entity_id)
|
||||
|
||||
# If none of the entities could be found during setup
|
||||
if not group_ids:
|
||||
logger.error('Unable to find any entities to track for group %s', name)
|
||||
@property
|
||||
def state_attr(self):
|
||||
""" State attributes of this group. """
|
||||
return {
|
||||
ATTR_ENTITY_ID: self.tracking,
|
||||
ATTR_AUTO: not self.user_defined,
|
||||
ATTR_FRIENDLY_NAME: self.name
|
||||
}
|
||||
|
||||
return False
|
||||
def update_tracked_entity_ids(self, entity_ids):
|
||||
""" Update the tracked entity IDs. """
|
||||
self.stop()
|
||||
self.tracking = tuple(entity_ids)
|
||||
self.group_on, self.group_off = None, None
|
||||
|
||||
elif warnings:
|
||||
logger.warning(
|
||||
'Warnings during setting up group %s: %s',
|
||||
name, ", ".join(warnings))
|
||||
self.force_update()
|
||||
|
||||
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
||||
state = group_on if group_state else group_off
|
||||
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
|
||||
self.start()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_group_state(entity_id, old_state, new_state):
|
||||
def force_update(self):
|
||||
""" Query all the tracked states and update group state. """
|
||||
for entity_id in self.tracking:
|
||||
state = self.hass.states.get(entity_id)
|
||||
|
||||
if state is not None:
|
||||
self._update_group_state(state.entity_id, None, state)
|
||||
|
||||
# If parsing the entitys did not result in a state, set UNKNOWN
|
||||
if self.state is None:
|
||||
self.hass.states.set(
|
||||
self.entity_id, STATE_UNKNOWN, self.state_attr)
|
||||
|
||||
def start(self):
|
||||
""" Starts the tracking. """
|
||||
self.hass.states.track_change(self.tracking, self._update_group_state)
|
||||
|
||||
def stop(self):
|
||||
""" Unregisters the group from Home Assistant. """
|
||||
self.hass.states.remove(self.entity_id)
|
||||
|
||||
self.hass.bus.remove_listener(
|
||||
ha.EVENT_STATE_CHANGED, self._update_group_state)
|
||||
|
||||
def _update_group_state(self, entity_id, old_state, new_state):
|
||||
""" Updates the group state based on a state change by
|
||||
a tracked entity. """
|
||||
|
||||
cur_gr_state = hass.states.get(group_entity_id).state
|
||||
# We have not determined type of group yet
|
||||
if self.group_on is None:
|
||||
self.group_on, self.group_off = _get_group_on_off(new_state.state)
|
||||
|
||||
if self.group_on is not None:
|
||||
# New state of the group is going to be based on the first
|
||||
# state that we can recognize
|
||||
self.hass.states.set(
|
||||
self.entity_id, new_state.state, self.state_attr)
|
||||
|
||||
return
|
||||
|
||||
# There is already a group state
|
||||
cur_gr_state = self.hass.states.get(self.entity_id).state
|
||||
group_on, group_off = self.group_on, self.group_off
|
||||
|
||||
# if cur_gr_state = OFF and new_state = ON: set ON
|
||||
# if cur_gr_state = ON and new_state = OFF: research
|
||||
@ -184,31 +189,21 @@ def setup_group(hass, name, entity_ids, user_defined=True):
|
||||
|
||||
if cur_gr_state == group_off and new_state.state == group_on:
|
||||
|
||||
hass.states.set(group_entity_id, group_on, state_attr)
|
||||
self.hass.states.set(
|
||||
self.entity_id, group_on, self.state_attr)
|
||||
|
||||
elif cur_gr_state == group_on and new_state.state == group_off:
|
||||
elif (cur_gr_state == group_on and
|
||||
new_state.state == group_off):
|
||||
|
||||
# Check if any of the other states is still on
|
||||
if not any([hass.states.is_state(ent_id, group_on)
|
||||
for ent_id in group_ids
|
||||
if entity_id != ent_id]):
|
||||
hass.states.set(group_entity_id, group_off, state_attr)
|
||||
|
||||
_GROUPS[group_entity_id] = hass.states.track_change(
|
||||
group_ids, update_group_state)
|
||||
|
||||
hass.states.set(group_entity_id, state, state_attr)
|
||||
|
||||
return True
|
||||
if not any(self.hass.states.is_state(ent_id, group_on)
|
||||
for ent_id in self.tracking if entity_id != ent_id):
|
||||
self.hass.states.set(
|
||||
self.entity_id, group_off, self.state_attr)
|
||||
|
||||
|
||||
def remove_group(hass, name):
|
||||
""" Remove a group and its state listener from Home Assistant. """
|
||||
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
||||
def setup_group(hass, name, entity_ids, user_defined=True):
|
||||
""" Sets up a group state that is the combined state of
|
||||
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
||||
|
||||
if hass.states.get(group_entity_id) is not None:
|
||||
hass.states.remove(group_entity_id)
|
||||
|
||||
if group_entity_id in _GROUPS:
|
||||
hass.bus.remove_listener(
|
||||
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))
|
||||
return Group(hass, name, entity_ids, user_defined)
|
||||
|
@ -86,7 +86,7 @@ import homeassistant as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
|
||||
from homeassistant.helpers import validate_config, TrackStates
|
||||
from homeassistant.helpers import TrackStates
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
from . import frontend
|
||||
@ -117,13 +117,18 @@ DATA_API_PASSWORD = 'api_password'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
def setup(hass, config=None):
|
||||
""" Sets up the HTTP API and debug interface. """
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER):
|
||||
return False
|
||||
if config is None or DOMAIN not in config:
|
||||
config = {DOMAIN: {}}
|
||||
|
||||
api_password = config[DOMAIN][CONF_API_PASSWORD]
|
||||
api_password = config[DOMAIN].get(CONF_API_PASSWORD)
|
||||
|
||||
no_password_set = api_password is None
|
||||
|
||||
if no_password_set:
|
||||
api_password = util.get_random_string()
|
||||
|
||||
# If no server host is given, accept all incoming requests
|
||||
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
@ -132,19 +137,16 @@ def setup(hass, config):
|
||||
|
||||
development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
|
||||
|
||||
server = HomeAssistantHTTPServer((server_host, server_port),
|
||||
RequestHandler, hass, api_password,
|
||||
development)
|
||||
server = HomeAssistantHTTPServer(
|
||||
(server_host, server_port), RequestHandler, hass, api_password,
|
||||
development, no_password_set)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True).start())
|
||||
|
||||
# If no local api set, set one with known information
|
||||
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
|
||||
hass.local_api = \
|
||||
rem.API(util.get_local_ip(), api_password, server_port)
|
||||
hass.local_api = rem.API(util.get_local_ip(), api_password, server_port)
|
||||
|
||||
return True
|
||||
|
||||
@ -158,13 +160,14 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, server_address, request_handler_class,
|
||||
hass, api_password, development=False):
|
||||
hass, api_password, development, no_password_set):
|
||||
super().__init__(server_address, request_handler_class)
|
||||
|
||||
self.server_address = server_address
|
||||
self.hass = hass
|
||||
self.api_password = api_password
|
||||
self.development = development
|
||||
self.no_password_set = no_password_set
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
@ -273,10 +276,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
api_password = self.headers.get(AUTH_HEADER)
|
||||
if self.server.no_password_set:
|
||||
api_password = self.server.api_password
|
||||
else:
|
||||
api_password = self.headers.get(AUTH_HEADER)
|
||||
|
||||
if not api_password and DATA_API_PASSWORD in data:
|
||||
api_password = data[DATA_API_PASSWORD]
|
||||
if not api_password and DATA_API_PASSWORD in data:
|
||||
api_password = data[DATA_API_PASSWORD]
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
@ -345,7 +351,6 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
""" DELETE request handler. """
|
||||
self._handle_request('DELETE')
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _handle_get_root(self, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
|
||||
@ -360,6 +365,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
else:
|
||||
app_url = "frontend-{}.html".format(frontend.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = (self.server.api_password if self.server.no_password_set
|
||||
else data.get('api_password', ''))
|
||||
|
||||
write(("<!doctype html>"
|
||||
"<html>"
|
||||
"<head><title>Home Assistant</title>"
|
||||
@ -378,19 +387,16 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
" src='/static/webcomponents.min.js'></script>"
|
||||
"<link rel='import' href='/static/{}' />"
|
||||
"<splash-login auth='{}'></splash-login>"
|
||||
"</body></html>").format(app_url, data.get('api_password', '')))
|
||||
"</body></html>").format(app_url, auth))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _handle_get_api(self, path_match, data):
|
||||
""" Renders the debug interface. """
|
||||
self._json_message("API running.")
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _handle_get_api_states(self, path_match, data):
|
||||
""" Returns a dict containing all entity ids and their state. """
|
||||
self._write_json(self.server.hass.states.all())
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def _handle_get_api_states_entity(self, path_match, data):
|
||||
""" Returns the state of a specific entity. """
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "78343829ea70bf07a9e939b321587122"
|
||||
VERSION = "43699d5ec727d3444985a1028d21e0d9"
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
@ -33,6 +33,8 @@
|
||||
"paper-dropdown": "polymer/paper-dropdown#~0.5.2",
|
||||
"paper-item": "polymer/paper-item#~0.5.2",
|
||||
"moment": "~2.8.4",
|
||||
"core-style": "polymer/core-style#~0.5.2"
|
||||
"core-style": "polymer/core-style#~0.5.2",
|
||||
"paper-slider": "polymer/paper-slider#~0.5.2",
|
||||
"color-picker-element": "~0.0.2"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-configurator" attributes="stateObj api" noscript>
|
||||
<template>
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info stateObj="{{stateObj}}"></state-info>
|
||||
<div class='state'>{{stateObj.stateDisplay}}</div>
|
||||
</div>
|
||||
|
||||
<!-- pre load the image so the dialog is rendered the proper size -->
|
||||
<template if="{{stateObj.attributes.description_image}}">
|
||||
<img hidden src="{{stateObj.attributes.description_image}}" />
|
||||
</template>
|
||||
</template>
|
||||
</polymer-element>
|
@ -0,0 +1,32 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-display.html">
|
||||
<link rel="import" href="state-card-toggle.html">
|
||||
<link rel="import" href="state-card-thermostat.html">
|
||||
<link rel="import" href="state-card-configurator.html">
|
||||
|
||||
<polymer-element name="state-card-content" attributes="api stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='card'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObjChanged: function() {
|
||||
while (this.$.card.lastChild) {
|
||||
this.$.card.removeChild(this.$.card.lastChild);
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + this.stateObj.cardType);
|
||||
stateCard.api = this.api;
|
||||
stateCard.stateObj = this.stateObj;
|
||||
this.$.card.appendChild(stateCard);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,23 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-display" attributes="stateObj" noscript>
|
||||
<template>
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info stateObj="{{stateObj}}"></state-info>
|
||||
<div class='state'>{{stateObj.stateDisplay}}</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
@ -0,0 +1,42 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-thermostat" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.target {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.current {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info stateObj="{{stateObj}}"></state-info>
|
||||
<div class='state'>
|
||||
<div class='target'>
|
||||
{{stateObj.stateDisplay}}
|
||||
</div>
|
||||
|
||||
<div class='current'>
|
||||
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,92 @@
|
||||
<script src="../bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-toggle" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
/* the splash while enabling */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
|
||||
color: #0091ea;
|
||||
}
|
||||
|
||||
/* filling of circle when checked */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
/* line when checked */
|
||||
paper-toggle-button::shadow #toggleBar[checked] {
|
||||
background-color: #039be5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info flex stateObj="{{stateObj}}"></state-info>
|
||||
|
||||
<paper-toggle-button self-center
|
||||
checked="{{toggleChecked}}"
|
||||
on-click="{{toggleClicked}}">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
toggleChecked: -1,
|
||||
|
||||
observe: {
|
||||
'stateObj.state': 'stateChanged'
|
||||
},
|
||||
|
||||
// prevent the event from propegating
|
||||
toggleClicked: function(ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
toggleCheckedChanged: function(oldVal, newVal) {
|
||||
// to filter out init
|
||||
if(oldVal === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newVal && this.stateObj.state == "off") {
|
||||
this.turn_on();
|
||||
} else if(!newVal && this.stateObj.state == "on") {
|
||||
this.turn_off();
|
||||
}
|
||||
},
|
||||
|
||||
stateChanged: function(oldVal, newVal) {
|
||||
this.toggleChecked = newVal === "on";
|
||||
},
|
||||
|
||||
turn_on: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
this.api.turn_on(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
turn_off: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
this.api.turn_off(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,33 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="state-card-content.html">
|
||||
|
||||
<polymer-element name="state-card" attributes="api stateObj" on-click="cardClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
|
||||
transition: all 0.30s ease-out;
|
||||
|
||||
position: relative;
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<state-card-content stateObj={{stateObj}} api={{api}}></state-card-content>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
cardClicked: function() {
|
||||
this.api.showmoreInfoDialog(this.stateObj.entity_id);
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,9 +1,11 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="bower_components/core-icons/social-icons.html">
|
||||
<link rel="import" href="bower_components/core-icons/image-icons.html">
|
||||
<link rel="import" href="bower_components/core-icons/hardware-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-icons/social-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icons/image-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
|
||||
|
||||
<link rel="import" href="../resources/home-assistant-icons.html">
|
||||
|
||||
<polymer-element name="domain-icon"
|
||||
attributes="domain state" constructor="DomainIcon">
|
||||
@ -51,6 +53,18 @@
|
||||
case "simple_alarm":
|
||||
return "social:notifications";
|
||||
|
||||
case "notify":
|
||||
return "announcement";
|
||||
|
||||
case "thermostat":
|
||||
return "homeassistant:thermostat";
|
||||
|
||||
case "sensor":
|
||||
return "visibility";
|
||||
|
||||
case "configurator":
|
||||
return "settings";
|
||||
|
||||
default:
|
||||
return "bookmark-outline";
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="entity-list" attributes="api cbEntityClicked">
|
||||
<template>
|
@ -1,4 +1,4 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="events-list" attributes="api cbEventClicked">
|
||||
<template>
|
@ -1,7 +1,7 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="bower_components/core-menu/core-submenu.html">
|
||||
<link rel="import" href="bower_components/core-item/core-item.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-submenu.html">
|
||||
<link rel="import" href="../bower_components/core-item/core-item.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/core-image/core-image.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-image/core-image.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
|
83
homeassistant/components/http/www_static/polymer/components/state-cards.html
Executable file
83
homeassistant/components/http/www_static/polymer/components/state-cards.html
Executable file
@ -0,0 +1,83 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../cards/state-card.html">
|
||||
|
||||
<polymer-element name="state-cards" attributes="api filter">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 764px) {
|
||||
:host {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
width: calc(50% - 44px);
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1100px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1450px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-states-content {
|
||||
max-width: 500px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
|
||||
padding: 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal layout wrap>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card class="state-card" stateObj={{state}} api={{api}}></state-card>
|
||||
</template>
|
||||
|
||||
<template if="{{states.length == 0}}">
|
||||
<div class='no-states-content'>
|
||||
<content></content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
filter: null,
|
||||
states: [],
|
||||
|
||||
observe: {
|
||||
'api.states': 'filterChanged'
|
||||
},
|
||||
|
||||
filterChanged: function() {
|
||||
if(this.filter === 'customgroup') {
|
||||
this.states = this.api.getCustomGroups();
|
||||
} else {
|
||||
// if no filter, return all non-group states
|
||||
this.states = this.api.states.filter(function(state) {
|
||||
return state.domain != 'group';
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
47
homeassistant/components/http/www_static/polymer/components/state-info.html
Executable file
47
homeassistant/components/http/www_static/polymer/components/state-info.html
Executable file
@ -0,0 +1,47 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="state-badge.html">
|
||||
|
||||
<polymer-element name="state-info" attributes="stateObj" noscript>
|
||||
<template>
|
||||
<style>
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<state-badge stateObj="{{stateObj}}"></state-badge>
|
||||
|
||||
<div class='info'>
|
||||
<div class='name'>
|
||||
{{stateObj.entityDisplay}}
|
||||
</div>
|
||||
|
||||
<div class="time-ago">
|
||||
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
|
||||
{{stateObj.relativeLastChanged}}
|
||||
</core-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
@ -0,0 +1,89 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/events-list.html">
|
||||
|
||||
<polymer-element name="event-fire-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Fire Event"
|
||||
class='two-column'
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input
|
||||
id="inputType" label="Event Type" floatingLabel="true"
|
||||
autofocus required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Event Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available events:</b>
|
||||
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.eventSelected = this.eventSelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(eventType, eventData) {
|
||||
this.setEventType(eventType);
|
||||
this.setEventData(eventData);
|
||||
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setEventType: function(eventType) {
|
||||
this.$.inputType.value = eventType;
|
||||
},
|
||||
|
||||
setEventData: function(eventData) {
|
||||
this.$.inputData.value = eventData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
},
|
||||
|
||||
eventSelected: function(eventType) {
|
||||
this.setEventType(eventType);
|
||||
},
|
||||
|
||||
clickFireEvent: function() {
|
||||
try {
|
||||
this.api.fire_event(
|
||||
this.$.inputType.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
this.$.dialog.close();
|
||||
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,18 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-action-dialog.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
|
||||
|
||||
<polymer-element name="ha-action-dialog" extends="paper-action-dialog">
|
||||
<template>
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
<shadow></shadow>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
layered: true,
|
||||
backdrop: true,
|
||||
transition: 'core-transition-bottom',
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,62 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
<link rel="import" href="../more-infos/more-info-content.html">
|
||||
|
||||
<polymer-element name="more-info-dialog" attributes="api">
|
||||
<template>
|
||||
<ha-action-dialog id="dialog">
|
||||
|
||||
<style>
|
||||
.title-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<state-card-content stateObj="{{stateObj}}" api="{{api}}" class='title-card'>
|
||||
</state-card-content>
|
||||
<more-info-content stateObj="{{stateObj}}" api="{{api}}"></more-info-content>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive on-click={{editClicked}}>Debug</paper-button>
|
||||
<paper-button affirmative>Dismiss</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObj: {},
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'reposition'
|
||||
},
|
||||
|
||||
/**
|
||||
* Whenever the attributes change, the more info component can
|
||||
* hide or show elements. We will reposition the dialog.
|
||||
*/
|
||||
reposition: function(oldVal, newVal) {
|
||||
// Only resize if already open
|
||||
if(this.$.dialog.opened) {
|
||||
this.job('resizeAfterLayoutChange', function() {
|
||||
this.$.dialog.resizeHandler();
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
},
|
||||
|
||||
show: function(stateObj) {
|
||||
this.stateObj = stateObj;
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
editClicked: function(ev) {
|
||||
this.api.showEditStateDialog(this.stateObj.entity_id);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,87 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/services-list.html">
|
||||
|
||||
<polymer-element name="service-call-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Call Service"
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Service Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available services:</b>
|
||||
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.serviceSelected = this.serviceSelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(domain, service, serviceData) {
|
||||
this.setService(domain, service);
|
||||
this.$.inputData.value = serviceData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setService: function(domain, service) {
|
||||
this.$.inputDomain.value = domain;
|
||||
this.$.inputService.value = service;
|
||||
},
|
||||
|
||||
serviceSelected: function(domain, service) {
|
||||
this.setService(domain, service);
|
||||
},
|
||||
|
||||
clickCallService: function() {
|
||||
try {
|
||||
this.api.call_service(
|
||||
this.$.inputDomain.value,
|
||||
this.$.inputService.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
|
||||
this.$.dialog.close();
|
||||
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,105 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="ha-action-dialog.html">
|
||||
<link rel="import" href="../components/entity-list.html">
|
||||
|
||||
<polymer-element name="state-set-dialog" attributes="api">
|
||||
<template>
|
||||
<ha-action-dialog
|
||||
id="dialog"
|
||||
heading="Set State"
|
||||
closeSelector='[dismissive]'>
|
||||
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
|
||||
<p>
|
||||
This dialog will update the representation of the device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</p>
|
||||
|
||||
<div layout horizontal>
|
||||
<div class='ha-form'>
|
||||
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="State attributes (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Current entities:</b>
|
||||
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
|
||||
</ha-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.entitySelected = this.entitySelected.bind(this);
|
||||
},
|
||||
|
||||
show: function(entityId, state, stateData) {
|
||||
this.setEntityId(entityId);
|
||||
this.setState(state);
|
||||
this.setStateData(stateData);
|
||||
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
setEntityId: function(entityId) {
|
||||
this.$.inputEntityID.value = entityId;
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.$.inputState.value = state;
|
||||
},
|
||||
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
this.$.inputData.value = value;
|
||||
},
|
||||
|
||||
entitySelected: function(entityId) {
|
||||
this.setEntityId(entityId);
|
||||
|
||||
var state = this.api.getState(entityId);
|
||||
this.setState(state.state);
|
||||
this.setStateData(state.attributes);
|
||||
},
|
||||
|
||||
clickSetState: function(ev) {
|
||||
try {
|
||||
this.api.set_state(
|
||||
this.$.inputEntityID.value,
|
||||
this.$.inputState.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
|
||||
);
|
||||
|
||||
this.$.dialog.close();
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,109 +0,0 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
|
||||
<link rel="import" href="bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="events-list.html">
|
||||
|
||||
<polymer-element name="event-fire-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<paper-action-dialog id="dialog" heading="Fire Event" transition="core-transition-bottom" backdrop>
|
||||
<style>
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.eventContainer {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
paper-action-dialog {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 64px);
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
.eventContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout horizontal>
|
||||
<div>
|
||||
<paper-input id="inputType" label="Event Type" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Event Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
<div class='eventContainer'>
|
||||
<b>Available events:</b>
|
||||
<events-list api={{api}} cbEventClicked={{eventSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickFireEvent}}>Fire Event</paper-button>
|
||||
</paper-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.eventSelected = this.eventSelected.bind(this)
|
||||
},
|
||||
|
||||
show: function(eventType, eventData) {
|
||||
this.setEventType(eventType);
|
||||
this.setEventData(eventData);
|
||||
|
||||
this.$.dialog.toggle();
|
||||
},
|
||||
|
||||
setEventType: function(eventType) {
|
||||
this.$.inputType.value = eventType;
|
||||
},
|
||||
|
||||
setEventData: function(eventData) {
|
||||
this.$.inputData.value = eventData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
},
|
||||
|
||||
eventSelected: function(eventType) {
|
||||
this.setEventType(eventType);
|
||||
},
|
||||
|
||||
clickFireEvent: function() {
|
||||
var data;
|
||||
|
||||
if(this.$.inputData.value != "") {
|
||||
data = JSON.parse(this.$.inputData.value);
|
||||
} else {
|
||||
data = {};
|
||||
}
|
||||
|
||||
this.api.fire_event(this.$.inputType.value, data);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,18 +1,46 @@
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<link rel="import" href="event-fire-dialog.html">
|
||||
<link rel="import" href="service-call-dialog.html">
|
||||
<link rel="import" href="state-set-dialog.html">
|
||||
<link rel="import" href="dialogs/event-fire-dialog.html">
|
||||
<link rel="import" href="dialogs/service-call-dialog.html">
|
||||
<link rel="import" href="dialogs/state-set-dialog.html">
|
||||
<link rel="import" href="dialogs/more-info-dialog.html">
|
||||
|
||||
<script>
|
||||
var ha = {};
|
||||
ha.util = {};
|
||||
|
||||
ha.util.parseTime = function(timeString) {
|
||||
return moment(timeString, "HH:mm:ss DD-MM-YYYY");
|
||||
};
|
||||
|
||||
ha.util.relativeTime = function(timeString) {
|
||||
return ha.util.parseTime(timeString).fromNow();
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.relativeHATime = function(timeString) {
|
||||
return ha.util.relativeTime(timeString);
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
|
||||
return (timeString || "").split(' ')[0];
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<polymer-element name="home-assistant-api" attributes="auth">
|
||||
<template>
|
||||
<paper-toast id="toast" role="alert" text=""></paper-toast>
|
||||
<event-fire-dialog id="eventDialog" api={{api}}></event-fire-dialog>
|
||||
<service-call-dialog id="serviceDialog" api={{api}}></service-call-dialog>
|
||||
<state-set-dialog id="stateDialog" api={{api}}></state-set-dialog>
|
||||
<state-set-dialog id="stateSetDialog" api={{api}}></state-set-dialog>
|
||||
<more-info-dialog id="moreInfoDialog" api={{api}}></more-info-dialog>
|
||||
</template>
|
||||
<script>
|
||||
var domainsWithCard = ['thermostat', 'configurator'];
|
||||
var domainsWithMoreInfo = ['light', 'group', 'sun', 'configurator'];
|
||||
|
||||
State = function(json, api) {
|
||||
this.api = api;
|
||||
@ -22,12 +50,12 @@
|
||||
this.entity_id = json.entity_id;
|
||||
var parts = json.entity_id.split(".");
|
||||
this.domain = parts[0];
|
||||
this.entity = parts[1];
|
||||
this.object_id = parts[1];
|
||||
|
||||
if(this.attributes.friendly_name) {
|
||||
this.entityDisplay = this.attributes.friendly_name;
|
||||
} else {
|
||||
this.entityDisplay = this.entity.replace(/_/g, " ");
|
||||
this.entityDisplay = this.object_id.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
this.state = json.state;
|
||||
@ -64,13 +92,33 @@
|
||||
// how to render the card for this state
|
||||
cardType: {
|
||||
get: function() {
|
||||
if(this.canToggle) {
|
||||
if(domainsWithCard.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else if(this.canToggle) {
|
||||
return "toggle";
|
||||
} else {
|
||||
return "display";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// how to render the more info of this state
|
||||
moreInfoType: {
|
||||
get: function() {
|
||||
if(domainsWithMoreInfo.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
relativeLastChanged: {
|
||||
get: function() {
|
||||
return ha.util.relativeTime(this.last_changed);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Polymer({
|
||||
@ -89,6 +137,14 @@
|
||||
},
|
||||
|
||||
// local methods
|
||||
removeState: function(entityId) {
|
||||
var state = this.getState(entityId);
|
||||
|
||||
if (state !== null) {
|
||||
this.states.splice(this.states.indexOf(state), 1);
|
||||
}
|
||||
},
|
||||
|
||||
getState: function(entityId) {
|
||||
var found = this.states.filter(function(state) {
|
||||
return state.entity_id == entityId;
|
||||
@ -97,6 +153,24 @@
|
||||
return found.length > 0 ? found[0] : null;
|
||||
},
|
||||
|
||||
getStates: function(entityIds) {
|
||||
var states = [];
|
||||
var state;
|
||||
for(var i = 0; i < entityIds.length; i++) {
|
||||
state = this.getState(entityIds[i]);
|
||||
|
||||
if(state !== null) {
|
||||
states.push(state);
|
||||
}
|
||||
}
|
||||
return states;
|
||||
},
|
||||
|
||||
getEntityIDs: function() {
|
||||
return this.states.map(
|
||||
function(state) { return state.entity_id; });
|
||||
},
|
||||
|
||||
hasService: function(domain, service) {
|
||||
var found = this.services.filter(function(serv) {
|
||||
return serv.domain == domain && serv.services.indexOf(service) !== -1;
|
||||
@ -105,6 +179,10 @@
|
||||
return found.length > 0;
|
||||
},
|
||||
|
||||
getCustomGroups: function() {
|
||||
return this.states.filter(function(state) { return state.isCustomGroup;});
|
||||
},
|
||||
|
||||
_laterFetchStates: function() {
|
||||
if(this.stateUpdateTimeout) {
|
||||
clearTimeout(this.stateUpdateTimeout);
|
||||
@ -114,8 +192,8 @@
|
||||
this.stateUpdateTimeout = setTimeout(this.fetchStates.bind(this), 60000);
|
||||
},
|
||||
|
||||
_sortStates: function(states) {
|
||||
states.sort(function(one, two) {
|
||||
_sortStates: function() {
|
||||
this.states.sort(function(one, two) {
|
||||
if (one.entity_id > two.entity_id) {
|
||||
return 1;
|
||||
} else if (one.entity_id < two.entity_id) {
|
||||
@ -126,34 +204,62 @@
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Pushes a new state to the state machine.
|
||||
* Will resort the states after a push and fire states-updated event.
|
||||
*/
|
||||
_pushNewState: function(new_state) {
|
||||
var state;
|
||||
var stateFound = false;
|
||||
|
||||
for(var i = 0; i < this.states.length; i++) {
|
||||
if(this.states[i].entity_id == new_state.entity_id) {
|
||||
state = this.states[i];
|
||||
state.attributes = new_state.attributes;
|
||||
state.last_changed = new_state.last_changed;
|
||||
state.state = new_state.state;
|
||||
|
||||
stateFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!stateFound) {
|
||||
this.states.push(new State(new_state, this));
|
||||
this._sortStates(this.states);
|
||||
if (this.__pushNewState(new_state)) {
|
||||
this._sortStates();
|
||||
}
|
||||
|
||||
this.fire('states-updated');
|
||||
},
|
||||
|
||||
_pushNewStates: function(new_states) {
|
||||
new_states.map(function(state) {
|
||||
this._pushNewState(state);
|
||||
/**
|
||||
* Creates or updates a state. Returns if a new state was added.
|
||||
*/
|
||||
__pushNewState: function(new_state) {
|
||||
var curState = this.getState(new_state.entity_id);
|
||||
|
||||
if (curState === null) {
|
||||
this.states.push(new State(new_state, this));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
curState.attributes = new_state.attributes;
|
||||
curState.last_changed = new_state.last_changed;
|
||||
curState.state = new_state.state;
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_pushNewStates: function(newStates, removeNonPresent) {
|
||||
removeNonPresent = !!removeNonPresent;
|
||||
var currentEntityIds = removeNonPresent ? this.getEntityIDs() : [];
|
||||
|
||||
var hasNew = newStates.reduce(function(hasNew, newState) {
|
||||
var isNewState = this.__pushNewState(newState);
|
||||
|
||||
if (isNewState) {
|
||||
return true;
|
||||
} else if(removeNonPresent) {
|
||||
currentEntityIds.splice(currentEntityIds.indexOf(newState.entity_id), 1);
|
||||
}
|
||||
|
||||
return hasNew;
|
||||
}.bind(this), false);
|
||||
|
||||
currentEntityIds.forEach(function(entityId) {
|
||||
this.removeState(entityId);
|
||||
}.bind(this));
|
||||
|
||||
if (hasNew) {
|
||||
this._sortStates();
|
||||
}
|
||||
|
||||
this.fire('states-updated');
|
||||
},
|
||||
|
||||
// call api methods
|
||||
@ -173,13 +279,7 @@
|
||||
|
||||
fetchStates: function(onSuccess, onError) {
|
||||
var successStatesUpdate = function(newStates) {
|
||||
this._sortStates(newStates);
|
||||
|
||||
this.states = newStates.map(function(json) {
|
||||
return new State(json, this);
|
||||
}.bind(this));
|
||||
|
||||
this.fire('states-updated');
|
||||
this._pushNewStates(newStates, true);
|
||||
|
||||
this._laterFetchStates();
|
||||
|
||||
@ -330,6 +430,10 @@
|
||||
},
|
||||
|
||||
// show dialogs
|
||||
showmoreInfoDialog: function(entityId) {
|
||||
this.$.moreInfoDialog.show(this.getState(entityId));
|
||||
},
|
||||
|
||||
showEditStateDialog: function(entityId) {
|
||||
var state = this.getState(entityId);
|
||||
|
||||
@ -341,7 +445,7 @@
|
||||
state = state || "";
|
||||
stateAttributes = stateAttributes || null;
|
||||
|
||||
this.$.stateDialog.show(entityId, state, stateAttributes);
|
||||
this.$.stateSetDialog.show(entityId, state, stateAttributes);
|
||||
},
|
||||
|
||||
showFireEventDialog: function(eventType, eventData) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
<link rel="import" href="bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="bower_components/paper-item/paper-item.html">
|
||||
|
||||
<link rel="import" href="states-cards.html">
|
||||
<link rel="import" href="components/state-cards.html">
|
||||
|
||||
<polymer-element name="home-assistant-main" attributes="api">
|
||||
<template>
|
||||
@ -94,43 +94,52 @@
|
||||
</paper-dropdown>
|
||||
</paper-menu-button>
|
||||
|
||||
<div class="bottom fit" horizontal layout>
|
||||
<paper-tabs id="tabsHolder" noink flex
|
||||
selected="0" on-core-select="{{tabClicked}}">
|
||||
|
||||
<paper-tab>ALL</paper-tab>
|
||||
<template if="{{hasCustomGroups}}">
|
||||
<div class="bottom fit" horizontal layout>
|
||||
<paper-tabs id="tabsHolder" noink flex
|
||||
selected="0" on-core-select="{{tabClicked}}">
|
||||
|
||||
<template repeat="{{group in customGroups}}">
|
||||
<paper-tab data-entity="{{group.entity_id}}">
|
||||
{{group.entityDisplay}}
|
||||
</paper-tab>
|
||||
</template>
|
||||
|
||||
</paper-tabs>
|
||||
</div>
|
||||
<paper-tab>ALL</paper-tab>
|
||||
<paper-tab data-filter='customgroup'>GROUPS</paper-tab>
|
||||
|
||||
</paper-tabs>
|
||||
</div>
|
||||
</template>
|
||||
</core-toolbar>
|
||||
|
||||
<states-cards
|
||||
<state-cards
|
||||
api="{{api}}"
|
||||
filter="{{selectedTab}}"
|
||||
class="content"></states-cards>
|
||||
filter="{{selectedFilter}}"
|
||||
class="content">
|
||||
<h3>Hi there!</h3>
|
||||
<p>
|
||||
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
|
||||
</p>
|
||||
<p>
|
||||
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
|
||||
</p>
|
||||
</state-cards>
|
||||
|
||||
</core-header-panel>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
selectedTab: null,
|
||||
selectedFilter: null,
|
||||
hasCustomGroups: false,
|
||||
|
||||
computed: {
|
||||
customGroups: "getCustomGroups(api.states)",
|
||||
hasCustomGroups: "customGroups.length > 0"
|
||||
observe: {
|
||||
'api.states': 'updateHasCustomGroup'
|
||||
},
|
||||
|
||||
// computed: {
|
||||
// hasCustomGroups: "api.getCustomGroups().length > 0"
|
||||
// },
|
||||
|
||||
tabClicked: function(ev) {
|
||||
if(ev.detail.isSelected) {
|
||||
// will be null for ALL tab
|
||||
this.selectedTab = ev.detail.item.getAttribute('data-entity');
|
||||
this.selectedFilter = ev.detail.item.getAttribute('data-filter');
|
||||
}
|
||||
},
|
||||
|
||||
@ -154,9 +163,8 @@
|
||||
this.api.logOut();
|
||||
},
|
||||
|
||||
getCustomGroups: function(states) {
|
||||
return states ?
|
||||
states.filter(function(state) { return state.isCustomGroup;}) : [];
|
||||
updateHasCustomGroup: function() {
|
||||
this.hasCustomGroups = this.api.getCustomGroups().length > 0;
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -0,0 +1,91 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<polymer-element name="more-info-configurator" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
p > img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
p.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p.error {
|
||||
color: #C62828;
|
||||
}
|
||||
|
||||
p.submit {
|
||||
text-align: center;
|
||||
height: 41px;
|
||||
}
|
||||
|
||||
p.submit paper-spinner {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
p.submit span {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout vertical>
|
||||
<template if="{{stateObj.state == 'configure'}}">
|
||||
|
||||
<p hidden?="{{!stateObj.attributes.description}}">
|
||||
{{stateObj.attributes.description}}
|
||||
</p>
|
||||
|
||||
<p class='error' hidden?="{{!stateObj.attributes.errors}}">
|
||||
{{stateObj.attributes.errors}}
|
||||
</p>
|
||||
|
||||
<p class='center' hidden?="{{!stateObj.attributes.description_image}}">
|
||||
<img src='{{stateObj.attributes.description_image}}' />
|
||||
</p>
|
||||
|
||||
<p class='submit'>
|
||||
<paper-button raised on-click="{{submitClicked}}"
|
||||
hidden?="{{action !== 'display'}}">
|
||||
{{stateObj.attributes.submit_caption || "Set configuration"}}
|
||||
</paper-button>
|
||||
|
||||
<span hidden?="{{action !== 'configuring'}}">
|
||||
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
action: "display",
|
||||
|
||||
submitClicked: function() {
|
||||
this.action = "configuring";
|
||||
var data = {
|
||||
configure_id: this.stateObj.attributes.configure_id
|
||||
};
|
||||
|
||||
this.api.call_service('configurator', 'configure', data, {
|
||||
success: function() {
|
||||
this.action = 'display';
|
||||
this.api.fetchAll();
|
||||
}.bind(this),
|
||||
error: function() {
|
||||
this.action = 'display';
|
||||
}.bind(this)
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,45 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="more-info-default.html">
|
||||
<link rel="import" href="more-info-light.html">
|
||||
<link rel="import" href="more-info-group.html">
|
||||
<link rel="import" href="more-info-sun.html">
|
||||
<link rel="import" href="more-info-configurator.html">
|
||||
|
||||
<polymer-element name="more-info-content" attributes="api stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='moreInfo' class='{{classNames}}'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
classNames: '',
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'stateAttributesChanged',
|
||||
},
|
||||
|
||||
stateObjChanged: function() {
|
||||
while (this.$.moreInfo.lastChild) {
|
||||
this.$.moreInfo.removeChild(this.$.moreInfo.lastChild);
|
||||
}
|
||||
|
||||
var moreInfo = document.createElement("more-info-" + this.stateObj.moreInfoType);
|
||||
moreInfo.api = this.api;
|
||||
moreInfo.stateObj = this.stateObj;
|
||||
this.$.moreInfo.appendChild(moreInfo);
|
||||
},
|
||||
|
||||
stateAttributesChanged: function(oldVal, newVal) {
|
||||
this.classNames = Object.keys(newVal).map(
|
||||
function(key) { return "has-" + key; }).join(' ');
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,44 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="more-info-default" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
padding-left: 10px;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout vertical>
|
||||
|
||||
<template repeat="{{key in stateObj.attributes | getKeys}}">
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div>
|
||||
{{key}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
{{stateObj.attributes[key]}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
getKeys: function(obj) {
|
||||
return Object.keys(obj || {});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,33 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
|
||||
<polymer-element name="more-info-group" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
.child-card {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.child-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card-content stateObj="{{state}}" api="{{api}}" class='child-card'>
|
||||
</state-card-content>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
observe: {
|
||||
'stateObj.attributes.entity_id': 'updateStates'
|
||||
},
|
||||
|
||||
updateStates: function() {
|
||||
this.states = this.api.getStates(this.stateObj.attributes.entity_id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,109 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
|
||||
|
||||
<link rel="import" href="../bower_components/color-picker-element/dist/color-picker.html">
|
||||
|
||||
<polymer-element name="more-info-light" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
.brightness {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.brightness paper-slider::shadow #sliderKnobInner,
|
||||
.brightness paper-slider::shadow #sliderBar::shadow #activeProgress {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
color-picker {
|
||||
display: block;
|
||||
width: 350px;
|
||||
margin: 0 auto;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in .3s;
|
||||
}
|
||||
|
||||
:host-context(.has-brightness) .brightness {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
:host-context(.has-xy_color) color-picker {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div>
|
||||
<div class='brightness'>
|
||||
<div center horizontal layout>
|
||||
<div>Brightness</div>
|
||||
<paper-slider
|
||||
max="255" flex id='brightness'
|
||||
on-core-change="{{brightnessSliderChanged}}">
|
||||
</paper-slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<color-picker id="colorpicker" width="350" height="200">
|
||||
</color-picker>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
// on-change is unpredictable so using on-core-change this has side effect
|
||||
// that it fires if changed by brightnessChanged(), thus an ignore boolean.
|
||||
ignoreNextBrightnessEvent: false,
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes.brightness': 'brightnessChanged',
|
||||
'stateObj.attributes.xy_color': 'colorChanged'
|
||||
},
|
||||
|
||||
brightnessChanged: function(oldVal, newVal) {
|
||||
this.ignoreNextBrightnessEvent = true;
|
||||
|
||||
this.$.brightness.value = newVal;
|
||||
},
|
||||
|
||||
domReady: function() {
|
||||
this.$.colorpicker.addEventListener('colorselected', this.colorPicked.bind(this));
|
||||
},
|
||||
|
||||
brightnessSliderChanged: function(ev, details, target) {
|
||||
if(this.ignoreNextBrightnessEvent) {
|
||||
this.ignoreNextBrightnessEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var bri = parseInt(target.value);
|
||||
|
||||
if(isNaN(bri)) return;
|
||||
|
||||
if(bri === 0) {
|
||||
this.api.turn_off(this.stateObj.entity_id);
|
||||
} else {
|
||||
this.api.call_service("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
brightness: bri
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorPicked: function(ev) {
|
||||
var color = ev.detail.rgb;
|
||||
|
||||
this.api.call_service("light", "turn_on", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
rgb_color: [color.r, color.g, color.b]
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,62 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="more-info-sun" attributes="stateObj api">
|
||||
<template>
|
||||
<style>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout vertical id='sunData'>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='rising'>
|
||||
<div>
|
||||
Rising {{stateObj.attributes.next_rising | relativeHATime}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
{{stateObj.attributes.next_rising | HATimeStripDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='setting'>
|
||||
<div>
|
||||
Setting {{stateObj.attributes.next_setting | relativeHATime}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
{{stateObj.attributes.next_setting | HATimeStripDate}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
|
||||
stateObjChanged: function() {
|
||||
var rising = ha.util.parseTime(this.stateObj.attributes.next_rising);
|
||||
var setting = ha.util.parseTime(this.stateObj.attributes.next_setting);
|
||||
|
||||
if(rising > setting) {
|
||||
this.$.sunData.appendChild(this.$.rising);
|
||||
} else {
|
||||
this.$.sunData.appendChild(this.$.setting);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -0,0 +1,13 @@
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
|
||||
|
||||
<core-iconset-svg id="homeassistant" iconSize="100">
|
||||
<svg><defs>
|
||||
<g id="thermostat">
|
||||
<!--
|
||||
Thermostat icon created by Scott Lewis from the Noun Project
|
||||
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
|
||||
-->
|
||||
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
@ -0,0 +1,60 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="home-assistant-style" noscript>
|
||||
<template>
|
||||
<core-style id="ha-dialog">
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
|
||||
min-width: 350px;
|
||||
max-width: 700px;
|
||||
|
||||
/* First two are from core-transition-bottom */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
opacity 0.2s ease-in,
|
||||
top .3s,
|
||||
left .3s !important;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
:host.two-column {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 464px) {
|
||||
:host {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
</core-style>
|
||||
</template>
|
||||
</polymer-element>
|
@ -1,108 +0,0 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
|
||||
<link rel="import" href="bower_components/paper-dialog/paper-dialog-transition.html">
|
||||
<link rel="import" href="bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="services-list.html">
|
||||
|
||||
<polymer-element name="service-call-dialog" attributes="api">
|
||||
<template>
|
||||
|
||||
<paper-action-dialog id="dialog" heading="Call Service" transition="core-transition-bottom" backdrop="true">
|
||||
<style>
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.serviceContainer {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
paper-action-dialog {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 64px);
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
.serviceContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout horizontal>
|
||||
<div>
|
||||
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Service Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
<div class='serviceContainer'>
|
||||
<b>Available services:</b>
|
||||
<services-list api={{api}} cbServiceClicked={{serviceSelected}}></event-list>
|
||||
</div>
|
||||
</div>
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickCallService}}>Call Service</paper-button>
|
||||
</paper-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.serviceSelected = this.serviceSelected.bind(this)
|
||||
},
|
||||
|
||||
show: function(domain, service, serviceData) {
|
||||
this.setService(domain, service);
|
||||
this.$.inputData.value = serviceData;
|
||||
// this.$.inputDataWrapper.update();
|
||||
this.$.dialog.toggle();
|
||||
},
|
||||
|
||||
setService: function(domain, service) {
|
||||
this.$.inputDomain.value = domain;
|
||||
this.$.inputService.value = service;
|
||||
},
|
||||
|
||||
serviceSelected: function(domain, service) {
|
||||
this.setService(domain, service);
|
||||
},
|
||||
|
||||
clickCallService: function() {
|
||||
var data;
|
||||
|
||||
if(this.$.inputData.value != "") {
|
||||
data = JSON.parse(this.$.inputData.value);
|
||||
}
|
||||
|
||||
this.api.call_service(
|
||||
this.$.inputDomain.value,
|
||||
this.$.inputService.value,
|
||||
data);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -6,6 +6,7 @@
|
||||
|
||||
<link rel="import" href="home-assistant-main.html">
|
||||
<link rel="import" href="home-assistant-api.html">
|
||||
<link rel="import" href="resources/home-assistant-style.html">
|
||||
|
||||
<polymer-element name="splash-login" attributes="auth">
|
||||
<template>
|
||||
@ -37,6 +38,7 @@
|
||||
|
||||
</style>
|
||||
|
||||
<home-assistant-style></home-assistant-style>
|
||||
<home-assistant-api auth="{{auth}}" id="api"></home-assistant-api>
|
||||
|
||||
<div layout horizontal center fit class='login' id="splash">
|
||||
|
@ -1,30 +0,0 @@
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="state-info.html">
|
||||
|
||||
<polymer-element name="state-card-display"
|
||||
attributes="stateObj cb_edit"
|
||||
noscript>
|
||||
<template>
|
||||
<core-style ref='state-card'></core-style>
|
||||
<style>
|
||||
.state {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info
|
||||
stateObj="{{stateObj}}"
|
||||
cb_edit="{{cb_edit}}">
|
||||
</state-info>
|
||||
|
||||
<div class='state'>{{stateObj.stateDisplay}}</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
@ -1,117 +0,0 @@
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
<link rel="import" href="bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="state-badge.html">
|
||||
|
||||
<polymer-element name="state-card-toggle"
|
||||
attributes="stateObj cb_turn_on, cb_turn_off cb_edit">
|
||||
<template>
|
||||
<core-style ref='state-card'></core-style>
|
||||
<style>
|
||||
.state {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* the splash while enabling */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
|
||||
color: #0091ea;
|
||||
}
|
||||
|
||||
/* filling of circle when checked */
|
||||
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
/* line when checked */
|
||||
paper-toggle-button::shadow #toggleBar[checked] {
|
||||
background-color: #039be5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
|
||||
<state-info
|
||||
stateObj="{{stateObj}}"
|
||||
cb_edit="{{cb_edit}}">
|
||||
</state-info>
|
||||
|
||||
<div class='state toggle' self-center flex>
|
||||
<paper-toggle-button checked="{{toggleChecked}}">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObj: {},
|
||||
|
||||
cb_turn_on: null,
|
||||
cb_turn_off: null,
|
||||
cb_edit: null,
|
||||
toggleChecked: -1,
|
||||
|
||||
observe: {
|
||||
'stateObj.state': 'stateChanged'
|
||||
},
|
||||
|
||||
lastChangedFromNow: function(lastChanged) {
|
||||
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
|
||||
},
|
||||
|
||||
toggleCheckedChanged: function(oldVal, newVal) {
|
||||
// to filter out init
|
||||
if(oldVal === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newVal && this.stateObj.state == "off") {
|
||||
this.turn_on();
|
||||
} else if(!newVal && this.stateObj.state == "on") {
|
||||
this.turn_off();
|
||||
}
|
||||
},
|
||||
|
||||
stateChanged: function(oldVal, newVal) {
|
||||
this.toggleChecked = newVal === "on";
|
||||
},
|
||||
|
||||
turn_on: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
if(this.cb_turn_on) {
|
||||
this.cb_turn_on(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
turn_off: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
if(this.cb_turn_off) {
|
||||
this.cb_turn_off(this.stateObj.entity_id, {
|
||||
success: function() {
|
||||
this.stateChanged(this.stateObj.state, this.stateObj.state);
|
||||
}.bind(this)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
editClicked: function() {
|
||||
if(this.cb_edit) {
|
||||
this.cb_edit(this.stateObj.entity_id);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,74 +0,0 @@
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
|
||||
<link rel="import" href="bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="state-badge.html">
|
||||
|
||||
<polymer-element name="state-info"
|
||||
attributes="stateObj cb_edit">
|
||||
<template>
|
||||
<style>
|
||||
state-badge {
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
state-badge:hover {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<state-badge
|
||||
stateObj="{{stateObj}}"
|
||||
on-click="{{editClicked}}">
|
||||
</state-badge>
|
||||
|
||||
<div class='info'>
|
||||
<div class='name'>
|
||||
{{stateObj.entityDisplay}}
|
||||
</div>
|
||||
|
||||
<div class="time-ago">
|
||||
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
|
||||
{{lastChangedFromNow(stateObj.last_changed)}}
|
||||
</core-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
stateObj: {},
|
||||
|
||||
lastChangedFromNow: function(lastChanged) {
|
||||
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
|
||||
},
|
||||
|
||||
editClicked: function() {
|
||||
if(this.cb_edit) {
|
||||
this.cb_edit(this.stateObj.entity_id);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,115 +0,0 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/paper-dialog/paper-action-dialog.html">
|
||||
<link rel="import" href="bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
|
||||
<link rel="import" href="entity-list.html">
|
||||
|
||||
<polymer-element name="state-set-dialog" attributes="api">
|
||||
<template>
|
||||
<paper-action-dialog id="dialog" heading="Set State" transition="core-transition-bottom" backdrop="true">
|
||||
<style>
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
}
|
||||
|
||||
paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.stateContainer {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
paper-action-dialog {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 64px);
|
||||
top: 64px;
|
||||
}
|
||||
|
||||
.stateContainer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout horizontal>
|
||||
<div>
|
||||
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="State attributes (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<!--
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
-->
|
||||
<textarea id="inputData" rows="5"></textarea>
|
||||
</paper-input-decorator>
|
||||
</div>
|
||||
<div class='stateContainer'>
|
||||
<b>Current entities:</b>
|
||||
<entity-list api={{api}} cbEntityClicked={{entitySelected}}></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
<paper-button dismissive>Cancel</paper-button>
|
||||
<paper-button affirmative on-click={{clickSetState}}>Set State</paper-button>
|
||||
</paper-action-dialog>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.entitySelected = this.entitySelected.bind(this)
|
||||
},
|
||||
|
||||
show: function(entityId, state, stateData) {
|
||||
this.setEntityId(entityId);
|
||||
this.setState(state);
|
||||
this.setStateData(stateData);
|
||||
|
||||
this.$.dialog.toggle();
|
||||
},
|
||||
|
||||
setEntityId: function(entityId) {
|
||||
this.$.inputEntityID.value = entityId;
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.$.inputState.value = state;
|
||||
},
|
||||
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
this.$.inputData.value = value;
|
||||
},
|
||||
|
||||
entitySelected: function(entityId) {
|
||||
this.setEntityId(entityId);
|
||||
|
||||
var state = this.api.getState(entityId);
|
||||
this.setState(state.state);
|
||||
this.setStateData(state.attributes);
|
||||
},
|
||||
|
||||
clickSetState: function() {
|
||||
this.api.set_state(
|
||||
this.$.inputEntityID.value,
|
||||
this.$.inputState.value,
|
||||
JSON.parse(this.$.inputData.value)
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,117 +0,0 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="state-card-display.html">
|
||||
<link rel="import" href="state-card-toggle.html">
|
||||
|
||||
<polymer-element name="states-cards" attributes="api filter">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media all and (min-width: 764px) {
|
||||
:host {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.state-card {
|
||||
width: calc(50% - 44px);
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1100px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1450px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<core-style id="state-card">
|
||||
<!-- generic state card CSS -->
|
||||
:host {
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
|
||||
transition: all 0.30s ease-out;
|
||||
|
||||
position: relative;
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<div horizontal layout wrap>
|
||||
|
||||
<template id="display">
|
||||
<state-card-display
|
||||
class='state-card'
|
||||
stateObj="{{state}}"
|
||||
cb_edit={{editCallback}}>
|
||||
</state-card-display>
|
||||
</template>
|
||||
|
||||
<template id="toggle">
|
||||
<state-card-toggle
|
||||
class='state-card'
|
||||
stateObj="{{state}}"
|
||||
cb_turn_on="{{api.turn_on}}"
|
||||
cb_turn_off="{{api.turn_off}}"
|
||||
cb_edit={{editCallback}}>
|
||||
</state-card-display>
|
||||
</template>
|
||||
|
||||
<template repeat="{{state in getStates(api.states, filter)}}">
|
||||
<template bind ref="{{state.cardType}}"></template>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
filter: null,
|
||||
|
||||
getStates: function(states, filter) {
|
||||
if(!states) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(!filter) {
|
||||
// if no filter, return all non-group states
|
||||
return states.filter(function(state) {
|
||||
return state.domain != 'group';
|
||||
});
|
||||
} else {
|
||||
// we have a filter, return the parent filter and its children
|
||||
var filter_state = this.api.getState(this.filter);
|
||||
|
||||
var map_states = function(entity_id) {
|
||||
return this.api.getState(entity_id);
|
||||
}.bind(this);
|
||||
|
||||
return [filter_state].concat(
|
||||
filter_state.attributes.entity_id.map(map_states));
|
||||
}
|
||||
},
|
||||
|
||||
ready: function() {
|
||||
this.editCallback = this.editCallback.bind(this);
|
||||
},
|
||||
|
||||
editCallback: function(entityId) {
|
||||
this.api.showEditStateDialog(entityId);
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -46,7 +46,6 @@ def media_prev_track(hass):
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
""" Listen for keyboard events. """
|
||||
try:
|
||||
|
@ -52,20 +52,20 @@ import logging
|
||||
import os
|
||||
import csv
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers import (
|
||||
extract_entity_ids, platform_devices_from_config)
|
||||
from homeassistant.components import group
|
||||
generate_entity_id, extract_entity_ids, config_per_platform)
|
||||
from homeassistant.components import group, discovery, wink
|
||||
|
||||
|
||||
DOMAIN = "light"
|
||||
DEPENDENCIES = []
|
||||
|
||||
GROUP_NAME_ALL_LIGHTS = 'all_lights'
|
||||
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
|
||||
GROUP_NAME_ALL_LIGHTS)
|
||||
GROUP_NAME_ALL_LIGHTS = 'all lights'
|
||||
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
@ -87,9 +87,14 @@ ATTR_FLASH = "flash"
|
||||
FLASH_SHORT = "short"
|
||||
FLASH_LONG = "long"
|
||||
|
||||
|
||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_LIGHTS: 'wink',
|
||||
discovery.services.PHILIPS_HUE: 'hue',
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -163,40 +168,51 @@ def setup(hass, config):
|
||||
|
||||
return False
|
||||
|
||||
lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER)
|
||||
# Dict to track entity_id -> lights
|
||||
lights = {}
|
||||
|
||||
if not lights:
|
||||
return False
|
||||
# Track all lights in a group
|
||||
light_group = group.Group(hass, GROUP_NAME_ALL_LIGHTS, user_defined=False)
|
||||
|
||||
ent_to_light = {}
|
||||
def add_lights(new_lights):
|
||||
""" Add lights to the component to track. """
|
||||
for light in new_lights:
|
||||
if light is not None and light not in lights.values():
|
||||
light.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, light.name, lights.keys())
|
||||
|
||||
no_name_count = 1
|
||||
lights[light.entity_id] = light
|
||||
|
||||
for light in lights:
|
||||
name = light.get_name()
|
||||
light.update_ha_state(hass)
|
||||
|
||||
if name is None:
|
||||
name = "Light #{}".format(no_name_count)
|
||||
no_name_count += 1
|
||||
light_group.update_tracked_entity_ids(lights.keys())
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
ent_to_light.keys())
|
||||
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
|
||||
platform = get_component(ENTITY_ID_FORMAT.format(p_type))
|
||||
|
||||
light.entity_id = entity_id
|
||||
ent_to_light[entity_id] = light
|
||||
if platform is None:
|
||||
_LOGGER.error("Unknown type specified: %s", p_type)
|
||||
|
||||
platform.setup_platform(hass, p_config, add_lights)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_lights_state(now):
|
||||
""" Update the states of all the lights. """
|
||||
for light in lights:
|
||||
light.update_ha_state(hass)
|
||||
if lights:
|
||||
_LOGGER.info("Updating light states")
|
||||
|
||||
for light in lights.values():
|
||||
light.update_ha_state(hass, True)
|
||||
|
||||
update_lights_state(None)
|
||||
|
||||
# Track all lights in a group
|
||||
group.setup_group(
|
||||
hass, GROUP_NAME_ALL_LIGHTS, ent_to_light.keys(), False)
|
||||
def light_discovered(service, info):
|
||||
""" Called when a light is discovered. """
|
||||
platform = get_component(
|
||||
ENTITY_ID_FORMAT.format(DISCOVERY_PLATFORMS[service]))
|
||||
|
||||
platform.setup_platform(hass, {}, add_lights, info)
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), light_discovered)
|
||||
|
||||
def handle_light_service(service):
|
||||
""" Hande a turn light on or off service call. """
|
||||
@ -204,12 +220,12 @@ def setup(hass, config):
|
||||
dat = service.data
|
||||
|
||||
# Convert the entity ids to valid light ids
|
||||
lights = [ent_to_light[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in ent_to_light]
|
||||
target_lights = [lights[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in lights]
|
||||
|
||||
if not lights:
|
||||
lights = list(ent_to_light.values())
|
||||
if not target_lights:
|
||||
target_lights = lights.values()
|
||||
|
||||
params = {}
|
||||
|
||||
@ -219,7 +235,7 @@ def setup(hass, config):
|
||||
params[ATTR_TRANSITION] = transition
|
||||
|
||||
if service.service == SERVICE_TURN_OFF:
|
||||
for light in lights:
|
||||
for light in target_lights:
|
||||
# pylint: disable=star-args
|
||||
light.turn_off(**params)
|
||||
|
||||
@ -277,11 +293,11 @@ def setup(hass, config):
|
||||
elif dat[ATTR_FLASH] == FLASH_LONG:
|
||||
params[ATTR_FLASH] = FLASH_LONG
|
||||
|
||||
for light in lights:
|
||||
for light in target_lights:
|
||||
# pylint: disable=star-args
|
||||
light.turn_on(**params)
|
||||
|
||||
for light in lights:
|
||||
for light in target_lights:
|
||||
light.update_ha_state(hass, True)
|
||||
|
||||
# Update light state every 30 seconds
|
||||
|
@ -2,7 +2,9 @@
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
|
||||
@ -16,27 +18,59 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
PHUE_CONFIG_FILE = "phue.conf"
|
||||
|
||||
|
||||
def get_devices(hass, config):
|
||||
# Map ip to request id for configuring
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the Hue lights. """
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
import phue
|
||||
# pylint: disable=unused-variable
|
||||
import phue # noqa
|
||||
except ImportError:
|
||||
logger.exception("Error while importing dependency phue.")
|
||||
_LOGGER.exception("Error while importing dependency phue.")
|
||||
|
||||
return []
|
||||
return
|
||||
|
||||
host = config.get(CONF_HOST, None)
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
|
||||
# Only act if we are not already configuring this host
|
||||
if host in _CONFIGURING:
|
||||
return
|
||||
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
|
||||
|
||||
def setup_bridge(host, hass, add_devices_callback):
|
||||
""" Setup a phue bridge based on host parameter. """
|
||||
import phue
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host, config_file_path=hass.get_config_path(PHUE_CONFIG_FILE))
|
||||
except socket.error: # Error connecting using Phue
|
||||
logger.exception((
|
||||
"Error while connecting to the bridge. "
|
||||
"Did you follow the instructions to set it up?"))
|
||||
except ConnectionRefusedError: # Wrong host was given
|
||||
_LOGGER.exception("Error connecting to the Hue bridge at %s", host)
|
||||
|
||||
return []
|
||||
return
|
||||
|
||||
except phue.PhueRegistrationException:
|
||||
_LOGGER.warning("Connected to Hue at %s but not registered.", host)
|
||||
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
|
||||
return
|
||||
|
||||
# If we came here and configuring this host, mark as done
|
||||
if host in _CONFIGURING:
|
||||
request_id = _CONFIGURING.pop(host)
|
||||
|
||||
configurator = get_component('configurator')
|
||||
|
||||
configurator.request_done(request_id)
|
||||
|
||||
lights = {}
|
||||
|
||||
@ -47,25 +81,53 @@ def get_devices(hass, config):
|
||||
api = bridge.get_api()
|
||||
except socket.error:
|
||||
# socket.error when we cannot reach Hue
|
||||
logger.exception("Cannot reach the bridge")
|
||||
_LOGGER.exception("Cannot reach the bridge")
|
||||
return
|
||||
|
||||
api_states = api.get('lights')
|
||||
|
||||
if not isinstance(api_states, dict):
|
||||
logger.error("Got unexpected result from Hue API")
|
||||
_LOGGER.error("Got unexpected result from Hue API")
|
||||
return
|
||||
|
||||
new_lights = []
|
||||
|
||||
for light_id, info in api_states.items():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
new_lights.append(lights[light_id])
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
|
||||
if new_lights:
|
||||
add_devices_callback(new_lights)
|
||||
|
||||
update_lights()
|
||||
|
||||
return list(lights.values())
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
""" Request configuration steps from the user. """
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if host in _CONFIGURING:
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[host], "Failed to register, please try again.")
|
||||
|
||||
return
|
||||
|
||||
def hue_configuration_callback(data):
|
||||
""" Actions to do when our configuration callback is called. """
|
||||
setup_bridge(host, hass, add_devices_callback)
|
||||
|
||||
_CONFIGURING[host] = configurator.request_config(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
description=("Press the button on the bridge to register Philips Hue "
|
||||
"with Home Assistant."),
|
||||
description_image="/static/images/config_philips_hue.jpg",
|
||||
submit_caption="I have pressed the button"
|
||||
)
|
||||
|
||||
|
||||
class HueLight(ToggleDevice):
|
||||
@ -77,9 +139,36 @@ class HueLight(ToggleDevice):
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
|
||||
def get_name(self):
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this Hue light """
|
||||
return "{}.{}".format(
|
||||
self.__class__, self.info.get('uniqueid', self.name))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the mame of the Hue light. """
|
||||
return self.info['name']
|
||||
return self.info.get('name', 'No name')
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.name
|
||||
}
|
||||
|
||||
if self.is_on:
|
||||
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
|
||||
attr[ATTR_XY_COLOR] = self.info['state']['xy']
|
||||
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.update_lights()
|
||||
|
||||
return self.info['state']['reachable'] and self.info['state']['on']
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the specified or all lights on. """
|
||||
@ -118,24 +207,6 @@ class HueLight(ToggleDevice):
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
self.update_lights()
|
||||
|
||||
return self.info['state']['reachable'] and self.info['state']['on']
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
attr = {
|
||||
ATTR_FRIENDLY_NAME: self.get_name()
|
||||
}
|
||||
|
||||
if self.is_on():
|
||||
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
|
||||
attr[ATTR_XY_COLOR] = self.info['state']['xy']
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
""" Synchronize state with bridge. """
|
||||
self.update_lights(no_throttle=True)
|
||||
|
53
homeassistant/components/light/wink.py
Normal file
53
homeassistant/components/light/wink.py
Normal file
@ -0,0 +1,53 @@
|
||||
""" Support for Hue lights. """
|
||||
import logging
|
||||
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.wink.pywink as pywink
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return Wink lights. """
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if not pywink.is_token_set() and token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token - "
|
||||
"get one at https://winkbearertoken.appspot.com/")
|
||||
return
|
||||
|
||||
elif token is not None:
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices_callback(
|
||||
WinkLight(light) for light in pywink.get_bulbs())
|
||||
|
||||
|
||||
class WinkLight(WinkToggleDevice):
|
||||
""" Represents a Wink light """
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
if brightness is not None:
|
||||
self.wink.setState(True, brightness / 255)
|
||||
|
||||
else:
|
||||
self.wink.setState(True)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
attr = super().state_attributes
|
||||
|
||||
if self.is_on:
|
||||
brightness = self.wink.brightness()
|
||||
|
||||
if brightness is not None:
|
||||
attr[ATTR_BRIGHTNESS] = int(brightness * 255)
|
||||
|
||||
return attr
|
83
homeassistant/components/notify/__init__.py
Normal file
83
homeassistant/components/notify/__init__.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""
|
||||
homeassistant.components.notify
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to notify people.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
|
||||
DOMAIN = "notify"
|
||||
DEPENDENCIES = []
|
||||
|
||||
# Title of notification
|
||||
ATTR_TITLE = "title"
|
||||
ATTR_TITLE_DEFAULT = "Home Assistant"
|
||||
|
||||
# Text to notify user of
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
SERVICE_NOTIFY = "notify"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_message(hass, message):
|
||||
""" Send a notification message. """
|
||||
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up notify services. """
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER):
|
||||
return False
|
||||
|
||||
platform = config[DOMAIN].get(CONF_PLATFORM)
|
||||
|
||||
notify_implementation = get_component(
|
||||
'notify.{}'.format(platform))
|
||||
|
||||
if notify_implementation is None:
|
||||
_LOGGER.error("Unknown notification service specified.")
|
||||
|
||||
return False
|
||||
|
||||
notify_service = notify_implementation.get_service(hass, config)
|
||||
|
||||
if notify_service is None:
|
||||
_LOGGER.error("Failed to initialize notification service %s",
|
||||
platform)
|
||||
|
||||
return False
|
||||
|
||||
def notify_message(call):
|
||||
""" Handle sending notification message service calls. """
|
||||
message = call.data.get(ATTR_MESSAGE)
|
||||
|
||||
if message is None:
|
||||
return
|
||||
|
||||
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
|
||||
notify_service.send_message(message, title=title)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NOTIFY, notify_message)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class BaseNotificationService(object):
|
||||
""" Provides an ABC for notifcation services. """
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
"""
|
||||
Send a message.
|
||||
kwargs can contain ATTR_TITLE to specify a title.
|
||||
"""
|
||||
raise NotImplementedError
|
56
homeassistant/components/notify/pushbullet.py
Normal file
56
homeassistant/components/notify/pushbullet.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""
|
||||
PushBullet platform for notify component.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.components.notify import (
|
||||
DOMAIN, ATTR_TITLE, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
""" Get the pushbullet notification service. """
|
||||
|
||||
if not validate_config(config,
|
||||
{DOMAIN: [CONF_API_KEY]},
|
||||
_LOGGER):
|
||||
return None
|
||||
|
||||
try:
|
||||
# pylint: disable=unused-variable
|
||||
from pushbullet import PushBullet, InvalidKeyError # noqa
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Unable to import pushbullet. "
|
||||
"Did you maybe not install the 'pushbullet.py' package?")
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
|
||||
|
||||
except InvalidKeyError:
|
||||
_LOGGER.error(
|
||||
"Wrong API key supplied. "
|
||||
"Get it at https://www.pushbullet.com/account")
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class PushBulletNotificationService(BaseNotificationService):
|
||||
""" Implements notification service for Pushbullet. """
|
||||
|
||||
def __init__(self, api_key):
|
||||
from pushbullet import PushBullet
|
||||
|
||||
self.pushbullet = PushBullet(api_key)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
""" Send a message to a user. """
|
||||
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
self.pushbullet.push_note(title, message)
|
@ -30,7 +30,6 @@ def setup(hass, config):
|
||||
entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring
|
||||
for pname, pstring in config[DOMAIN].items()}
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_process_states(time):
|
||||
""" Check ps for currently running processes and update states. """
|
||||
with os.popen(PS_STRING, 'r') as psfile:
|
||||
|
89
homeassistant/components/sensor/__init__.py
Normal file
89
homeassistant/components/sensor/__init__.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""
|
||||
homeassistant.components.sensor
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Component to interface with various sensors that can be monitored.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
STATE_OPEN)
|
||||
from homeassistant.helpers import (
|
||||
platform_devices_from_config)
|
||||
from homeassistant.components import group, discovery, wink
|
||||
|
||||
DOMAIN = 'sensor'
|
||||
DEPENDENCIES = []
|
||||
|
||||
GROUP_NAME_ALL_SENSORS = 'all_sensors'
|
||||
ENTITY_ID_ALL_SENSORS = group.ENTITY_ID_FORMAT.format(
|
||||
GROUP_NAME_ALL_SENSORS)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=1)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
wink.DISCOVER_SENSORS: 'wink',
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
""" Returns if the sensor is open based on the statemachine. """
|
||||
entity_id = entity_id or ENTITY_ID_ALL_SENSORS
|
||||
|
||||
return hass.states.is_state(entity_id, STATE_OPEN)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Track states and offer events for sensors. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
sensors = platform_devices_from_config(
|
||||
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def update_sensor_states(now):
|
||||
""" Update states of all sensors. """
|
||||
if sensors:
|
||||
logger.info("Updating sensor states")
|
||||
|
||||
for sensor in sensors.values():
|
||||
sensor.update_ha_state(hass, True)
|
||||
|
||||
update_sensor_states(None)
|
||||
|
||||
# Track all sensors in a group
|
||||
sensor_group = group.Group(
|
||||
hass, GROUP_NAME_ALL_SENSORS, sensors.keys(), False)
|
||||
|
||||
def sensor_discovered(service, info):
|
||||
""" Called when a sensor is discovered. """
|
||||
platform = get_component("{}.{}".format(
|
||||
DOMAIN, DISCOVERY_PLATFORMS[service]))
|
||||
|
||||
discovered = platform.devices_discovered(hass, config, info)
|
||||
|
||||
for sensor in discovered:
|
||||
if sensor is not None and sensor not in sensors.values():
|
||||
sensor.entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(sensor.name)),
|
||||
sensors.keys())
|
||||
|
||||
sensors[sensor.entity_id] = sensor
|
||||
|
||||
sensor.update_ha_state(hass)
|
||||
|
||||
sensor_group.update_tracked_entity_ids(sensors.keys())
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), sensor_discovered)
|
||||
|
||||
# Fire every 3 seconds
|
||||
hass.track_time_change(update_sensor_states, seconds=range(0, 60, 3))
|
||||
|
||||
return True
|
33
homeassistant/components/sensor/wink.py
Normal file
33
homeassistant/components/sensor/wink.py
Normal file
@ -0,0 +1,33 @@
|
||||
""" Support for Wink sensors. """
|
||||
import logging
|
||||
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.wink.pywink as pywink
|
||||
|
||||
from homeassistant.components.wink import WinkSensorDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
|
||||
def get_devices(hass, config):
|
||||
""" Find and return Wink sensors. """
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token - "
|
||||
"get one at https://winkbearertoken.appspot.com/")
|
||||
return []
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
return get_sensors()
|
||||
|
||||
|
||||
def devices_discovered(hass, config, info):
|
||||
""" Called when a device is discovered. """
|
||||
return get_sensors()
|
||||
|
||||
|
||||
def get_sensors():
|
||||
""" Returns the Wink sensors. """
|
||||
return [WinkSensorDevice(sensor) for sensor in pywink.get_sensors()]
|
@ -34,6 +34,7 @@ def setup(hass, config):
|
||||
|
||||
device_tracker = loader.get_component('device_tracker')
|
||||
light = loader.get_component('light')
|
||||
notify = loader.get_component('notify')
|
||||
|
||||
light_ids = []
|
||||
|
||||
@ -67,13 +68,16 @@ def setup(hass, config):
|
||||
hass, unknown_light_id,
|
||||
flash=light.FLASH_LONG, rgb_color=[255, 0, 0])
|
||||
|
||||
# Send a message to the user
|
||||
notify.send_message(
|
||||
hass, "The lights just got turned on while no one was home.")
|
||||
|
||||
# Setup services to test the effect
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TEST_KNOWN_ALARM, lambda call: known_alarm())
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TEST_UNKNOWN_ALARM, lambda call: unknown_alarm())
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def unknown_alarm_if_lights_on(entity_id, old_state, new_state):
|
||||
""" Called when a light has been turned on. """
|
||||
if not device_tracker.is_on(hass):
|
||||
@ -83,7 +87,6 @@ def setup(hass, config):
|
||||
light.ENTITY_ID_ALL_LIGHTS,
|
||||
unknown_alarm_if_lights_on, STATE_OFF, STATE_ON)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def ring_known_alarm(entity_id, old_state, new_state):
|
||||
""" Called when a known person comes home. """
|
||||
if light.is_on(hass, known_light_id):
|
||||
|
@ -6,19 +6,19 @@ Component to interface with various switches that can be controlled remotely.
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import (
|
||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers import (
|
||||
extract_entity_ids, platform_devices_from_config)
|
||||
from homeassistant.components import group
|
||||
generate_entity_id, extract_entity_ids, platform_devices_from_config)
|
||||
from homeassistant.components import group, discovery, wink
|
||||
|
||||
DOMAIN = 'switch'
|
||||
DEPENDENCIES = []
|
||||
|
||||
GROUP_NAME_ALL_SWITCHES = 'all_switches'
|
||||
ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format(
|
||||
GROUP_NAME_ALL_SWITCHES)
|
||||
GROUP_NAME_ALL_SWITCHES = 'all switches'
|
||||
ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format('all_switches')
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
@ -27,6 +27,12 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
# Maps discovered services to their platforms
|
||||
DISCOVERY_PLATFORMS = {
|
||||
discovery.services.BELKIN_WEMO: 'wemo',
|
||||
wink.DISCOVER_SWITCHES: 'wink',
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -55,52 +61,54 @@ def setup(hass, config):
|
||||
""" Track states and offer events for switches. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
switches = platform_devices_from_config(config, DOMAIN, hass, logger)
|
||||
switches = platform_devices_from_config(
|
||||
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
|
||||
|
||||
if not switches:
|
||||
return False
|
||||
|
||||
# Setup a dict mapping entity IDs to devices
|
||||
ent_to_switch = {}
|
||||
|
||||
no_name_count = 1
|
||||
|
||||
for switch in switches:
|
||||
name = switch.get_name()
|
||||
|
||||
if name is None:
|
||||
name = "Switch #{}".format(no_name_count)
|
||||
no_name_count += 1
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
ent_to_switch.keys())
|
||||
|
||||
switch.entity_id = entity_id
|
||||
ent_to_switch[entity_id] = switch
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def update_states(now):
|
||||
""" Update states of all switches. """
|
||||
if switches:
|
||||
logger.info("Updating switch states")
|
||||
|
||||
logger.info("Updating switch states")
|
||||
|
||||
for switch in switches:
|
||||
switch.update_ha_state(hass)
|
||||
for switch in switches.values():
|
||||
switch.update_ha_state(hass, True)
|
||||
|
||||
update_states(None)
|
||||
|
||||
# Track all switches in a group
|
||||
switch_group = group.Group(
|
||||
hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False)
|
||||
|
||||
def switch_discovered(service, info):
|
||||
""" Called when a switch is discovered. """
|
||||
platform = get_component("{}.{}".format(
|
||||
DOMAIN, DISCOVERY_PLATFORMS[service]))
|
||||
|
||||
discovered = platform.devices_discovered(hass, config, info)
|
||||
|
||||
for switch in discovered:
|
||||
if switch is not None and switch not in switches.values():
|
||||
switch.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, switch.name, switches.keys())
|
||||
|
||||
switches[switch.entity_id] = switch
|
||||
|
||||
switch.update_ha_state(hass)
|
||||
|
||||
switch_group.update_tracked_entity_ids(switches.keys())
|
||||
|
||||
discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), switch_discovered)
|
||||
|
||||
def handle_switch_service(service):
|
||||
""" Handles calls to the switch services. """
|
||||
devices = [ent_to_switch[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in ent_to_switch]
|
||||
target_switches = [switches[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in switches]
|
||||
|
||||
if not devices:
|
||||
devices = switches
|
||||
if not target_switches:
|
||||
target_switches = switches.values()
|
||||
|
||||
for switch in devices:
|
||||
for switch in target_switches:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
switch.turn_on()
|
||||
else:
|
||||
@ -108,10 +116,6 @@ def setup(hass, config):
|
||||
|
||||
switch.update_ha_state(hass)
|
||||
|
||||
# Track all switches in a group
|
||||
group.setup_group(hass, GROUP_NAME_ALL_SWITCHES,
|
||||
ent_to_switch.keys(), False)
|
||||
|
||||
# Update state every 30 seconds
|
||||
hass.track_time_change(update_states, second=[0, 30])
|
||||
|
||||
|
@ -11,7 +11,6 @@ except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_devices(hass, config):
|
||||
""" Find and return Tellstick switches. """
|
||||
try:
|
||||
@ -36,20 +35,17 @@ class TellstickSwitch(ToggleDevice):
|
||||
self.tellstick = tellstick
|
||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||
|
||||
def get_name(self):
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.tellstick.name
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.tellstick.turn_on()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
last_command = self.tellstick.last_sent_command(
|
||||
@ -57,6 +53,10 @@ class TellstickSwitch(ToggleDevice):
|
||||
|
||||
return last_command == tc_constants.TELLSTICK_TURNON
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return self.state_attr
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.tellstick.turn_on()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turns the switch off. """
|
||||
self.tellstick.turn_off()
|
||||
|
@ -2,38 +2,55 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers import ToggleDevice
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOSTS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||
from homeassistant.components.switch import (
|
||||
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_devices(hass, config):
|
||||
""" Find and return WeMo switches. """
|
||||
|
||||
pywemo, _ = get_pywemo()
|
||||
|
||||
if pywemo is None:
|
||||
return []
|
||||
|
||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||
switches = pywemo.discover_devices()
|
||||
|
||||
# Filter out the switches and wrap in WemoSwitch object
|
||||
return [WemoSwitch(switch) for switch in switches
|
||||
if isinstance(switch, pywemo.Switch)]
|
||||
|
||||
|
||||
def devices_discovered(hass, config, info):
|
||||
""" Called when a device is discovered. """
|
||||
_, discovery = get_pywemo()
|
||||
|
||||
if discovery is None:
|
||||
return []
|
||||
|
||||
device = discovery.device_from_description(info)
|
||||
|
||||
return [] if device is None else [WemoSwitch(device)]
|
||||
|
||||
|
||||
def get_pywemo():
|
||||
""" Tries to import PyWemo. """
|
||||
try:
|
||||
# Pylint does not play nice if not every folders has an __init__.py
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.pywemo.pywemo as pywemo
|
||||
import homeassistant.external.pywemo.pywemo.discovery as discovery
|
||||
|
||||
return pywemo, discovery
|
||||
|
||||
except ImportError:
|
||||
logging.getLogger(__name__).exception((
|
||||
"Failed to import pywemo. "
|
||||
"Did you maybe not run `git submodule init` "
|
||||
"and `git submodule update`?"))
|
||||
|
||||
return []
|
||||
|
||||
if CONF_HOSTS in config:
|
||||
switches = (pywemo.device_from_host(host) for host
|
||||
in config[CONF_HOSTS].split(","))
|
||||
|
||||
else:
|
||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||
switches = pywemo.discover_devices()
|
||||
|
||||
# Filter out the switches and wrap in WemoSwitch object
|
||||
return [WemoSwitch(switch) for switch in switches
|
||||
if isinstance(switch, pywemo.Switch)]
|
||||
return None, None
|
||||
|
||||
|
||||
class WemoSwitch(ToggleDevice):
|
||||
@ -41,23 +58,18 @@ class WemoSwitch(ToggleDevice):
|
||||
def __init__(self, wemo):
|
||||
self.wemo = wemo
|
||||
|
||||
def get_name(self):
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this WeMo switch """
|
||||
return "{}.{}".format(self.__class__, self.wemo.serialnumber)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the switch if any. """
|
||||
return self.wemo.name
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wemo.off()
|
||||
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self.wemo.get_state(True)
|
||||
|
||||
def get_state_attributes(self):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
if self.wemo.model.startswith('Belkin Insight'):
|
||||
cur_info = self.wemo.insight_params
|
||||
@ -69,3 +81,20 @@ class WemoSwitch(ToggleDevice):
|
||||
}
|
||||
else:
|
||||
return {ATTR_FRIENDLY_NAME: self.wemo.name}
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if switch is on. """
|
||||
return self.wemo.get_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wemo.on()
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wemo.off()
|
||||
|
||||
def update(self):
|
||||
""" Update Wemo state. """
|
||||
self.wemo.get_state(True)
|
||||
|
33
homeassistant/components/switch/wink.py
Normal file
33
homeassistant/components/switch/wink.py
Normal file
@ -0,0 +1,33 @@
|
||||
""" Support for WeMo switchces. """
|
||||
import logging
|
||||
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.wink.pywink as pywink
|
||||
|
||||
from homeassistant.components.wink import WinkToggleDevice
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
|
||||
|
||||
def get_devices(hass, config):
|
||||
""" Find and return Wink switches. """
|
||||
token = config.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if token is None:
|
||||
logging.getLogger(__name__).error(
|
||||
"Missing wink access_token - "
|
||||
"get one at https://winkbearertoken.appspot.com/")
|
||||
return []
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
return get_switches()
|
||||
|
||||
|
||||
def devices_discovered(hass, config, info):
|
||||
""" Called when a device is discovered. """
|
||||
return get_switches()
|
||||
|
||||
|
||||
def get_switches():
|
||||
""" Returns the Wink switches. """
|
||||
return [WinkToggleDevice(switch) for switch in pywink.get_switches()]
|
@ -129,7 +129,6 @@ def setup(hass, config):
|
||||
sensor.has_value(datatype):
|
||||
update_sensor_value_state(sensor_name, sensor.value(datatype))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def update_sensors_state(time):
|
||||
""" Update the state of all sensors """
|
||||
for sensor in sensors:
|
||||
|
193
homeassistant/components/thermostat/__init__.py
Normal file
193
homeassistant/components/thermostat/__init__.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""
|
||||
homeassistant.components.thermostat
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to interact with thermostats.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.helpers import (
|
||||
extract_entity_ids, platform_devices_from_config)
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import Device
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_ON, STATE_OFF)
|
||||
|
||||
DOMAIN = "thermostat"
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
SERVICE_TURN_AWAY_MODE_ON = "turn_away_mode_on"
|
||||
SERVICE_TURN_AWAY_MODE_OFF = "turn_away_mode_off"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
|
||||
ATTR_CURRENT_TEMPERATURE = "current_temperature"
|
||||
ATTR_AWAY_MODE = "away_mode"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def turn_away_mode_on(hass, entity_id=None):
|
||||
""" Turn all or specified thermostat away mode on. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_ON, data)
|
||||
|
||||
|
||||
def turn_away_mode_off(hass, entity_id=None):
|
||||
""" Turn all or specified thermostat away mode off. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, data)
|
||||
|
||||
|
||||
def set_temperature(hass, temperature, entity_id=None):
|
||||
""" Set new target temperature. """
|
||||
data = {ATTR_TEMPERATURE: temperature}
|
||||
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Setup thermostats. """
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
thermostats = platform_devices_from_config(
|
||||
config, DOMAIN, hass, ENTITY_ID_FORMAT, _LOGGER)
|
||||
|
||||
if not thermostats:
|
||||
return False
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def update_state(now):
|
||||
""" Update thermostat state. """
|
||||
logger.info("Updating nest state")
|
||||
|
||||
for thermostat in thermostats.values():
|
||||
thermostat.update_ha_state(hass, True)
|
||||
|
||||
# Update state every minute
|
||||
hass.track_time_change(update_state, second=[0])
|
||||
update_state(None)
|
||||
|
||||
def thermostat_service(service):
|
||||
""" Handles calls to the services. """
|
||||
# Convert the entity ids to valid light ids
|
||||
target_thermostats = [thermostats[entity_id] for entity_id
|
||||
in extract_entity_ids(hass, service)
|
||||
if entity_id in thermostats]
|
||||
|
||||
if not target_thermostats:
|
||||
target_thermostats = thermostats.values()
|
||||
|
||||
if service.service == SERVICE_TURN_AWAY_MODE_ON:
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.turn_away_mode_on()
|
||||
|
||||
elif service.service == SERVICE_TURN_AWAY_MODE_OFF:
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.turn_away_mode_off()
|
||||
|
||||
elif service.service == SERVICE_SET_TEMPERATURE:
|
||||
temperature = util.convert(
|
||||
service.data.get(ATTR_TEMPERATURE), float)
|
||||
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.nest.set_temperature(temperature)
|
||||
|
||||
for thermostat in target_thermostats:
|
||||
thermostat.update_ha_state(hass, True)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TURN_AWAY_MODE_OFF, thermostat_service)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TURN_AWAY_MODE_ON, thermostat_service)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ThermostatDevice(Device):
|
||||
""" Represents a thermostat within Home Assistant. """
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the current state. """
|
||||
return self.target_temperature
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Returns the unit of measurement. """
|
||||
return ""
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
data = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: self.unit_of_measurement,
|
||||
ATTR_CURRENT_TEMPERATURE: self.current_temperature
|
||||
}
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
|
||||
if is_away is not None:
|
||||
data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
|
||||
|
||||
device_attr = self.device_state_attributes
|
||||
|
||||
if device_attr is not None:
|
||||
data.update(device_attr)
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
""" Returns the current temperature. """
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""
|
||||
Returns if away mode is on.
|
||||
Return None if no away mode available.
|
||||
"""
|
||||
return None
|
||||
|
||||
def set_temperate(self, temperature):
|
||||
""" Set new target temperature. """
|
||||
pass
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away mode on. """
|
||||
pass
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
""" Turns away mode off. """
|
||||
pass
|
96
homeassistant/components/thermostat/nest.py
Normal file
96
homeassistant/components/thermostat/nest.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""
|
||||
Adds support for Nest thermostats.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
|
||||
|
||||
|
||||
def get_devices(hass, config):
|
||||
""" Gets Nest thermostats. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is None or password is None:
|
||||
logger.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return []
|
||||
|
||||
try:
|
||||
import nest
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Error while importing dependency nest. "
|
||||
"Did you maybe not install the python-nest dependency?")
|
||||
|
||||
return []
|
||||
|
||||
napi = nest.Nest(username, password)
|
||||
|
||||
return [
|
||||
NestThermostat(structure, device)
|
||||
for structure in napi.structures
|
||||
for device in structure.devices]
|
||||
|
||||
|
||||
class NestThermostat(ThermostatDevice):
|
||||
""" Represents a Nest thermostat within Home Assistant. """
|
||||
|
||||
def __init__(self, structure, device):
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the nest, if any. """
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
""" Returns the unit of measurement. """
|
||||
return TEMP_CELCIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
""" Returns device specific state attributes. """
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
"fan": self.device.fan,
|
||||
"mode": self.device.mode
|
||||
}
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
""" Returns the current temperature. """
|
||||
return round(self.device.temperature, 1)
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
return round(self.device.target, 1)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
""" Returns if away mode is on. """
|
||||
return self.structure.away
|
||||
|
||||
def set_temperature(self, temperature):
|
||||
""" Set new target temperature """
|
||||
self.device.target = temperature
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
""" Turns away on. """
|
||||
self.structure.away = True
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
""" Turns away off. """
|
||||
self.structure.away = False
|
||||
|
||||
def update(self):
|
||||
""" Python-nest has its own mechanism for staying up to date. """
|
||||
pass
|
132
homeassistant/components/wink.py
Normal file
132
homeassistant/components/wink.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""
|
||||
Connects to a Wink hub and loads relevant components to control its devices.
|
||||
"""
|
||||
import logging
|
||||
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import homeassistant.external.wink.pywink as pywink
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import validate_config, ToggleDevice, Device
|
||||
from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED, CONF_ACCESS_TOKEN,
|
||||
STATE_OPEN, STATE_CLOSED,
|
||||
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
||||
|
||||
DOMAIN = "wink"
|
||||
DEPENDENCIES = []
|
||||
|
||||
DISCOVER_LIGHTS = "wink.lights"
|
||||
DISCOVER_SWITCHES = "wink.switches"
|
||||
DISCOVER_SENSORS = "wink.sensors"
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
""" Sets up the Wink component. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not validate_config(config, {DOMAIN: [CONF_ACCESS_TOKEN]}, logger):
|
||||
return False
|
||||
|
||||
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
|
||||
|
||||
# Load components for the devices in the Wink that we support
|
||||
for component_name, func_exists, discovery_type in (
|
||||
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
|
||||
('switch', pywink.get_switches, DISCOVER_SWITCHES),
|
||||
('sensor', pywink.get_sensors, DISCOVER_SENSORS)):
|
||||
|
||||
if func_exists():
|
||||
component = get_component(component_name)
|
||||
|
||||
# Ensure component is loaded
|
||||
if component.DOMAIN not in hass.components:
|
||||
bootstrap.setup_component(hass, component.DOMAIN, config)
|
||||
|
||||
# Fire discovery event
|
||||
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
|
||||
ATTR_SERVICE: discovery_type,
|
||||
ATTR_DISCOVERED: {}
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class WinkSensorDevice(Device):
|
||||
""" represents a wink sensor within home assistant. """
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state. """
|
||||
return STATE_OPEN if self.is_open else STATE_CLOSED
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this wink switch """
|
||||
return "{}.{}".format(self.__class__, self.wink.deviceId())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the sensor if any. """
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return {
|
||||
ATTR_FRIENDLY_NAME: self.wink.name()
|
||||
}
|
||||
|
||||
def update(self):
|
||||
""" Update state of the sensor. """
|
||||
self.wink.updateState()
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
""" True if door is open. """
|
||||
return self.wink.state()
|
||||
|
||||
|
||||
class WinkToggleDevice(ToggleDevice):
|
||||
""" represents a WeMo switch within home assistant. """
|
||||
|
||||
def __init__(self, wink):
|
||||
self.wink = wink
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns the id of this WeMo switch """
|
||||
return "{}.{}".format(self.__class__, self.wink.deviceId())
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the light if any. """
|
||||
return self.wink.name()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if light is on. """
|
||||
return self.wink.state()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
return {
|
||||
ATTR_FRIENDLY_NAME: self.wink.name()
|
||||
}
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turns the switch on. """
|
||||
self.wink.setState(True)
|
||||
|
||||
def turn_off(self):
|
||||
""" Turns the switch off. """
|
||||
self.wink.setState(False)
|
||||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.wait_till_desired_reached()
|
@ -2,6 +2,9 @@
|
||||
# Can be used to specify a catch all when registering state or event listeners.
|
||||
MATCH_ALL = '*'
|
||||
|
||||
# If no name is specified
|
||||
DEVICE_DEFAULT_NAME = "Unnamed Device"
|
||||
|
||||
# #### CONFIG ####
|
||||
CONF_LATITUDE = "latitude"
|
||||
CONF_LONGITUDE = "longitude"
|
||||
@ -14,6 +17,8 @@ CONF_HOST = "host"
|
||||
CONF_HOSTS = "hosts"
|
||||
CONF_USERNAME = "username"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_API_KEY = "api_key"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
|
||||
# #### EVENTS ####
|
||||
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
||||
@ -22,12 +27,16 @@ EVENT_STATE_CHANGED = "state_changed"
|
||||
EVENT_TIME_CHANGED = "time_changed"
|
||||
EVENT_CALL_SERVICE = "call_service"
|
||||
EVENT_SERVICE_EXECUTED = "service_executed"
|
||||
EVENT_PLATFORM_DISCOVERED = "platform_discovered"
|
||||
|
||||
# #### STATES ####
|
||||
STATE_ON = 'on'
|
||||
STATE_OFF = 'off'
|
||||
STATE_HOME = 'home'
|
||||
STATE_NOT_HOME = 'not_home'
|
||||
STATE_UNKNOWN = "unknown"
|
||||
STATE_OPEN = 'open'
|
||||
STATE_CLOSED = 'closed'
|
||||
|
||||
# #### STATE AND EVENT ATTRIBUTES ####
|
||||
# Contains current time for a TIME_CHANGED event
|
||||
@ -52,6 +61,16 @@ ATTR_ENTITY_PICTURE = "entity_picture"
|
||||
# The unit of measurement if applicable
|
||||
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"
|
||||
|
||||
# Temperature attribute
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
# #### MISC ####
|
||||
TEMP_CELCIUS = "°C"
|
||||
TEMP_FAHRENHEIT = "°F"
|
||||
|
||||
# Contains the information that is discovered
|
||||
ATTR_DISCOVERED = "discovered"
|
||||
|
||||
# #### SERVICES ####
|
||||
SERVICE_HOMEASSISTANT_STOP = "stop"
|
||||
|
||||
|
1
homeassistant/external/netdisco
vendored
Submodule
1
homeassistant/external/netdisco
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 68877783cb989b874cbcaec5f388a8a4345891a6
|
1
homeassistant/external/noop
vendored
Submodule
1
homeassistant/external/noop
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 45fae73c1f44342010fa07f3ed8909bf2819a508
|
2
homeassistant/external/pynetgear
vendored
2
homeassistant/external/pynetgear
vendored
@ -1 +1 @@
|
||||
Subproject commit 991fbc9c42abe3e8bcce50fbf549d9fceefe1e29
|
||||
Subproject commit e946ecf7926b9b2adaa1e3127a9738201a1b1fc7
|
2
homeassistant/external/pywemo
vendored
2
homeassistant/external/pywemo
vendored
@ -1 +1 @@
|
||||
Subproject commit b86d410cd67ea1e3a60355aa23d17fe6761cb8c5
|
||||
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4
|
408
homeassistant/external/wink/pywink.py
vendored
Normal file
408
homeassistant/external/wink/pywink.py
vendored
Normal file
@ -0,0 +1,408 @@
|
||||
__author__ = 'JOHNMCL'
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
baseUrl = "https://winkapi.quirky.com"
|
||||
|
||||
headers = {}
|
||||
|
||||
|
||||
class wink_sensor_pod(object):
|
||||
""" represents a wink.py sensor
|
||||
json_obj holds the json stat at init (and if there is a refresh it's updated
|
||||
it's the native format for this objects methods
|
||||
and looks like so:
|
||||
{
|
||||
"data": {
|
||||
"last_event": {
|
||||
"brightness_occurred_at": None,
|
||||
"loudness_occurred_at": None,
|
||||
"vibration_occurred_at": None
|
||||
},
|
||||
"model_name": "Tripper",
|
||||
"capabilities": {
|
||||
"sensor_types": [
|
||||
{
|
||||
"field": "opened",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "battery",
|
||||
"type": "percentage"
|
||||
}
|
||||
]
|
||||
},
|
||||
"manufacturer_device_model": "quirky_ge_tripper",
|
||||
"location": "",
|
||||
"radio_type": "zigbee",
|
||||
"manufacturer_device_id": None,
|
||||
"gang_id": None,
|
||||
"sensor_pod_id": "37614",
|
||||
"subscription": {
|
||||
},
|
||||
"units": {
|
||||
},
|
||||
"upc_id": "184",
|
||||
"hidden_at": None,
|
||||
"last_reading": {
|
||||
"battery_voltage_threshold_2": 0,
|
||||
"opened": False,
|
||||
"battery_alarm_mask": 0,
|
||||
"opened_updated_at": 1421697092.7347496,
|
||||
"battery_voltage_min_threshold_updated_at": 1421697092.7347229,
|
||||
"battery_voltage_min_threshold": 0,
|
||||
"connection": None,
|
||||
"battery_voltage": 25,
|
||||
"battery_voltage_threshold_1": 25,
|
||||
"connection_updated_at": None,
|
||||
"battery_voltage_threshold_3": 0,
|
||||
"battery_voltage_updated_at": 1421697092.7347066,
|
||||
"battery_voltage_threshold_1_updated_at": 1421697092.7347302,
|
||||
"battery_voltage_threshold_3_updated_at": 1421697092.7347434,
|
||||
"battery_voltage_threshold_2_updated_at": 1421697092.7347374,
|
||||
"battery": 1.0,
|
||||
"battery_updated_at": 1421697092.7347553,
|
||||
"battery_alarm_mask_updated_at": 1421697092.734716
|
||||
},
|
||||
"triggers": [
|
||||
],
|
||||
"name": "MasterBathroom",
|
||||
"lat_lng": [
|
||||
37.550773,
|
||||
-122.279182
|
||||
],
|
||||
"uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7",
|
||||
"locale": "en_us",
|
||||
"device_manufacturer": "quirky_ge",
|
||||
"created_at": 1421523277,
|
||||
"local_id": "2",
|
||||
"hub_id": "88264"
|
||||
},
|
||||
}
|
||||
|
||||
"""
|
||||
def __init__(self, aJSonObj, objectprefix="sensor_pods"):
|
||||
self.jsonState = aJSonObj
|
||||
self.objectprefix = objectprefix
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s %s" % (self.name(), self.deviceId(), self.state())
|
||||
|
||||
def __repr__(self):
|
||||
return "<Wink sensor %s %s %s>" % (self.name(), self.deviceId(), self.state())
|
||||
|
||||
@property
|
||||
def _last_reading(self):
|
||||
return self.jsonState.get('last_reading') or {}
|
||||
|
||||
def name(self):
|
||||
return self.jsonState.get('name', "Unknown Name")
|
||||
|
||||
def state(self):
|
||||
return self._last_reading.get('opened', False)
|
||||
|
||||
def deviceId(self):
|
||||
return self.jsonState.get('sensor_pod_id', self.name())
|
||||
|
||||
def refresh_state_at_hub(self):
|
||||
"""
|
||||
Tell hub to query latest status from device and upload to Wink.
|
||||
PS: Not sure if this even works..
|
||||
"""
|
||||
urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId())
|
||||
requests.get(urlString, headers=headers)
|
||||
|
||||
def updateState(self):
|
||||
""" Update state with latest info from Wink API. """
|
||||
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
|
||||
arequest = requests.get(urlString, headers=headers)
|
||||
self._updateStateFromResponse(arequest.json())
|
||||
|
||||
def _updateStateFromResponse(self, response_json):
|
||||
"""
|
||||
:param response_json: the json obj returned from query
|
||||
:return:
|
||||
"""
|
||||
self.jsonState = response_json.get('data')
|
||||
|
||||
class wink_binary_switch(object):
|
||||
""" represents a wink.py switch
|
||||
json_obj holds the json stat at init (and if there is a refresh it's updated
|
||||
it's the native format for this objects methods
|
||||
and looks like so:
|
||||
|
||||
{
|
||||
"data": {
|
||||
"binary_switch_id": "4153",
|
||||
"name": "Garage door indicator",
|
||||
"locale": "en_us",
|
||||
"units": {},
|
||||
"created_at": 1411614982,
|
||||
"hidden_at": null,
|
||||
"capabilities": {},
|
||||
"subscription": {},
|
||||
"triggers": [],
|
||||
"desired_state": {
|
||||
"powered": false
|
||||
},
|
||||
"manufacturer_device_model": "leviton_dzs15",
|
||||
"manufacturer_device_id": null,
|
||||
"device_manufacturer": "leviton",
|
||||
"model_name": "Switch",
|
||||
"upc_id": "94",
|
||||
"gang_id": null,
|
||||
"hub_id": "11780",
|
||||
"local_id": "9",
|
||||
"radio_type": "zwave",
|
||||
"last_reading": {
|
||||
"powered": false,
|
||||
"powered_updated_at": 1411614983.6153464,
|
||||
"powering_mode": null,
|
||||
"powering_mode_updated_at": null,
|
||||
"consumption": null,
|
||||
"consumption_updated_at": null,
|
||||
"cost": null,
|
||||
"cost_updated_at": null,
|
||||
"budget_percentage": null,
|
||||
"budget_percentage_updated_at": null,
|
||||
"budget_velocity": null,
|
||||
"budget_velocity_updated_at": null,
|
||||
"summation_delivered": null,
|
||||
"summation_delivered_updated_at": null,
|
||||
"sum_delivered_multiplier": null,
|
||||
"sum_delivered_multiplier_updated_at": null,
|
||||
"sum_delivered_divisor": null,
|
||||
"sum_delivered_divisor_updated_at": null,
|
||||
"sum_delivered_formatting": null,
|
||||
"sum_delivered_formatting_updated_at": null,
|
||||
"sum_unit_of_measure": null,
|
||||
"sum_unit_of_measure_updated_at": null,
|
||||
"desired_powered": false,
|
||||
"desired_powered_updated_at": 1417893563.7567682,
|
||||
"desired_powering_mode": null,
|
||||
"desired_powering_mode_updated_at": null
|
||||
},
|
||||
"current_budget": null,
|
||||
"lat_lng": [
|
||||
38.429996,
|
||||
-122.653721
|
||||
],
|
||||
"location": "",
|
||||
"order": 0
|
||||
},
|
||||
"errors": [],
|
||||
"pagination": {}
|
||||
}
|
||||
|
||||
"""
|
||||
def __init__(self, aJSonObj, objectprefix="binary_switches"):
|
||||
self.jsonState = aJSonObj
|
||||
self.objectprefix = objectprefix
|
||||
# Tuple (desired state, time)
|
||||
self._last_call = (0, None)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s %s" % (self.name(), self.deviceId(), self.state())
|
||||
|
||||
def __repr__(self):
|
||||
return "<Wink switch %s %s %s>" % (self.name(), self.deviceId(), self.state())
|
||||
|
||||
@property
|
||||
def _last_reading(self):
|
||||
return self.jsonState.get('last_reading') or {}
|
||||
|
||||
def name(self):
|
||||
return self.jsonState.get('name', "Unknown Name")
|
||||
|
||||
def state(self):
|
||||
# Optimistic approach to setState:
|
||||
# Within 15 seconds of a call to setState we assume it worked.
|
||||
if self._recent_state_set():
|
||||
return self._last_call[1]
|
||||
|
||||
return self._last_reading.get('powered', False)
|
||||
|
||||
def deviceId(self):
|
||||
return self.jsonState.get('binary_switch_id', self.name())
|
||||
|
||||
def setState(self, state):
|
||||
"""
|
||||
:param state: a boolean of true (on) or false ('off')
|
||||
:return: nothing
|
||||
"""
|
||||
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
|
||||
values = {"desired_state": {"powered": state}}
|
||||
arequest = requests.put(urlString, data=json.dumps(values), headers=headers)
|
||||
self._updateStateFromResponse(arequest.json())
|
||||
|
||||
self._last_call = (time.time(), state)
|
||||
|
||||
def refresh_state_at_hub(self):
|
||||
"""
|
||||
Tell hub to query latest status from device and upload to Wink.
|
||||
PS: Not sure if this even works..
|
||||
"""
|
||||
urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId())
|
||||
requests.get(urlString, headers=headers)
|
||||
|
||||
def updateState(self):
|
||||
""" Update state with latest info from Wink API. """
|
||||
urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId())
|
||||
arequest = requests.get(urlString, headers=headers)
|
||||
self._updateStateFromResponse(arequest.json())
|
||||
|
||||
def wait_till_desired_reached(self):
|
||||
""" Wait till desired state reached. Max 10s. """
|
||||
if self._recent_state_set():
|
||||
return
|
||||
|
||||
# self.refresh_state_at_hub()
|
||||
tries = 1
|
||||
|
||||
while True:
|
||||
self.updateState()
|
||||
last_read = self._last_reading
|
||||
|
||||
if last_read.get('desired_powered') == last_read.get('powered') \
|
||||
or tries == 5:
|
||||
break
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
tries += 1
|
||||
self.updateState()
|
||||
last_read = self._last_reading
|
||||
|
||||
def _updateStateFromResponse(self, response_json):
|
||||
"""
|
||||
:param response_json: the json obj returned from query
|
||||
:return:
|
||||
"""
|
||||
self.jsonState = response_json.get('data')
|
||||
|
||||
def _recent_state_set(self):
|
||||
return time.time() - self._last_call[0] < 15
|
||||
|
||||
|
||||
class wink_bulb(wink_binary_switch):
|
||||
""" represents a wink.py bulb
|
||||
json_obj holds the json stat at init (and if there is a refresh it's updated
|
||||
it's the native format for this objects methods
|
||||
and looks like so:
|
||||
|
||||
"light_bulb_id": "33990",
|
||||
"name": "downstaurs lamp",
|
||||
"locale": "en_us",
|
||||
"units":{},
|
||||
"created_at": 1410925804,
|
||||
"hidden_at": null,
|
||||
"capabilities":{},
|
||||
"subscription":{},
|
||||
"triggers":[],
|
||||
"desired_state":{"powered": true, "brightness": 1},
|
||||
"manufacturer_device_model": "lutron_p_pkg1_w_wh_d",
|
||||
"manufacturer_device_id": null,
|
||||
"device_manufacturer": "lutron",
|
||||
"model_name": "Caseta Wireless Dimmer & Pico",
|
||||
"upc_id": "3",
|
||||
"hub_id": "11780",
|
||||
"local_id": "8",
|
||||
"radio_type": "lutron",
|
||||
"linked_service_id": null,
|
||||
"last_reading":{
|
||||
"brightness": 1,
|
||||
"brightness_updated_at": 1417823487.490747,
|
||||
"connection": true,
|
||||
"connection_updated_at": 1417823487.4907365,
|
||||
"powered": true,
|
||||
"powered_updated_at": 1417823487.4907532,
|
||||
"desired_powered": true,
|
||||
"desired_powered_updated_at": 1417823485.054675,
|
||||
"desired_brightness": 1,
|
||||
"desired_brightness_updated_at": 1417409293.2591703
|
||||
},
|
||||
"lat_lng":[38.429962, -122.653715],
|
||||
"location": "",
|
||||
"order": 0
|
||||
|
||||
"""
|
||||
jsonState = {}
|
||||
|
||||
def __init__(self, ajsonobj):
|
||||
super().__init__(ajsonobj, "light_bulbs")
|
||||
|
||||
def deviceId(self):
|
||||
return self.jsonState.get('light_bulb_id', self.name())
|
||||
|
||||
def brightness(self):
|
||||
return self._last_reading.get('brightness')
|
||||
|
||||
def setState(self, state, brightness=None):
|
||||
"""
|
||||
:param state: a boolean of true (on) or false ('off')
|
||||
:return: nothing
|
||||
"""
|
||||
urlString = baseUrl + "/light_bulbs/%s" % self.deviceId()
|
||||
values = {
|
||||
"desired_state": {
|
||||
"powered": state
|
||||
}
|
||||
}
|
||||
|
||||
if brightness is not None:
|
||||
values["desired_state"]["brightness"] = brightness
|
||||
|
||||
urlString = baseUrl + "/light_bulbs/%s" % self.deviceId()
|
||||
arequest = requests.put(urlString, data=json.dumps(values), headers=headers)
|
||||
self._updateStateFromResponse(arequest.json())
|
||||
|
||||
self._last_call = (time.time(), state)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Wink Bulb %s %s %s>" % (
|
||||
self.name(), self.deviceId(), self.state())
|
||||
|
||||
|
||||
def get_devices(filter, constructor):
|
||||
arequestUrl = baseUrl + "/users/me/wink_devices"
|
||||
j = requests.get(arequestUrl, headers=headers).json()
|
||||
|
||||
items = j.get('data')
|
||||
|
||||
devices = []
|
||||
for item in items:
|
||||
id = item.get(filter)
|
||||
if (id is not None and item.get("hidden_at") is None):
|
||||
devices.append(constructor(item))
|
||||
|
||||
return devices
|
||||
|
||||
def get_bulbs():
|
||||
return get_devices('light_bulb_id', wink_bulb)
|
||||
|
||||
def get_switches():
|
||||
return get_devices('binary_switch_id', wink_binary_switch)
|
||||
|
||||
def get_sensors():
|
||||
return get_devices('sensor_pod_id', wink_sensor_pod)
|
||||
|
||||
def is_token_set():
|
||||
""" Returns if an auth token has been set. """
|
||||
return bool(headers)
|
||||
|
||||
|
||||
def set_bearer_token(token):
|
||||
global headers
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
sw = get_bulbs()
|
||||
lamp = sw[3]
|
||||
lamp.setState(False)
|
@ -7,7 +7,21 @@ from homeassistant import NoEntitySpecifiedError
|
||||
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE)
|
||||
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.util import ensure_unique_string, slugify
|
||||
|
||||
|
||||
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||
""" Generate a unique entity ID based on given entity IDs or used ids. """
|
||||
if current_ids is None:
|
||||
if hass is None:
|
||||
raise RuntimeError("Missing required parameter currentids or hass")
|
||||
|
||||
current_ids = hass.states.entity_ids()
|
||||
|
||||
return ensure_unique_string(
|
||||
entity_id_format.format(slugify(name)), current_ids)
|
||||
|
||||
|
||||
def extract_entity_ids(hass, service):
|
||||
@ -112,7 +126,9 @@ def config_per_platform(config, domain, logger):
|
||||
config_key = "{} {}".format(domain, found)
|
||||
|
||||
|
||||
def platform_devices_from_config(config, domain, hass, logger):
|
||||
def platform_devices_from_config(config, domain, hass,
|
||||
entity_id_format, logger):
|
||||
|
||||
""" Parses the config for specified domain.
|
||||
Loads different platforms and retrieve domains. """
|
||||
devices = []
|
||||
@ -143,33 +159,66 @@ def platform_devices_from_config(config, domain, hass, logger):
|
||||
|
||||
devices.extend(p_devices)
|
||||
|
||||
if len(devices) == 0:
|
||||
logger.error("No devices found for %s", domain)
|
||||
# Setup entity IDs for each device
|
||||
device_dict = {}
|
||||
|
||||
return devices
|
||||
no_name_count = 0
|
||||
|
||||
for device in devices:
|
||||
# Get the name or set to default if none given
|
||||
name = device.name or DEVICE_DEFAULT_NAME
|
||||
|
||||
if name == DEVICE_DEFAULT_NAME:
|
||||
no_name_count += 1
|
||||
name = "{} {}".format(domain, no_name_count)
|
||||
|
||||
entity_id = generate_entity_id(
|
||||
entity_id_format, name, device_dict.keys())
|
||||
|
||||
device.entity_id = entity_id
|
||||
device_dict[entity_id] = device
|
||||
|
||||
return device_dict
|
||||
|
||||
|
||||
class ToggleDevice(object):
|
||||
""" ABC for devices that can be turned on and off. """
|
||||
class Device(object):
|
||||
""" ABC for Home Assistant devices. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
entity_id = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
""" Returns a unique id. """
|
||||
return "{}.{}".format(self.__class__, id(self))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self.get_name()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self.get_state()
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
""" Returns the state attributes. """
|
||||
return {}
|
||||
|
||||
# DEPRECATION NOTICE:
|
||||
# Device is moving from getters to properties.
|
||||
# For now the new properties will call the old functions
|
||||
# This will be removed in the future.
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the device if any. """
|
||||
return None
|
||||
return DEVICE_DEFAULT_NAME
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
pass
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
pass
|
||||
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return False
|
||||
def get_state(self):
|
||||
""" Returns state of the device. """
|
||||
return "Unknown"
|
||||
|
||||
def get_state_attributes(self):
|
||||
""" Returns optional state attributes. """
|
||||
@ -186,12 +235,37 @@ class ToggleDevice(object):
|
||||
"""
|
||||
if self.entity_id is None:
|
||||
raise NoEntitySpecifiedError(
|
||||
"No entity specified for device {}".format(self.get_name()))
|
||||
"No entity specified for device {}".format(self.name))
|
||||
|
||||
if force_refresh:
|
||||
self.update()
|
||||
|
||||
state = STATE_ON if self.is_on() else STATE_OFF
|
||||
return hass.states.set(self.entity_id, self.state,
|
||||
self.state_attributes)
|
||||
|
||||
return hass.states.set(self.entity_id, state,
|
||||
self.get_state_attributes())
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, Device) and
|
||||
other.unique_id == self.unique_id)
|
||||
|
||||
|
||||
class ToggleDevice(Device):
|
||||
""" ABC for devices that can be turned on and off. """
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state. """
|
||||
return STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
return False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
pass
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
pass
|
||||
|
@ -112,17 +112,11 @@ class HomeAssistant(ha.HomeAssistant):
|
||||
self.states = StateMachine(self.bus, self.remote_api)
|
||||
|
||||
def start(self):
|
||||
# If there is no local API setup but we do want to connect with remote
|
||||
# We create a random password and set up a local api
|
||||
# Ensure a local API exists to connect with remote
|
||||
if self.local_api is None:
|
||||
import homeassistant.components.http as http
|
||||
import random
|
||||
|
||||
# pylint: disable=too-many-format-args
|
||||
random_password = '{:30}'.format(random.randrange(16**30))
|
||||
|
||||
http.setup(
|
||||
self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}})
|
||||
http.setup(self)
|
||||
|
||||
ha.Timer(self)
|
||||
|
||||
|
@ -12,6 +12,8 @@ from datetime import datetime, timedelta
|
||||
import re
|
||||
import enum
|
||||
import socket
|
||||
import random
|
||||
import string
|
||||
from functools import wraps
|
||||
|
||||
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
|
||||
@ -134,16 +136,16 @@ def convert(value, to_type, default=None):
|
||||
def ensure_unique_string(preferred_string, current_strings):
|
||||
""" Returns a string that is not present in current_strings.
|
||||
If preferred string exists will append _2, _3, .. """
|
||||
string = preferred_string
|
||||
test_string = preferred_string
|
||||
current_strings = list(current_strings)
|
||||
|
||||
tries = 1
|
||||
|
||||
while string in current_strings:
|
||||
while test_string in current_strings:
|
||||
tries += 1
|
||||
string = "{}_{}".format(preferred_string, tries)
|
||||
test_string = "{}_{}".format(preferred_string, tries)
|
||||
|
||||
return string
|
||||
return test_string
|
||||
|
||||
|
||||
# Taken from: http://stackoverflow.com/a/11735897
|
||||
@ -163,6 +165,15 @@ def get_local_ip():
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
|
||||
|
||||
# Taken from http://stackoverflow.com/a/23728630
|
||||
def get_random_string(length=10):
|
||||
""" Returns a random string with letters and digits. """
|
||||
generator = random.SystemRandom()
|
||||
source_chars = string.ascii_letters + string.digits
|
||||
|
||||
return ''.join(generator.choice(source_chars) for _ in range(length))
|
||||
|
||||
|
||||
class OrderedEnum(enum.Enum):
|
||||
""" Taken from Python 3.4.0 docs. """
|
||||
# pylint: disable=no-init, too-few-public-methods
|
||||
@ -289,19 +300,24 @@ class Throttle(object):
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wrapper that allows wrapped to be called only once per min_time.
|
||||
If we cannot acquire the lock, it is running so return None.
|
||||
"""
|
||||
with lock:
|
||||
last_call = wrapper.last_call
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = last_call is None or kwargs.pop('no_throttle', False)
|
||||
if lock.acquire(False):
|
||||
try:
|
||||
last_call = wrapper.last_call
|
||||
|
||||
if force or datetime.now() - last_call > self.min_time:
|
||||
# Check if method is never called or no_throttle is given
|
||||
force = not last_call or kwargs.pop('no_throttle', False)
|
||||
|
||||
result = method(*args, **kwargs)
|
||||
wrapper.last_call = datetime.now()
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
if force or datetime.now() - last_call > self.min_time:
|
||||
|
||||
result = method(*args, **kwargs)
|
||||
wrapper.last_call = datetime.now()
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
wrapper.last_call = None
|
||||
|
||||
|
8
pylintrc
8
pylintrc
@ -6,12 +6,16 @@ reports=no
|
||||
# locally-disabled - it spams too much
|
||||
# duplicate-code - unavoidable
|
||||
# cyclic-import - doesn't test if both import on load
|
||||
# file-ignored - we ignore a file to work around a pylint bug
|
||||
# abstract-class-little-used - Prevents from setting right foundation
|
||||
# abstract-class-not-used - is flaky, should not show up but does
|
||||
# unused-argument - generic callbacks and setup methods create a lot of warnings
|
||||
disable=
|
||||
locally-disabled,
|
||||
duplicate-code,
|
||||
cyclic-import,
|
||||
file-ignored
|
||||
abstract-class-little-used,
|
||||
abstract-class-not-used,
|
||||
unused-argument
|
||||
|
||||
[EXCEPTIONS]
|
||||
overgeneral-exceptions=Exception,HomeAssistantError
|
||||
|
@ -3,6 +3,9 @@ requests>=2.0
|
||||
|
||||
# optional, needed for specific components
|
||||
|
||||
# discovery
|
||||
zeroconf>=0.16.0
|
||||
|
||||
# sun
|
||||
pyephem>=3.7
|
||||
|
||||
@ -18,5 +21,11 @@ pyuserinput>=0.1.9
|
||||
# switch.tellstick, tellstick_sensor
|
||||
tellcore-py>=1.0.4
|
||||
|
||||
# namp_tracker plugin
|
||||
# device_tracker.nmap
|
||||
python-libnmap
|
||||
|
||||
# notify.pushbullet
|
||||
pushbullet.py>=0.7.1
|
||||
|
||||
# thermostat.nest
|
||||
python-nest>=2.1
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
pylint homeassistant
|
||||
flake8 homeassistant --exclude bower_components,external
|
||||
python3 -m unittest discover ha_test
|
@ -1,3 +1,8 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# To build the frontend, you need node, bower and vulcanize
|
||||
# npm install -g bower vulcanize
|
||||
|
||||
@ -19,4 +24,10 @@ mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/po
|
||||
# Generate the MD5 hash of the new frontend
|
||||
cd ..
|
||||
echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > frontend.py
|
||||
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py
|
||||
if [ $(command -v md5) ]; then
|
||||
echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py
|
||||
elif [ $(command -v md5sum) ]; then
|
||||
echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> frontend.py
|
||||
else
|
||||
echo 'Could not find a MD5 utility'
|
||||
fi
|
7
scripts/check_style
Executable file
7
scripts/check_style
Executable file
@ -0,0 +1,7 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
pylint homeassistant
|
||||
flake8 homeassistant --exclude bower_components,external
|
6
scripts/run_tests
Executable file
6
scripts/run_tests
Executable file
@ -0,0 +1,6 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
python3 -m unittest discover tests
|
8
scripts/update
Executable file
8
scripts/update
Executable file
@ -0,0 +1,8 @@
|
||||
# If current pwd is scripts, go 1 up.
|
||||
if [ ${PWD##*/} == "scripts" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
git pull --recurse-submodules=yes
|
||||
git submodule update --init --recursive
|
||||
python3 -m pip install -r requirements.txt
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user