mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Merge branch 'dev' into component-thermostat
* dev: Use tuples instead of lists internally Use properties instead of getters for Device class Upgrade pushbullet.py to 0.7.1 Prevent devices from being discovered twice Update netdisco to latest version Update netdisco to latest version Updated requirements.txt for the discovery component Automatic discovery and setting up of devices Ensure groups always have unique entity id Rename ha_test folder to tests Make group component more flexible Reorganized the main to be more modular Updated PyWemo to latest version Fix warnings from flake8 and pylint Check flags in ARP table for NUD_REACHABLE before assuming a device is online. Fixes #18. Pull in PyWemo bugfixes
This commit is contained in:
commit
cac1f56b2d
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -4,3 +4,6 @@
|
|||||||
[submodule "homeassistant/external/pywemo"]
|
[submodule "homeassistant/external/pywemo"]
|
||||||
path = homeassistant/external/pywemo
|
path = homeassistant/external/pywemo
|
||||||
url = https://github.com/balloob/pywemo.git
|
url = https://github.com/balloob/pywemo.git
|
||||||
|
[submodule "homeassistant/external/netdisco"]
|
||||||
|
path = homeassistant/external/netdisco
|
||||||
|
url = https://github.com/balloob/netdisco.git
|
||||||
|
@ -7,6 +7,6 @@ install:
|
|||||||
script:
|
script:
|
||||||
- flake8 homeassistant --exclude bower_components,external
|
- flake8 homeassistant --exclude bower_components,external
|
||||||
- pylint homeassistant
|
- pylint homeassistant
|
||||||
- coverage run --source=homeassistant -m unittest discover ha_test
|
- coverage run --source=homeassistant -m unittest discover tests
|
||||||
after_success:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
||||||
|
@ -51,6 +51,7 @@ class HomeAssistant(object):
|
|||||||
self.bus = EventBus(pool)
|
self.bus = EventBus(pool)
|
||||||
self.services = ServiceRegistry(self.bus, pool)
|
self.services = ServiceRegistry(self.bus, pool)
|
||||||
self.states = StateMachine(self.bus)
|
self.states = StateMachine(self.bus)
|
||||||
|
self.components = []
|
||||||
|
|
||||||
self.config_dir = os.path.join(os.getcwd(), 'config')
|
self.config_dir = os.path.join(os.getcwd(), 'config')
|
||||||
|
|
||||||
@ -222,7 +223,7 @@ def _process_match_param(parameter):
|
|||||||
elif isinstance(parameter, list):
|
elif isinstance(parameter, list):
|
||||||
return parameter
|
return parameter
|
||||||
else:
|
else:
|
||||||
return [parameter]
|
return (parameter,)
|
||||||
|
|
||||||
|
|
||||||
def _matcher(subject, pattern):
|
def _matcher(subject, pattern):
|
||||||
@ -588,7 +589,7 @@ class StateMachine(object):
|
|||||||
|
|
||||||
# Ensure it is a lowercase list with entity ids we want to match on
|
# Ensure it is a lowercase list with entity ids we want to match on
|
||||||
if isinstance(entity_ids, str):
|
if isinstance(entity_ids, str):
|
||||||
entity_ids = [entity_ids.lower()]
|
entity_ids = (entity_ids.lower(),)
|
||||||
else:
|
else:
|
||||||
entity_ids = [entity_id.lower() for entity_id in entity_ids]
|
entity_ids = [entity_id.lower() for entity_id in entity_ids]
|
||||||
|
|
||||||
|
@ -18,19 +18,8 @@ except ImportError:
|
|||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def validate_dependencies():
|
||||||
""" Starts Home Assistant. Will create demo config if no config found. """
|
""" Validate all dependencies that HA uses. """
|
||||||
|
|
||||||
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
|
|
||||||
import_fail = False
|
import_fail = False
|
||||||
|
|
||||||
for module in ['requests']:
|
for module in ['requests']:
|
||||||
@ -44,11 +33,14 @@ def main():
|
|||||||
if import_fail:
|
if import_fail:
|
||||||
print(("Install dependencies by running: "
|
print(("Install dependencies by running: "
|
||||||
"pip3 install -r requirements.txt"))
|
"pip3 install -r requirements.txt"))
|
||||||
exit()
|
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
|
# Test if configuration directory exists
|
||||||
config_dir = os.path.join(os.getcwd(), args.config)
|
|
||||||
|
|
||||||
if not os.path.isdir(config_dir):
|
if not os.path.isdir(config_dir):
|
||||||
print(('Fatal Error: Unable to find specified configuration '
|
print(('Fatal Error: Unable to find specified configuration '
|
||||||
'directory {} ').format(config_dir))
|
'directory {} ').format(config_dir))
|
||||||
@ -68,6 +60,27 @@ def main():
|
|||||||
'to write a default one to {}').format(config_path))
|
'to write a default one to {}').format(config_path))
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
|
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_dependencies()
|
||||||
|
|
||||||
|
config_dir = os.path.join(os.getcwd(), args.config)
|
||||||
|
|
||||||
|
config_path = ensure_config_path(config_dir)
|
||||||
|
|
||||||
hass = bootstrap.from_config_file(config_path)
|
hass = bootstrap.from_config_file(config_path)
|
||||||
hass.start()
|
hass.start()
|
||||||
hass.block_till_stopped()
|
hass.block_till_stopped()
|
||||||
|
@ -19,6 +19,33 @@ import homeassistant.loader as loader
|
|||||||
import homeassistant.components as core_components
|
import homeassistant.components as core_components
|
||||||
|
|
||||||
|
|
||||||
|
_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)
|
||||||
|
|
||||||
|
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
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
def from_config_dict(config, hass=None):
|
def from_config_dict(config, hass=None):
|
||||||
"""
|
"""
|
||||||
@ -29,8 +56,6 @@ def from_config_dict(config, hass=None):
|
|||||||
if hass is None:
|
if hass is None:
|
||||||
hass = homeassistant.HomeAssistant()
|
hass = homeassistant.HomeAssistant()
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
loader.prepare(hass)
|
loader.prepare(hass)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
@ -42,12 +67,12 @@ def from_config_dict(config, hass=None):
|
|||||||
if ' ' not in key and key != homeassistant.DOMAIN)
|
if ' ' not in key and key != homeassistant.DOMAIN)
|
||||||
|
|
||||||
if not core_components.setup(hass, config):
|
if not core_components.setup(hass, config):
|
||||||
logger.error(("Home Assistant core failed to initialize. "
|
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||||
"Further initialization aborted."))
|
"Further initialization aborted.")
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
logger.info("Home Assistant core initialized")
|
_LOGGER.info("Home Assistant core initialized")
|
||||||
|
|
||||||
# Setup the components
|
# Setup the components
|
||||||
|
|
||||||
@ -57,22 +82,11 @@ def from_config_dict(config, hass=None):
|
|||||||
add_worker = True
|
add_worker = True
|
||||||
|
|
||||||
for domain in loader.load_order_components(components):
|
for domain in loader.load_order_components(components):
|
||||||
component = loader.get_component(domain)
|
if setup_component(hass, domain, config):
|
||||||
|
add_worker = add_worker and domain != "group"
|
||||||
|
|
||||||
try:
|
if add_worker:
|
||||||
if component.setup(hass, config):
|
hass.pool.add_worker()
|
||||||
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)
|
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
@ -112,7 +126,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
|
|||||||
logging.getLogger('').addHandler(err_handler)
|
logging.getLogger('').addHandler(err_handler)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.getLogger(__name__).error(
|
_LOGGER.error(
|
||||||
"Unable to setup error log %s (access denied)", err_log_path)
|
"Unable to setup error log %s (access denied)", err_log_path)
|
||||||
|
|
||||||
# Read config
|
# Read config
|
||||||
|
@ -6,13 +6,19 @@ Provides functionality to interact with Chromecasts.
|
|||||||
"""
|
"""
|
||||||
import logging
|
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
|
import homeassistant.util as util
|
||||||
from homeassistant.helpers import extract_entity_ids
|
from homeassistant.helpers import extract_entity_ids
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
||||||
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
||||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
|
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
|
||||||
CONF_HOSTS)
|
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'chromecast'
|
DOMAIN = 'chromecast'
|
||||||
@ -105,12 +111,35 @@ def media_prev_track(hass, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals, too-many-branches
|
def setup_chromecast(casts, host):
|
||||||
def setup(hass, config):
|
""" Tries to convert host to Chromecast object and set it up. """
|
||||||
""" Listen for chromecast events. """
|
|
||||||
logger = logging.getLogger(__name__)
|
# Check if already setup
|
||||||
|
if any(cast.host == host for cast in casts.values()):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
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
|
import pychromecast
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.exception(("Failed to import pychromecast. "
|
logger.exception(("Failed to import pychromecast. "
|
||||||
@ -119,33 +148,24 @@ def setup(hass, config):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if CONF_HOSTS in config[DOMAIN]:
|
casts = {}
|
||||||
hosts = config[DOMAIN][CONF_HOSTS].split(",")
|
|
||||||
|
|
||||||
# If no hosts given, scan for chromecasts
|
# If discovery component not loaded, scan ourselves
|
||||||
else:
|
if discovery.DOMAIN not in hass.components:
|
||||||
logger.info("Scanning for Chromecasts")
|
logger.info("Scanning for Chromecasts")
|
||||||
hosts = pychromecast.discover_chromecasts()
|
hosts = pychromecast.discover_chromecasts()
|
||||||
|
|
||||||
casts = {}
|
for host in hosts:
|
||||||
|
setup_chromecast(casts, host)
|
||||||
|
|
||||||
for host in hosts:
|
# pylint: disable=unused-argument
|
||||||
try:
|
def chromecast_discovered(service, info):
|
||||||
cast = pychromecast.PyChromecast(host)
|
""" 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(
|
discovery.listen(
|
||||||
ENTITY_ID_FORMAT.format(
|
hass, discovery.services.GOOGLE_CAST, chromecast_discovered)
|
||||||
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
|
|
||||||
|
|
||||||
def update_chromecast_state(entity_id, chromecast):
|
def update_chromecast_state(entity_id, chromecast):
|
||||||
""" Retrieve state of Chromecast and update statemachine. """
|
""" Retrieve state of Chromecast and update statemachine. """
|
||||||
@ -194,10 +214,11 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def update_chromecast_states(time): # pylint: disable=unused-argument
|
def update_chromecast_states(time): # pylint: disable=unused-argument
|
||||||
""" Updates all chromecast states. """
|
""" Updates all chromecast states. """
|
||||||
logger.info("Updating Chromecast status")
|
if casts:
|
||||||
|
logger.info("Updating Chromecast status")
|
||||||
|
|
||||||
for entity_id, cast in casts.items():
|
for entity_id, cast in casts.items():
|
||||||
update_chromecast_state(entity_id, cast)
|
update_chromecast_state(entity_id, cast)
|
||||||
|
|
||||||
def _service_to_entities(service):
|
def _service_to_entities(service):
|
||||||
""" Helper method to get entities from service. """
|
""" Helper method to get entities from service. """
|
||||||
|
@ -111,19 +111,16 @@ class DeviceTracker(object):
|
|||||||
""" Triggers update of the device states. """
|
""" Triggers update of the device states. """
|
||||||
self.update_devices(now)
|
self.update_devices(now)
|
||||||
|
|
||||||
|
dev_group = group.Group(hass, GROUP_NAME_ALL_DEVICES)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def reload_known_devices_service(service):
|
def reload_known_devices_service(service):
|
||||||
""" Reload known devices file. """
|
""" Reload known devices file. """
|
||||||
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
|
|
||||||
|
|
||||||
self._read_known_devices_file()
|
self._read_known_devices_file()
|
||||||
|
|
||||||
self.update_devices(datetime.now())
|
self.update_devices(datetime.now())
|
||||||
|
|
||||||
if self.tracked:
|
dev_group.update_tracked_entity_ids(self.device_entity_ids)
|
||||||
group.setup_group(
|
|
||||||
self.hass, GROUP_NAME_ALL_DEVICES,
|
|
||||||
self.device_entity_ids, False)
|
|
||||||
|
|
||||||
reload_known_devices_service(None)
|
reload_known_devices_service(None)
|
||||||
|
|
||||||
|
@ -101,7 +101,12 @@ class LuciDeviceScanner(object):
|
|||||||
result = _req_json_rpc(url, 'net.arptable',
|
result = _req_json_rpc(url, 'net.arptable',
|
||||||
params={'auth': self.token})
|
params={'auth': self.token})
|
||||||
if result:
|
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
|
return True
|
||||||
|
|
||||||
|
88
homeassistant/components/discovery.py
Normal file
88
homeassistant/components/discovery.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Starts a service to scan in intervals for new devices.
|
||||||
|
|
||||||
|
Will emit EVENT_SERVICE_DISCOVERED whenever a new service has been discovered.
|
||||||
|
|
||||||
|
Knows which components handle certain types, will make sure they are
|
||||||
|
loaded before the EVENT_SERVICE_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, ATTR_SERVICE
|
||||||
|
|
||||||
|
DOMAIN = "discovery"
|
||||||
|
DEPENDENCIES = []
|
||||||
|
|
||||||
|
EVENT_SERVICE_DISCOVERED = "service_discovered"
|
||||||
|
|
||||||
|
ATTR_DISCOVERED = "discovered"
|
||||||
|
|
||||||
|
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 not isinstance(service, str):
|
||||||
|
service = (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_SERVICE_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:
|
||||||
|
if bootstrap.setup_component(hass, component, config):
|
||||||
|
hass.pool.add_worker()
|
||||||
|
|
||||||
|
hass.bus.fire(EVENT_SERVICE_DISCOVERED, {
|
||||||
|
ATTR_SERVICE: service,
|
||||||
|
ATTR_DISCOVERED: info
|
||||||
|
})
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
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.
|
Provides functionality to group devices that can be turned on or off.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.const import (
|
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"
|
DOMAIN = "group"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
|
|||||||
# List of ON/OFF state tuples for groupable states
|
# List of ON/OFF state tuples for groupable states
|
||||||
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||||
|
|
||||||
_GROUPS = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_group_on_off(state):
|
def _get_group_on_off(state):
|
||||||
""" Determine the group on/off states based on a state. """
|
""" Determine the group on/off states based on a state. """
|
||||||
@ -94,89 +91,98 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
|
|||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Sets up all groups found definded in the configuration. """
|
""" Sets up all groups found definded in the configuration. """
|
||||||
for name, entity_ids in config.get(DOMAIN, {}).items():
|
for name, entity_ids in config.get(DOMAIN, {}).items():
|
||||||
entity_ids = entity_ids.split(",")
|
setup_group(hass, name, entity_ids.split(","))
|
||||||
|
|
||||||
setup_group(hass, name, entity_ids)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def setup_group(hass, name, entity_ids, user_defined=True):
|
class Group(object):
|
||||||
""" Sets up a group state that is the combined state of
|
""" Tracks a group of entity ids. """
|
||||||
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
def __init__(self, hass, name, entity_ids=None, user_defined=True):
|
||||||
logger = logging.getLogger(__name__)
|
self.hass = hass
|
||||||
|
self.name = name
|
||||||
|
self.user_defined = user_defined
|
||||||
|
|
||||||
# In case an iterable is passed in
|
self.entity_id = util.ensure_unique_string(
|
||||||
entity_ids = list(entity_ids)
|
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||||
|
hass.states.entity_ids(DOMAIN))
|
||||||
|
|
||||||
if not entity_ids:
|
self.tracking = []
|
||||||
logger.error(
|
self.group_on, self.group_off = None, None
|
||||||
'Error setting up group %s: no entities passed in to track', name)
|
|
||||||
|
|
||||||
return False
|
if entity_ids is not None:
|
||||||
|
self.update_tracked_entity_ids(entity_ids)
|
||||||
# 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
|
|
||||||
else:
|
else:
|
||||||
group_ids.append(entity_id)
|
self.force_update()
|
||||||
|
|
||||||
# Keep track of the group state to init later on
|
@property
|
||||||
group_state = group_state or state.state == group_on
|
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
|
@property
|
||||||
if not group_ids:
|
def state_attr(self):
|
||||||
logger.error('Unable to find any entities to track for group %s', name)
|
""" 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:
|
self.force_update()
|
||||||
logger.warning(
|
|
||||||
'Warnings during setting up group %s: %s',
|
|
||||||
name, ", ".join(warnings))
|
|
||||||
|
|
||||||
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
self.start()
|
||||||
state = group_on if group_state else group_off
|
|
||||||
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
|
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)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def update_group_state(entity_id, old_state, new_state):
|
def _update_group_state(self, entity_id, old_state, new_state):
|
||||||
""" Updates the group state based on a state change by
|
""" Updates the group state based on a state change by
|
||||||
a tracked entity. """
|
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 = OFF and new_state = ON: set ON
|
||||||
# if cur_gr_state = ON and new_state = OFF: research
|
# if cur_gr_state = ON and new_state = OFF: research
|
||||||
@ -184,31 +190,21 @@ def setup_group(hass, name, entity_ids, user_defined=True):
|
|||||||
|
|
||||||
if cur_gr_state == group_off and new_state.state == group_on:
|
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
|
# Check if any of the other states is still on
|
||||||
if not any([hass.states.is_state(ent_id, group_on)
|
if not any(self.hass.states.is_state(ent_id, group_on)
|
||||||
for ent_id in group_ids
|
for ent_id in self.tracking if entity_id != ent_id):
|
||||||
if entity_id != ent_id]):
|
self.hass.states.set(
|
||||||
hass.states.set(group_entity_id, group_off, state_attr)
|
self.entity_id, group_off, self.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
|
|
||||||
|
|
||||||
|
|
||||||
def remove_group(hass, name):
|
def setup_group(hass, name, entity_ids, user_defined=True):
|
||||||
""" Remove a group and its state listener from Home Assistant. """
|
""" Sets up a group state that is the combined state of
|
||||||
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
||||||
|
|
||||||
if hass.states.get(group_entity_id) is not None:
|
return Group(hass, name, entity_ids, user_defined)
|
||||||
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))
|
|
||||||
|
@ -178,8 +178,7 @@ def setup(hass, config):
|
|||||||
update_lights_state(None)
|
update_lights_state(None)
|
||||||
|
|
||||||
# Track all lights in a group
|
# Track all lights in a group
|
||||||
group.setup_group(
|
group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
|
||||||
hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
|
|
||||||
|
|
||||||
def handle_light_service(service):
|
def handle_light_service(service):
|
||||||
""" Hande a turn light on or off service call. """
|
""" Hande a turn light on or off service call. """
|
||||||
|
@ -77,9 +77,36 @@ class HueLight(ToggleDevice):
|
|||||||
self.bridge = bridge
|
self.bridge = bridge
|
||||||
self.update_lights = update_lights
|
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. """
|
""" 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):
|
def turn_on(self, **kwargs):
|
||||||
""" Turn the specified or all lights on. """
|
""" Turn the specified or all lights on. """
|
||||||
@ -118,24 +145,6 @@ class HueLight(ToggleDevice):
|
|||||||
|
|
||||||
self.bridge.set_light(self.light_id, command)
|
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):
|
def update(self):
|
||||||
""" Synchronize state with bridge. """
|
""" Synchronize state with bridge. """
|
||||||
self.update_lights(no_throttle=True)
|
self.update_lights(no_throttle=True)
|
||||||
|
@ -50,7 +50,7 @@ def setup(hass, config):
|
|||||||
notify_service = notify_implementation.get_service(hass, config)
|
notify_service = notify_implementation.get_service(hass, config)
|
||||||
|
|
||||||
if notify_service is None:
|
if notify_service is None:
|
||||||
_LOGGER.error("Failed to initialize notificatino service %s",
|
_LOGGER.error("Failed to initialize notification service %s",
|
||||||
platform)
|
platform)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -22,16 +22,22 @@ def get_service(hass, config):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
from pushbullet import PushBullet # noqa
|
from pushbullet import PushBullet, InvalidKeyError # noqa
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Unable to import pushbullet. "
|
"Unable to import pushbullet. "
|
||||||
"Did you maybe not install the 'pushbullet' package?")
|
"Did you maybe not install the 'pushbullet.py' package?")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return PushBulletNotificationService(config[DOMAIN][CONF_API_KEY])
|
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
|
# pylint: disable=too-few-public-methods
|
||||||
|
@ -6,12 +6,13 @@ Component to interface with various switches that can be controlled remotely.
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.loader import get_component
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
extract_entity_ids, platform_devices_from_config)
|
extract_entity_ids, platform_devices_from_config)
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group, discovery
|
||||||
|
|
||||||
DOMAIN = 'switch'
|
DOMAIN = 'switch'
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
@ -27,6 +28,11 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
|||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
# Maps discovered services to their platforms
|
||||||
|
DISCOVERY = {
|
||||||
|
discovery.services.BELKIN_WEMO: 'wemo'
|
||||||
|
}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -58,21 +64,41 @@ def setup(hass, config):
|
|||||||
switches = platform_devices_from_config(
|
switches = platform_devices_from_config(
|
||||||
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
|
config, DOMAIN, hass, ENTITY_ID_FORMAT, logger)
|
||||||
|
|
||||||
if not switches:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
def update_states(now):
|
def update_states(now):
|
||||||
""" Update states of all switches. """
|
""" Update states of all switches. """
|
||||||
|
if switches:
|
||||||
|
logger.info("Updating switch states")
|
||||||
|
|
||||||
logger.info("Updating switch states")
|
for switch in switches.values():
|
||||||
|
switch.update_ha_state(hass)
|
||||||
for switch in switches.values():
|
|
||||||
switch.update_ha_state(hass)
|
|
||||||
|
|
||||||
update_states(None)
|
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[service]))
|
||||||
|
|
||||||
|
switch = platform.device_discovered(hass, config, info)
|
||||||
|
|
||||||
|
if switch is not None and switch not in switches.values():
|
||||||
|
switch.entity_id = util.ensure_unique_string(
|
||||||
|
ENTITY_ID_FORMAT.format(util.slugify(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.services.BELKIN_WEMO, switch_discovered)
|
||||||
|
|
||||||
def handle_switch_service(service):
|
def handle_switch_service(service):
|
||||||
""" Handles calls to the switch services. """
|
""" Handles calls to the switch services. """
|
||||||
target_switches = [switches[entity_id] for entity_id
|
target_switches = [switches[entity_id] for entity_id
|
||||||
@ -90,10 +116,6 @@ def setup(hass, config):
|
|||||||
|
|
||||||
switch.update_ha_state(hass)
|
switch.update_ha_state(hass)
|
||||||
|
|
||||||
# Track all switches in a group
|
|
||||||
group.setup_group(hass, GROUP_NAME_ALL_SWITCHES,
|
|
||||||
switches.keys(), False)
|
|
||||||
|
|
||||||
# Update state every 30 seconds
|
# Update state every 30 seconds
|
||||||
hass.track_time_change(update_states, second=[0, 30])
|
hass.track_time_change(update_states, second=[0, 30])
|
||||||
|
|
||||||
|
@ -36,10 +36,24 @@ class TellstickSwitch(ToggleDevice):
|
|||||||
self.tellstick = tellstick
|
self.tellstick = tellstick
|
||||||
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
|
||||||
|
|
||||||
def get_name(self):
|
@property
|
||||||
|
def name(self):
|
||||||
""" Returns the name of the switch if any. """
|
""" Returns the name of the switch if any. """
|
||||||
return self.tellstick.name
|
return self.tellstick.name
|
||||||
|
|
||||||
|
@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(
|
||||||
|
self.last_sent_command_mask)
|
||||||
|
|
||||||
|
return last_command == tc_constants.TELLSTICK_TURNON
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turns the switch on. """
|
""" Turns the switch on. """
|
||||||
@ -49,14 +63,3 @@ class TellstickSwitch(ToggleDevice):
|
|||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
""" Turns the switch off. """
|
""" Turns the switch off. """
|
||||||
self.tellstick.turn_off()
|
self.tellstick.turn_off()
|
||||||
|
|
||||||
def is_on(self):
|
|
||||||
""" True if switch is on. """
|
|
||||||
last_command = self.tellstick.last_sent_command(
|
|
||||||
self.last_sent_command_mask)
|
|
||||||
|
|
||||||
return last_command == tc_constants.TELLSTICK_TURNON
|
|
||||||
|
|
||||||
def get_state_attributes(self):
|
|
||||||
""" Returns optional state attributes. """
|
|
||||||
return self.state_attr
|
|
||||||
|
@ -11,16 +11,9 @@ from homeassistant.components.switch import (
|
|||||||
def get_devices(hass, config):
|
def get_devices(hass, config):
|
||||||
""" Find and return WeMo switches. """
|
""" Find and return WeMo switches. """
|
||||||
|
|
||||||
try:
|
pywemo, _ = get_pywemo()
|
||||||
# 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
|
|
||||||
except ImportError:
|
|
||||||
logging.getLogger(__name__).exception((
|
|
||||||
"Failed to import pywemo. "
|
|
||||||
"Did you maybe not run `git submodule init` "
|
|
||||||
"and `git submodule update`?"))
|
|
||||||
|
|
||||||
|
if pywemo is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||||
@ -31,28 +24,53 @@ def get_devices(hass, config):
|
|||||||
if isinstance(switch, pywemo.Switch)]
|
if isinstance(switch, pywemo.Switch)]
|
||||||
|
|
||||||
|
|
||||||
|
def device_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 None if device is None else WemoSwitch(device)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pywemo():
|
||||||
|
""" Tries to import PyWemo. """
|
||||||
|
try:
|
||||||
|
# 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 None, None
|
||||||
|
|
||||||
|
|
||||||
class WemoSwitch(ToggleDevice):
|
class WemoSwitch(ToggleDevice):
|
||||||
""" represents a WeMo switch within home assistant. """
|
""" represents a WeMo switch within home assistant. """
|
||||||
def __init__(self, wemo):
|
def __init__(self, wemo):
|
||||||
self.wemo = 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. """
|
""" Returns the name of the switch if any. """
|
||||||
return self.wemo.name
|
return self.wemo.name
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
@property
|
||||||
""" Turns the switch on. """
|
def state_attributes(self):
|
||||||
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):
|
|
||||||
""" Returns optional state attributes. """
|
""" Returns optional state attributes. """
|
||||||
if self.wemo.model.startswith('Belkin Insight'):
|
if self.wemo.model.startswith('Belkin Insight'):
|
||||||
cur_info = self.wemo.insight_params
|
cur_info = self.wemo.insight_params
|
||||||
@ -64,3 +82,16 @@ class WemoSwitch(ToggleDevice):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {ATTR_FRIENDLY_NAME: self.wemo.name}
|
return {ATTR_FRIENDLY_NAME: self.wemo.name}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if switch is on. """
|
||||||
|
return self.wemo.get_state(True)
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
""" Turns the switch on. """
|
||||||
|
self.wemo.on()
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
""" Turns the switch off. """
|
||||||
|
self.wemo.off()
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
# Can be used to specify a catch all when registering state or event listeners.
|
# Can be used to specify a catch all when registering state or event listeners.
|
||||||
MATCH_ALL = '*'
|
MATCH_ALL = '*'
|
||||||
|
|
||||||
|
# If no name is specified
|
||||||
|
DEVICE_DEFAULT_NAME = "Unnamed Device"
|
||||||
|
|
||||||
# #### CONFIG ####
|
# #### CONFIG ####
|
||||||
CONF_LATITUDE = "latitude"
|
CONF_LATITUDE = "latitude"
|
||||||
CONF_LONGITUDE = "longitude"
|
CONF_LONGITUDE = "longitude"
|
||||||
@ -29,6 +32,7 @@ STATE_ON = 'on'
|
|||||||
STATE_OFF = 'off'
|
STATE_OFF = 'off'
|
||||||
STATE_HOME = 'home'
|
STATE_HOME = 'home'
|
||||||
STATE_NOT_HOME = 'not_home'
|
STATE_NOT_HOME = 'not_home'
|
||||||
|
STATE_UNKNOWN = "unknown"
|
||||||
|
|
||||||
# #### STATE AND EVENT ATTRIBUTES ####
|
# #### STATE AND EVENT ATTRIBUTES ####
|
||||||
# Contains current time for a TIME_CHANGED event
|
# Contains current time for a TIME_CHANGED event
|
||||||
|
1
homeassistant/external/netdisco
vendored
Submodule
1
homeassistant/external/netdisco
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 27026d1f4a13afceb794a176f01cad9c1b37dc3b
|
2
homeassistant/external/pywemo
vendored
2
homeassistant/external/pywemo
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 687fc4930967da6b2aa258a0e6bb0c4026a1907c
|
Subproject commit 7f6c383ded75f1273cbca28e858b8a8c96da66d4
|
@ -7,7 +7,8 @@ from homeassistant import NoEntitySpecifiedError
|
|||||||
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
from homeassistant.const import (
|
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
|
from homeassistant.util import ensure_unique_string, slugify
|
||||||
|
|
||||||
|
|
||||||
@ -146,20 +147,17 @@ def platform_devices_from_config(config, domain, hass,
|
|||||||
|
|
||||||
devices.extend(p_devices)
|
devices.extend(p_devices)
|
||||||
|
|
||||||
if len(devices) == 0:
|
|
||||||
logger.error("No devices found for %s", domain)
|
|
||||||
|
|
||||||
# Setup entity IDs for each device
|
# Setup entity IDs for each device
|
||||||
no_name_count = 1
|
|
||||||
|
|
||||||
device_dict = {}
|
device_dict = {}
|
||||||
|
|
||||||
for device in devices:
|
no_name_count = 0
|
||||||
name = device.get_name()
|
|
||||||
|
|
||||||
if name is None:
|
for device in devices:
|
||||||
name = "{} #{}".format(domain, no_name_count)
|
name = device.name
|
||||||
|
|
||||||
|
if name == DEVICE_DEFAULT_NAME:
|
||||||
no_name_count += 1
|
no_name_count += 1
|
||||||
|
name = "{} #{}".format(domain, no_name_count)
|
||||||
|
|
||||||
entity_id = ensure_unique_string(
|
entity_id = ensure_unique_string(
|
||||||
entity_id_format.format(slugify(name)),
|
entity_id_format.format(slugify(name)),
|
||||||
@ -177,9 +175,34 @@ class Device(object):
|
|||||||
|
|
||||||
entity_id = None
|
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):
|
def get_name(self):
|
||||||
""" Returns the name of the device if any. """
|
""" Returns the name of the device if any. """
|
||||||
return None
|
return DEVICE_DEFAULT_NAME
|
||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
""" Returns state of the device. """
|
""" Returns state of the device. """
|
||||||
@ -200,22 +223,32 @@ class Device(object):
|
|||||||
"""
|
"""
|
||||||
if self.entity_id is None:
|
if self.entity_id is None:
|
||||||
raise NoEntitySpecifiedError(
|
raise NoEntitySpecifiedError(
|
||||||
"No entity specified for device {}".format(self.get_name()))
|
"No entity specified for device {}".format(self.name))
|
||||||
|
|
||||||
if force_refresh:
|
if force_refresh:
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
return hass.states.set(self.entity_id, self.get_state(),
|
return hass.states.set(self.entity_id, self.state,
|
||||||
self.get_state_attributes())
|
self.state_attributes)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, Device) and
|
||||||
|
other.unique_id == self.unique_id)
|
||||||
|
|
||||||
|
|
||||||
class ToggleDevice(Device):
|
class ToggleDevice(Device):
|
||||||
""" ABC for devices that can be turned on and off. """
|
""" ABC for devices that can be turned on and off. """
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
def get_state(self):
|
@property
|
||||||
|
def state(self):
|
||||||
""" Returns the state. """
|
""" Returns the state. """
|
||||||
return STATE_ON if self.is_on() else STATE_OFF
|
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):
|
def turn_on(self, **kwargs):
|
||||||
""" Turn the device on. """
|
""" Turn the device on. """
|
||||||
@ -224,7 +257,3 @@ class ToggleDevice(Device):
|
|||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
""" Turn the device off. """
|
""" Turn the device off. """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_on(self):
|
|
||||||
""" True if device is on. """
|
|
||||||
return False
|
|
||||||
|
@ -3,6 +3,9 @@ requests>=2.0
|
|||||||
|
|
||||||
# optional, needed for specific components
|
# optional, needed for specific components
|
||||||
|
|
||||||
|
# discovery
|
||||||
|
zeroconf>=0.16.0
|
||||||
|
|
||||||
# sun
|
# sun
|
||||||
pyephem>=3.7
|
pyephem>=3.7
|
||||||
|
|
||||||
@ -18,8 +21,8 @@ pyuserinput>=0.1.9
|
|||||||
# switch.tellstick, tellstick_sensor
|
# switch.tellstick, tellstick_sensor
|
||||||
tellcore-py>=1.0.4
|
tellcore-py>=1.0.4
|
||||||
|
|
||||||
# namp_tracker plugin
|
# device_tracker.nmap
|
||||||
python-libnmap
|
python-libnmap
|
||||||
|
|
||||||
# pushbullet
|
# notify.pushbullet
|
||||||
pushbullet.py>=0.5.0
|
pushbullet.py>=0.7.1
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
pylint homeassistant
|
pylint homeassistant
|
||||||
flake8 homeassistant --exclude bower_components,external
|
flake8 homeassistant --exclude bower_components,external
|
||||||
python3 -m unittest discover ha_test
|
python3 -m unittest discover tests
|
||||||
|
@ -7,7 +7,7 @@ Provides a mock switch platform.
|
|||||||
Call init before using it in your tests to ensure clean test data.
|
Call init before using it in your tests to ensure clean test data.
|
||||||
"""
|
"""
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
from ha_test.helpers import MockToggleDevice
|
from tests.helpers import MockToggleDevice
|
||||||
|
|
||||||
|
|
||||||
DEVICES = []
|
DEVICES = []
|
@ -7,7 +7,7 @@ Provides a mock switch platform.
|
|||||||
Call init before using it in your tests to ensure clean test data.
|
Call init before using it in your tests to ensure clean test data.
|
||||||
"""
|
"""
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
from ha_test.helpers import MockToggleDevice
|
from tests.helpers import MockToggleDevice
|
||||||
|
|
||||||
|
|
||||||
DEVICES = []
|
DEVICES = []
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.helper
|
tests.helper
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
Helper method for writing tests.
|
Helper method for writing tests.
|
||||||
@ -8,7 +8,7 @@ import os
|
|||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
from homeassistant.helpers import ToggleDevice
|
from homeassistant.helpers import ToggleDevice
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF
|
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
|
||||||
|
|
||||||
|
|
||||||
def get_test_home_assistant():
|
def get_test_home_assistant():
|
||||||
@ -45,29 +45,37 @@ class MockModule(object):
|
|||||||
class MockToggleDevice(ToggleDevice):
|
class MockToggleDevice(ToggleDevice):
|
||||||
""" Provides a mock toggle device. """
|
""" Provides a mock toggle device. """
|
||||||
def __init__(self, name, state):
|
def __init__(self, name, state):
|
||||||
self.name = name
|
self._name = name or DEVICE_DEFAULT_NAME
|
||||||
self.state = state
|
self._state = state
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|
||||||
def get_name(self):
|
@property
|
||||||
|
def name(self):
|
||||||
""" Returns the name of the device if any. """
|
""" Returns the name of the device if any. """
|
||||||
self.calls.append(('get_name', {}))
|
self.calls.append(('name', {}))
|
||||||
return self.name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the name of the device if any. """
|
||||||
|
self.calls.append(('state', {}))
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if device is on. """
|
||||||
|
self.calls.append(('is_on', {}))
|
||||||
|
return self._state == STATE_ON
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turn the device on. """
|
""" Turn the device on. """
|
||||||
self.calls.append(('turn_on', kwargs))
|
self.calls.append(('turn_on', kwargs))
|
||||||
self.state = STATE_ON
|
self._state = STATE_ON
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
""" Turn the device off. """
|
""" Turn the device off. """
|
||||||
self.calls.append(('turn_off', kwargs))
|
self.calls.append(('turn_off', kwargs))
|
||||||
self.state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
def is_on(self):
|
|
||||||
""" True if device is on. """
|
|
||||||
self.calls.append(('is_on', {}))
|
|
||||||
return self.state == STATE_ON
|
|
||||||
|
|
||||||
def last_call(self, method=None):
|
def last_call(self, method=None):
|
||||||
if method is None:
|
if method is None:
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_chromecast
|
tests.test_component_chromecast
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Chromecast component.
|
Tests Chromecast component.
|
||||||
@ -79,12 +79,3 @@ class TestChromecast(unittest.TestCase):
|
|||||||
self.assertEqual(service_name, call.service)
|
self.assertEqual(service_name, call.service)
|
||||||
self.assertEqual(self.test_entity,
|
self.assertEqual(self.test_entity,
|
||||||
call.data.get(ATTR_ENTITY_ID))
|
call.data.get(ATTR_ENTITY_ID))
|
||||||
|
|
||||||
def test_setup(self):
|
|
||||||
"""
|
|
||||||
Test Chromecast setup.
|
|
||||||
We do not have access to a Chromecast while testing so test errors.
|
|
||||||
In an ideal world we would create a mock pychromecast API..
|
|
||||||
"""
|
|
||||||
self.assertFalse(chromecast.setup(
|
|
||||||
self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}}))
|
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_core
|
tests.test_component_core
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests core compoments.
|
Tests core compoments.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_demo
|
tests.test_component_demo
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests demo component.
|
Tests demo component.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_group
|
tests.test_component_group
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests the group compoments.
|
Tests the group compoments.
|
||||||
@ -75,7 +75,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
|
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def test_device_tracker(self):
|
def test_writing_known_devices_file(self):
|
||||||
""" Test the device tracker class. """
|
""" Test the device tracker class. """
|
||||||
scanner = loader.get_component(
|
scanner = loader.get_component(
|
||||||
'device_tracker.test').get_scanner(None, None)
|
'device_tracker.test').get_scanner(None, None)
|
||||||
@ -117,7 +117,6 @@ class TestComponentsDeviceTracker(unittest.TestCase):
|
|||||||
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
|
dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL)
|
|
||||||
nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
|
nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
|
||||||
timedelta(seconds=1))
|
timedelta(seconds=1))
|
||||||
nowMinGone = nowAlmostMinGone + timedelta(seconds=2)
|
nowMinGone = nowAlmostMinGone + timedelta(seconds=2)
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_group
|
tests.test_component_group
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests the group compoments.
|
Tests the group compoments.
|
||||||
@ -9,7 +9,7 @@ import unittest
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
|
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN
|
||||||
import homeassistant.components.group as group
|
import homeassistant.components.group as group
|
||||||
|
|
||||||
|
|
||||||
@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase):
|
|||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_setup_group(self):
|
def test_setup_group_with_mixed_groupable_states(self):
|
||||||
""" Test setup_group method. """
|
""" Try to setup a group with mixed groupable states """
|
||||||
# Try to setup a group with mixed groupable states
|
|
||||||
self.hass.states.set('device_tracker.Paulus', STATE_HOME)
|
self.hass.states.set('device_tracker.Paulus', STATE_HOME)
|
||||||
self.assertTrue(group.setup_group(
|
group.setup_group(
|
||||||
self.hass, 'person_and_light',
|
self.hass, 'person_and_light',
|
||||||
['light.Bowl', 'device_tracker.Paulus']))
|
['light.Bowl', 'device_tracker.Paulus'])
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
self.hass.states.get(
|
self.hass.states.get(
|
||||||
group.ENTITY_ID_FORMAT.format('person_and_light')).state)
|
group.ENTITY_ID_FORMAT.format('person_and_light')).state)
|
||||||
|
|
||||||
# Try to setup a group with a non existing state
|
def test_setup_group_with_a_non_existing_state(self):
|
||||||
self.assertNotIn('non.existing', self.hass.states.entity_ids())
|
""" Try to setup a group with a non existing state """
|
||||||
self.assertTrue(group.setup_group(
|
grp = group.setup_group(
|
||||||
self.hass, 'light_and_nothing',
|
self.hass, 'light_and_nothing',
|
||||||
['light.Bowl', 'non.existing']))
|
['light.Bowl', 'non.existing'])
|
||||||
self.assertEqual(
|
|
||||||
STATE_ON,
|
|
||||||
self.hass.states.get(
|
|
||||||
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
|
|
||||||
|
|
||||||
# Try to setup a group with non groupable states
|
self.assertEqual(STATE_ON, grp.state.state)
|
||||||
|
|
||||||
|
def test_setup_group_with_non_groupable_states(self):
|
||||||
self.hass.states.set('cast.living_room', "Plex")
|
self.hass.states.set('cast.living_room', "Plex")
|
||||||
self.hass.states.set('cast.bedroom', "Netflix")
|
self.hass.states.set('cast.bedroom', "Netflix")
|
||||||
self.assertFalse(
|
|
||||||
group.setup_group(
|
|
||||||
self.hass, 'chromecasts',
|
|
||||||
['cast.living_room', 'cast.bedroom']))
|
|
||||||
|
|
||||||
# Try to setup an empty group
|
grp = group.setup_group(
|
||||||
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
|
self.hass, 'chromecasts',
|
||||||
|
['cast.living_room', 'cast.bedroom'])
|
||||||
|
|
||||||
|
self.assertEqual(STATE_UNKNOWN, grp.state.state)
|
||||||
|
|
||||||
|
def test_setup_empty_group(self):
|
||||||
|
""" Try to setup an empty group. """
|
||||||
|
grp = group.setup_group(self.hass, 'nothing', [])
|
||||||
|
|
||||||
|
self.assertEqual(STATE_UNKNOWN, grp.state.state)
|
||||||
|
|
||||||
def test_monitor_group(self):
|
def test_monitor_group(self):
|
||||||
""" Test if the group keeps track of states. """
|
""" Test if the group keeps track of states. """
|
||||||
@ -159,3 +162,10 @@ class TestComponentsGroup(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(STATE_ON, group_state.state)
|
self.assertEqual(STATE_ON, group_state.state)
|
||||||
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
|
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
|
||||||
|
|
||||||
|
def test_groups_get_unique_names(self):
|
||||||
|
""" Two groups with same name should both have a unique entity id. """
|
||||||
|
grp1 = group.Group(self.hass, 'Je suis Charlie')
|
||||||
|
grp2 = group.Group(self.hass, 'Je suis Charlie')
|
||||||
|
|
||||||
|
self.assertNotEqual(grp1.entity_id, grp2.entity_id)
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_http
|
tests.test_component_http
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Home Assistant HTTP component does what it should do.
|
Tests Home Assistant HTTP component does what it should do.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_switch
|
tests.test_component_switch
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests switch component.
|
Tests switch component.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_sun
|
tests.test_component_sun
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Sun component.
|
Tests Sun component.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_component_switch
|
tests.test_component_switch
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests switch component.
|
Tests switch component.
|
||||||
@ -7,7 +7,6 @@ Tests switch component.
|
|||||||
# pylint: disable=too-many-public-methods,protected-access
|
# pylint: disable=too-many-public-methods,protected-access
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import homeassistant as ha
|
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
||||||
import homeassistant.components.switch as switch
|
import homeassistant.components.switch as switch
|
||||||
@ -82,29 +81,12 @@ class TestSwitch(unittest.TestCase):
|
|||||||
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
|
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
|
||||||
self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id))
|
self.assertTrue(switch.is_on(self.hass, self.switch_3.entity_id))
|
||||||
|
|
||||||
def test_setup(self):
|
def test_setup_two_platforms(self):
|
||||||
# Bogus config
|
""" Test with bad config. """
|
||||||
self.assertFalse(switch.setup(self.hass, {}))
|
|
||||||
|
|
||||||
self.assertFalse(switch.setup(self.hass, {switch.DOMAIN: {}}))
|
|
||||||
|
|
||||||
# Test with non-existing component
|
|
||||||
self.assertFalse(switch.setup(
|
|
||||||
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
|
|
||||||
))
|
|
||||||
|
|
||||||
# Test if switch component returns 0 switches
|
# Test if switch component returns 0 switches
|
||||||
test_platform = loader.get_component('switch.test')
|
test_platform = loader.get_component('switch.test')
|
||||||
test_platform.init(True)
|
test_platform.init(True)
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
[], test_platform.get_switches(None, None))
|
|
||||||
|
|
||||||
self.assertFalse(switch.setup(
|
|
||||||
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
|
|
||||||
))
|
|
||||||
|
|
||||||
# Test if we can load 2 platforms
|
|
||||||
loader.set_component('switch.test2', test_platform)
|
loader.set_component('switch.test2', test_platform)
|
||||||
test_platform.init(False)
|
test_platform.init(False)
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_core
|
tests.test_core
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Provides tests to verify that Home Assistant core works.
|
Provides tests to verify that Home Assistant core works.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_helpers
|
tests.test_helpers
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests component helpers.
|
Tests component helpers.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_ha_test.test_loader
|
ha_tests.test_loader
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Provides tests to verify that we can load components.
|
Provides tests to verify that we can load components.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.remote
|
tests.remote
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Home Assistant remote methods and classes.
|
Tests Home Assistant remote methods and classes.
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
ha_test.test_util
|
tests.test_util
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Home Assistant util methods.
|
Tests Home Assistant util methods.
|
Loading…
x
Reference in New Issue
Block a user