Compare commits

...

10 Commits

Author SHA1 Message Date
Paulus Schoutsen
4750656f1a Bumped version to 0.81.0b1 2018-10-23 14:09:47 +02:00
Jaxom Nutt
011cc624b6 Bug fix for clicksend (#17713)
* Bug fix

Current version causes 500 error since it is sending an array of from numbers to ClickSend. Changing the from number to 'hass' identifies all messages as coming from Home Assistant making them more recognisable and removes the bug.

* Amendment

Changed it to use 'hass' as the default instead of defaulting to the recipient which is the array. Would have worked if users set their own name but users who were using the default were experiencing the issue.

* Added DEFAULT_SENDER variable
2018-10-23 14:09:41 +02:00
Anders Melchiorsen
2d9a964953 Update limitlessled to 1.1.3 (#17703) 2018-10-23 14:09:40 +02:00
Matt Snyder
87133a0e77 Update flux library version (#17677) 2018-10-23 14:09:40 +02:00
Nicko van Someren
fe8dec27a3 Fixed issue #16903 re exception with multiple simultanious writes (#17636)
Reworked tests/components/emulated_hue/test_init.py to not be
dependent on the specific internal implementation of util/jsonn.py
2018-10-23 14:09:39 +02:00
Bram Kragten
b5323cd894 Add lovelace websocket get and set card (#17600)
* Add ws get, set card

* lint+fix test

* Add test for set

* Added more tests, catch unsupported yaml constructors

Like !include will now give an error in the frontend.

* lint
2018-10-23 14:09:39 +02:00
Malte Franken
23316a8344 Geo location trigger added (#16967)
* zone trigger supports entity id pattern

* fixed lint error

* fixed test code

* initial version of new geo_location trigger

* revert to original

* simplified code and added tests

* refactored geo_location trigger to be based on a source defined by the entity

* amended test cases

* small refactorings
2018-10-23 14:09:38 +02:00
Paulus Schoutsen
86ecd7555a Update translations 2018-10-23 14:05:03 +02:00
Paulus Schoutsen
02f55b039c Update frontend to 20181023.0 2018-10-23 14:04:37 +02:00
Paulus Schoutsen
ebaf7f8c00 Bumped version to 0.81.0b0 2018-10-21 20:34:50 +02:00
24 changed files with 931 additions and 90 deletions

View File

@@ -0,0 +1,74 @@
"""
Offer geo location automation rules.
For more details about this automation trigger, please refer to the
documentation at
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger
"""
import voluptuous as vol
from homeassistant.components.geo_location import DOMAIN
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
from homeassistant.helpers import (
condition, config_validation as cv)
from homeassistant.helpers.config_validation import entity_domain
EVENT_ENTER = 'enter'
EVENT_LEAVE = 'leave'
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'geo_location',
vol.Required(CONF_SOURCE): cv.string,
vol.Required(CONF_ZONE): entity_domain('zone'),
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
def source_match(state, source):
"""Check if the state matches the provided source."""
return state and state.attributes.get('source') == source
async def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@callback
def state_change_listener(event):
"""Handle specific state changes."""
# Skip if the event is not a geo_location entity.
if not event.data.get('entity_id').startswith(DOMAIN):
return
# Skip if the event's source does not match the trigger's source.
from_state = event.data.get('old_state')
to_state = event.data.get('new_state')
if not source_match(from_state, source) \
and not source_match(to_state, source):
return
zone_state = hass.states.get(zone_entity_id)
from_match = condition.zone(hass, zone_state, from_state)
to_match = condition.zone(hass, zone_state, to_state)
# pylint: disable=too-many-boolean-expressions
if trigger_event == EVENT_ENTER and not from_match and to_match or \
trigger_event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action({
'trigger': {
'platform': 'geo_location',
'source': source,
'entity_id': event.data.get('entity_id'),
'from_state': from_state,
'to_state': to_state,
'zone': zone_state,
'event': trigger_event,
},
}, context=event.context))
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)

View File

@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20181021.0']
REQUIREMENTS = ['home-assistant-frontend==20181023.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@@ -0,0 +1,5 @@
{
"config": {
"title": "IFTT"
}
}

View File

@@ -18,7 +18,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
REQUIREMENTS = ['flux_led==0.21']
REQUIREMENTS = ['flux_led==0.22']
_LOGGER = logging.getLogger(__name__)

View File

@@ -20,7 +20,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_hs_to_RGB)
from homeassistant.helpers.restore_state import async_get_last_state
REQUIREMENTS = ['limitlessled==1.1.2']
REQUIREMENTS = ['limitlessled==1.1.3']
_LOGGER = logging.getLogger(__name__)

View File

@@ -2,9 +2,10 @@
import logging
import uuid
import os
from os import O_WRONLY, O_CREAT, O_TRUNC
from os import O_CREAT, O_TRUNC, O_WRONLY
from collections import OrderedDict
from typing import Union, List, Dict
from typing import Dict, List, Union
import voluptuous as vol
from homeassistant.components import websocket_api
@@ -14,21 +15,45 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'lovelace'
REQUIREMENTS = ['ruamel.yaml==0.15.72']
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'
WS_TYPE_GET_CARD = 'lovelace/config/card/get'
WS_TYPE_SET_CARD = 'lovelace/config/card/set'
SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI,
OLD_WS_TYPE_GET_LOVELACE_UI),
})
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_CARD,
vol.Required('card_id'): str,
vol.Optional('format', default='yaml'): str,
})
SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SET_CARD,
vol.Required('card_id'): str,
vol.Required('card_config'): vol.Any(str, Dict),
vol.Optional('format', default='yaml'): str,
})
class WriteError(HomeAssistantError):
"""Error writing the data."""
class CardNotFoundError(HomeAssistantError):
"""Card not found in data."""
class UnsupportedYamlError(HomeAssistantError):
"""Unsupported YAML."""
def save_yaml(fname: str, data: JSON_TYPE):
"""Save a YAML file."""
from ruamel.yaml import YAML
@@ -45,7 +70,7 @@ def save_yaml(fname: str, data: JSON_TYPE):
_LOGGER.error(str(exc))
raise HomeAssistantError(exc)
except OSError as exc:
_LOGGER.exception('Saving YAML file failed: %s', fname)
_LOGGER.exception('Saving YAML file %s failed: %s', fname, exc)
raise WriteError(exc)
finally:
if os.path.exists(tmp_fname):
@@ -57,18 +82,29 @@ def save_yaml(fname: str, data: JSON_TYPE):
_LOGGER.error("YAML replacement cleanup failed: %s", exc)
def _yaml_unsupported(loader, node):
raise UnsupportedYamlError(
'Unsupported YAML, you can not use {} in ui-lovelace.yaml'
.format(node.tag))
def load_yaml(fname: str) -> JSON_TYPE:
"""Load a YAML file."""
from ruamel.yaml import YAML
from ruamel.yaml.constructor import RoundTripConstructor
from ruamel.yaml.error import YAMLError
RoundTripConstructor.add_constructor(None, _yaml_unsupported)
yaml = YAML(typ='rt')
try:
with open(fname, encoding='utf-8') as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
return yaml.load(conf_file) or OrderedDict()
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
_LOGGER.error("YAML error in %s: %s", fname, exc)
raise HomeAssistantError(exc)
except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc)
@@ -76,21 +112,86 @@ def load_yaml(fname: str) -> JSON_TYPE:
def load_config(fname: str) -> JSON_TYPE:
"""Load a YAML file and adds id to card if not present."""
"""Load a YAML file and adds id to views and cards if not present."""
config = load_yaml(fname)
# Check if all cards have an ID or else add one
# Check if all views and cards have an id or else add one
updated = False
index = 0
for view in config.get('views', []):
if 'id' not in view:
updated = True
view.insert(0, 'id', index,
comment="Automatically created id")
for card in view.get('cards', []):
if 'id' not in card:
updated = True
card['id'] = uuid.uuid4().hex
card.move_to_end('id', last=False)
card.insert(0, 'id', uuid.uuid4().hex,
comment="Automatically created id")
index += 1
if updated:
save_yaml(fname, config)
return config
def object_to_yaml(data: JSON_TYPE) -> str:
"""Create yaml string from object."""
from ruamel.yaml import YAML
from ruamel.yaml.error import YAMLError
from ruamel.yaml.compat import StringIO
yaml = YAML(typ='rt')
yaml.indent(sequence=4, offset=2)
stream = StringIO()
try:
yaml.dump(data, stream)
return stream.getvalue()
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
raise HomeAssistantError(exc)
def yaml_to_object(data: str) -> JSON_TYPE:
"""Create object from yaml string."""
from ruamel.yaml import YAML
from ruamel.yaml.error import YAMLError
yaml = YAML(typ='rt')
try:
return yaml.load(data)
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
raise HomeAssistantError(exc)
def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE:
"""Load a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
return object_to_yaml(card)
return card
raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))
def set_card(fname: str, card_id: str, card_config: str, data_format: str)\
-> bool:
"""Save a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
card_config = yaml_to_object(card_config)
card.update(card_config)
save_yaml(fname, config)
return True
raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))
async def async_setup(hass, config):
"""Set up the Lovelace commands."""
# Backwards compat. Added in 0.80. Remove after 0.85
@@ -102,6 +203,14 @@ async def async_setup(hass, config):
WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
SCHEMA_GET_LOVELACE_UI)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_CARD, websocket_lovelace_get_card,
SCHEMA_GET_CARD)
hass.components.websocket_api.async_register_command(
WS_TYPE_SET_CARD, websocket_lovelace_set_card,
SCHEMA_SET_CARD)
return True
@@ -111,13 +220,15 @@ async def websocket_lovelace_config(hass, connection, msg):
error = None
try:
config = await hass.async_add_executor_job(
load_config, hass.config.path('ui-lovelace.yaml'))
load_config, hass.config.path(LOVELACE_CONFIG_FILE))
message = websocket_api.result_message(
msg['id'], config
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except HomeAssistantError as err:
error = 'load_error', str(err)
@@ -125,3 +236,59 @@ async def websocket_lovelace_config(hass, connection, msg):
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)
@websocket_api.async_response
async def websocket_lovelace_get_card(hass, connection, msg):
"""Send lovelace card config over websocket config."""
error = None
try:
card = await hass.async_add_executor_job(
get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'],
msg.get('format', 'yaml'))
message = websocket_api.result_message(
msg['id'], card
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except HomeAssistantError as err:
error = 'load_error', str(err)
if error is not None:
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)
@websocket_api.async_response
async def websocket_lovelace_set_card(hass, connection, msg):
"""Receive lovelace card config over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
set_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['card_config'], msg.get('format', 'yaml'))
message = websocket_api.result_message(
msg['id'], result
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except HomeAssistantError as err:
error = 'save_error', str(err)
if error is not None:
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)

View File

@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
"one_instance_allowed": "Only a single instance is necessary."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
},
"step": {
"user": {
"description": "Are you sure you want to set up Mailgun?",
"title": "Set up the Mailgun Webhook"
}
},
"title": "Mailgun"
}
}

View File

@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.",
"one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
},
"step": {
"user": {
"description": "S\u00e9cher fir Mailgun anzeriichten?",
"title": "Mailgun Webhook ariichten"
}
},
"title": "Mailgun"
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"hassio_confirm": {
"data": {
"discovery": "Ke\u015ffetmeyi etkinle\u015ftir"
}
}
}
}
}

View File

@@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
DEFAULT_SENDER = 'hass'
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
@@ -29,7 +31,7 @@ def validate_sender(config):
"""Set the optional sender name if sender name is not provided."""
if CONF_SENDER in config:
return config
config[CONF_SENDER] = config[CONF_RECIPIENT]
config[CONF_SENDER] = DEFAULT_SENDER
return config
@@ -61,7 +63,7 @@ class ClicksendNotificationService(BaseNotificationService):
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipients = config.get(CONF_RECIPIENT)
self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
self.sender = config.get(CONF_SENDER)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""

View File

@@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Account bestaat al",
"invalid_credentials": "Ongeldige gebruikersgegevens"
},
"step": {
"user": {
"data": {
"code": "Code (voor Home Assistant)",
"password": "Wachtwoord",
"username": "E-mailadres"
},
"title": "Vul uw gegevens in"
}
},
"title": "SimpliSafe"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parola",
"username": "E-posta adresi"
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"config": {
"error": {
"name_exists": "Bu ad zaten var",
"wrong_location": "Konum sadece \u0130sve\u00e7"
},
"step": {
"user": {
"data": {
"latitude": "Enlem",
"longitude": "Boylam"
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"user_privilege": "Gebruiker moet beheerder zijn"
},
"error": {
"faulty_credentials": "Foutieve gebruikersgegevens",
"service_unavailable": "Geen service beschikbaar"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Wachtwoord",
"port": "Poort",
"username": "Gebruikersnaam"
},
"title": "Stel de UniFi-controller in"
}
},
"title": "UniFi-controller"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parola",
"username": "Kullan\u0131c\u0131 ad\u0131"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"enable_sensors": "Trafik sens\u00f6rleri ekleyin"
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)"
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 81
PATCH_VERSION = '0.dev0'
PATCH_VERSION = '0b1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
@@ -129,6 +129,7 @@ CONF_SENSOR_TYPE = 'sensor_type'
CONF_SENSORS = 'sensors'
CONF_SHOW_ON_MAP = 'show_on_map'
CONF_SLAVE = 'slave'
CONF_SOURCE = 'source'
CONF_SSL = 'ssl'
CONF_STATE = 'state'
CONF_STATE_TEMPLATE = 'state_template'

View File

@@ -4,7 +4,7 @@ from typing import Union, List, Dict
import json
import os
from os import O_WRONLY, O_CREAT, O_TRUNC
import tempfile
from homeassistant.exceptions import HomeAssistantError
@@ -46,13 +46,17 @@ def save_json(filename: str, data: Union[List, Dict],
Returns True on success.
"""
tmp_filename = filename + "__TEMP__"
tmp_filename = ""
tmp_path = os.path.split(filename)[0]
try:
json_data = json.dumps(data, sort_keys=True, indent=4)
mode = 0o600 if private else 0o644
with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode),
'w', encoding='utf-8') as fdesc:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8',
dir=tmp_path, delete=False) as fdesc:
fdesc.write(json_data)
tmp_filename = fdesc.name
if not private:
os.chmod(tmp_filename, 0o644)
os.replace(tmp_filename, filename)
except TypeError as error:
_LOGGER.exception('Failed to serialize to JSON: %s',

View File

@@ -376,7 +376,7 @@ fitbit==0.3.0
fixerio==1.0.0a0
# homeassistant.components.light.flux_led
flux_led==0.21
flux_led==0.22
# homeassistant.components.sensor.foobot
foobot_async==0.3.1
@@ -466,7 +466,7 @@ hole==0.3.0
holidays==0.9.8
# homeassistant.components.frontend
home-assistant-frontend==20181021.0
home-assistant-frontend==20181023.0
# homeassistant.components.homekit_controller
# homekit==0.10
@@ -559,7 +559,7 @@ liffylights==0.9.4
lightify==1.0.6.1
# homeassistant.components.light.limitlessled
limitlessled==1.1.2
limitlessled==1.1.3
# homeassistant.components.linode
linode-api==4.1.9b1

View File

@@ -97,7 +97,7 @@ hdate==0.6.5
holidays==0.9.8
# homeassistant.components.frontend
home-assistant-frontend==20181021.0
home-assistant-frontend==20181023.0
# homeassistant.components.homematicip_cloud
homematicip==0.9.8

View File

@@ -0,0 +1,271 @@
"""The tests for the geo location trigger."""
import unittest
from homeassistant.components import automation, zone
from homeassistant.core import callback, Context
from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, mock_component
from tests.components.automation import common
class TestAutomationGeoLocation(unittest.TestCase):
"""Test the geo location trigger."""
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
mock_component(self.hass, 'group')
assert setup_component(self.hass, zone.DOMAIN, {
'zone': {
'name': 'test',
'latitude': 32.880837,
'longitude': -117.237561,
'radius': 250,
}
})
self.calls = []
@callback
def record_call(service):
"""Record calls."""
self.calls.append(service)
self.hass.services.register('test', 'automation', record_call)
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
def test_if_fires_on_zone_enter(self):
"""Test for firing on zone enter."""
context = Context()
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758,
'source': 'test_source'
})
self.hass.block_till_done()
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'enter',
},
'action': {
'service': 'test.automation',
'data_template': {
'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
'platform', 'entity_id',
'from_state.state', 'to_state.state',
'zone.name'))
},
}
}
})
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
}, context=context)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
assert self.calls[0].context is context
self.assertEqual(
'geo_location - geo_location.entity - hello - hello - test',
self.calls[0].data['some'])
# Set out of zone again so we can trigger call
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.block_till_done()
common.turn_off(self.hass)
self.hass.block_till_done()
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_for_enter_on_zone_leave(self):
"""Test for not firing on zone leave."""
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564,
'source': 'test_source'
})
self.hass.block_till_done()
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'enter',
},
'action': {
'service': 'test.automation',
}
}
})
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758
})
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_zone_leave(self):
"""Test for firing on zone leave."""
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564,
'source': 'test_source'
})
self.hass.block_till_done()
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'leave',
},
'action': {
'service': 'test.automation',
}
}
})
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758,
'source': 'test_source'
})
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_for_leave_on_zone_enter(self):
"""Test for not firing on zone enter."""
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.881011,
'longitude': -117.234758,
'source': 'test_source'
})
self.hass.block_till_done()
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'leave',
},
'action': {
'service': 'test.automation',
}
}
})
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564
})
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_zone_appear(self):
"""Test for firing if entity appears in zone."""
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'enter',
},
'action': {
'service': 'test.automation',
'data_template': {
'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
'platform', 'entity_id',
'from_state.state', 'to_state.state',
'zone.name'))
},
}
}
})
# Entity appears in zone without previously existing outside the zone.
context = Context()
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564,
'source': 'test_source'
}, context=context)
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
assert self.calls[0].context is context
self.assertEqual(
'geo_location - geo_location.entity - - hello - test',
self.calls[0].data['some'])
def test_if_fires_on_zone_disappear(self):
"""Test for firing if entity disappears from zone."""
self.hass.states.set('geo_location.entity', 'hello', {
'latitude': 32.880586,
'longitude': -117.237564,
'source': 'test_source'
})
self.hass.block_till_done()
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
'trigger': {
'platform': 'geo_location',
'source': 'test_source',
'zone': 'zone.test',
'event': 'leave',
},
'action': {
'service': 'test.automation',
'data_template': {
'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
'platform', 'entity_id',
'from_state.state', 'to_state.state',
'zone.name'))
},
}
}
})
# Entity disappears from zone without new coordinates outside the zone.
self.hass.states.async_remove('geo_location.entity')
self.hass.block_till_done()
self.assertEqual(1, len(self.calls))
self.assertEqual(
'geo_location - geo_location.entity - hello - - test',
self.calls[0].data['some'])

View File

@@ -1,7 +1,5 @@
"""Test the Emulated Hue component."""
import json
from unittest.mock import patch, Mock, mock_open, MagicMock
from unittest.mock import patch, Mock, MagicMock
from homeassistant.components.emulated_hue import Config
@@ -14,30 +12,30 @@ def test_config_google_home_entity_id_to_number():
'type': 'google_home'
})
mop = mock_open(read_data=json.dumps({'1': 'light.test2'}))
handle = mop()
with patch('homeassistant.components.emulated_hue.load_json',
return_value={'1': 'light.test2'}) as json_loader:
with patch('homeassistant.components.emulated_hue'
'.save_json') as json_saver:
number = conf.entity_id_to_number('light.test')
assert number == '2'
with patch('homeassistant.util.json.open', mop, create=True):
with patch('homeassistant.util.json.os.open', return_value=0):
with patch('homeassistant.util.json.os.replace'):
number = conf.entity_id_to_number('light.test')
assert number == '2'
assert handle.write.call_count == 1
assert json.loads(handle.write.mock_calls[0][1][0]) == {
'1': 'light.test2',
'2': 'light.test',
}
assert json_saver.mock_calls[0][1][1] == {
'1': 'light.test2', '2': 'light.test'
}
number = conf.entity_id_to_number('light.test')
assert number == '2'
assert handle.write.call_count == 1
assert json_saver.call_count == 1
assert json_loader.call_count == 1
number = conf.entity_id_to_number('light.test2')
assert number == '1'
assert handle.write.call_count == 1
number = conf.entity_id_to_number('light.test')
assert number == '2'
assert json_saver.call_count == 1
entity_id = conf.number_to_entity_id('1')
assert entity_id == 'light.test2'
number = conf.entity_id_to_number('light.test2')
assert number == '1'
assert json_saver.call_count == 1
entity_id = conf.number_to_entity_id('1')
assert entity_id == 'light.test2'
def test_config_google_home_entity_id_to_number_altered():
@@ -48,30 +46,30 @@ def test_config_google_home_entity_id_to_number_altered():
'type': 'google_home'
})
mop = mock_open(read_data=json.dumps({'21': 'light.test2'}))
handle = mop()
with patch('homeassistant.components.emulated_hue.load_json',
return_value={'21': 'light.test2'}) as json_loader:
with patch('homeassistant.components.emulated_hue'
'.save_json') as json_saver:
number = conf.entity_id_to_number('light.test')
assert number == '22'
assert json_saver.call_count == 1
assert json_loader.call_count == 1
with patch('homeassistant.util.json.open', mop, create=True):
with patch('homeassistant.util.json.os.open', return_value=0):
with patch('homeassistant.util.json.os.replace'):
number = conf.entity_id_to_number('light.test')
assert number == '22'
assert handle.write.call_count == 1
assert json.loads(handle.write.mock_calls[0][1][0]) == {
'21': 'light.test2',
'22': 'light.test',
}
assert json_saver.mock_calls[0][1][1] == {
'21': 'light.test2',
'22': 'light.test',
}
number = conf.entity_id_to_number('light.test')
assert number == '22'
assert handle.write.call_count == 1
number = conf.entity_id_to_number('light.test')
assert number == '22'
assert json_saver.call_count == 1
number = conf.entity_id_to_number('light.test2')
assert number == '21'
assert handle.write.call_count == 1
number = conf.entity_id_to_number('light.test2')
assert number == '21'
assert json_saver.call_count == 1
entity_id = conf.number_to_entity_id('21')
assert entity_id == 'light.test2'
entity_id = conf.number_to_entity_id('21')
assert entity_id == 'light.test2'
def test_config_google_home_entity_id_to_number_empty():
@@ -82,29 +80,29 @@ def test_config_google_home_entity_id_to_number_empty():
'type': 'google_home'
})
mop = mock_open(read_data='')
handle = mop()
with patch('homeassistant.components.emulated_hue.load_json',
return_value={}) as json_loader:
with patch('homeassistant.components.emulated_hue'
'.save_json') as json_saver:
number = conf.entity_id_to_number('light.test')
assert number == '1'
assert json_saver.call_count == 1
assert json_loader.call_count == 1
with patch('homeassistant.util.json.open', mop, create=True):
with patch('homeassistant.util.json.os.open', return_value=0):
with patch('homeassistant.util.json.os.replace'):
number = conf.entity_id_to_number('light.test')
assert number == '1'
assert handle.write.call_count == 1
assert json.loads(handle.write.mock_calls[0][1][0]) == {
'1': 'light.test',
}
assert json_saver.mock_calls[0][1][1] == {
'1': 'light.test',
}
number = conf.entity_id_to_number('light.test')
assert number == '1'
assert handle.write.call_count == 1
number = conf.entity_id_to_number('light.test')
assert number == '1'
assert json_saver.call_count == 1
number = conf.entity_id_to_number('light.test2')
assert number == '2'
assert handle.write.call_count == 2
number = conf.entity_id_to_number('light.test2')
assert number == '2'
assert json_saver.call_count == 2
entity_id = conf.number_to_entity_id('2')
assert entity_id == 'light.test2'
entity_id = conf.number_to_entity_id('2')
assert entity_id == 'light.test2'
def test_config_alexa_entity_id_to_number():

View File

@@ -9,7 +9,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.components.lovelace import (load_yaml,
save_yaml, load_config)
save_yaml, load_config,
UnsupportedYamlError)
TEST_YAML_A = """\
title: My Awesome Home
@@ -55,6 +56,8 @@ views:
# Title of the view. Will be used as the tooltip for tab icon
title: Second view
cards:
- id: test
type: entities
# Entities card will take a list of entities and show their state.
- type: entities
# Title of the entities card
@@ -79,6 +82,7 @@ TEST_YAML_B = """\
title: Home
views:
- title: Dashboard
id: dashboard
icon: mdi:home
cards:
- id: testid
@@ -102,6 +106,15 @@ views:
type: vertical-stack
"""
# Test unsupported YAML
TEST_UNSUP_YAML = """\
title: Home
views:
- title: Dashboard
icon: mdi:home
cards: !include cards.yaml
"""
class TestYAML(unittest.TestCase):
"""Test lovelace.yaml save and load."""
@@ -147,9 +160,11 @@ class TestYAML(unittest.TestCase):
"""Test if id is added."""
fname = self._path_for("test6")
with patch('homeassistant.components.lovelace.load_yaml',
return_value=self.yaml.load(TEST_YAML_A)):
return_value=self.yaml.load(TEST_YAML_A)), \
patch('homeassistant.components.lovelace.save_yaml'):
data = load_config(fname)
assert 'id' in data['views'][0]['cards'][0]
assert 'id' in data['views'][1]
def test_id_not_changed(self):
"""Test if id is not changed if already exists."""
@@ -256,7 +271,7 @@ async def test_lovelace_ui_not_found(hass, hass_ws_client):
async def test_lovelace_ui_load_err(hass, hass_ws_client):
"""Test lovelace_ui command cannot find file."""
"""Test lovelace_ui command load error."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
@@ -272,3 +287,156 @@ async def test_lovelace_ui_load_err(hass, hass_ws_client):
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'load_error'
async def test_lovelace_ui_load_json_err(hass, hass_ws_client):
"""Test lovelace_ui command load error."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
with patch('homeassistant.components.lovelace.load_config',
side_effect=UnsupportedYamlError):
await client.send_json({
'id': 5,
'type': 'lovelace/config',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'unsupported_error'
async def test_lovelace_get_card(hass, hass_ws_client):
"""Test get_card command."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/get',
'card_id': 'test',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success']
assert msg['result'] == 'id: test\ntype: entities\n'
async def test_lovelace_get_card_not_found(hass, hass_ws_client):
"""Test get_card command cannot find card."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/get',
'card_id': 'not_found',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'card_not_found'
async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client):
"""Test get_card command bad yaml."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
with patch('homeassistant.components.lovelace.load_yaml',
side_effect=HomeAssistantError):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/get',
'card_id': 'testid',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'load_error'
async def test_lovelace_set_card(hass, hass_ws_client):
"""Test set_card command."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)), \
patch('homeassistant.components.lovelace.save_yaml') \
as save_yaml_mock:
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'card_id': 'test',
'card_config': 'id: test\ntype: glance\n',
})
msg = await client.receive_json()
result = save_yaml_mock.call_args_list[0][0][1]
assert result.mlget(['views', 1, 'cards', 0, 'type'],
list_ok=True) == 'glance'
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success']
async def test_lovelace_set_card_not_found(hass, hass_ws_client):
"""Test set_card command cannot find card."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'card_id': 'not_found',
'card_config': 'id: test\ntype: glance\n',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'card_not_found'
async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client):
"""Test set_card command bad yaml."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)), \
patch('homeassistant.components.lovelace.yaml_to_object',
side_effect=HomeAssistantError):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'card_id': 'test',
'card_config': 'id: test\ntype: glance\n',
})
msg = await client.receive_json()
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'save_error'