From 39ec614e86a9cf14f41558c8f22cb971d302a817 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 22 Sep 2016 19:18:11 +0200 Subject: [PATCH 1/5] Add mysensors notify platform * Make add_devices optional in platform callback function. * Use new argument structure for all existing mysensors platforms. * Add notify platform. --- .../components/binary_sensor/mysensors.py | 2 +- homeassistant/components/climate/mysensors.py | 2 +- homeassistant/components/cover/mysensors.py | 2 +- homeassistant/components/light/mysensors.py | 2 +- homeassistant/components/mysensors.py | 16 +++-- homeassistant/components/notify/mysensors.py | 60 +++++++++++++++++++ homeassistant/components/sensor/mysensors.py | 2 +- homeassistant/components/switch/mysensors.py | 2 +- 8 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/notify/mysensors.py diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index e938f94645779f..6406dabd26f800 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, MySensorsBinarySensor)) + map_sv_types, devices, MySensorsBinarySensor, add_devices)) class MySensorsBinarySensor( diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 13a062a335e5f5..6c55b3b4451dc9 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, MySensorsHVAC)) + map_sv_types, devices, MySensorsHVAC, add_devices)) class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 7dd63a8c7451ad..a75ad36354b133 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): }) devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, MySensorsCover)) + map_sv_types, devices, MySensorsCover, add_devices)) class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice): diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 86d033cf4cef0e..9a018192f638b7 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): }) devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, device_class_map)) + map_sv_types, devices, device_class_map, add_devices)) class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index b6778760b1a8a6..537a5856f432c1 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -172,7 +172,7 @@ def gw_start(event): return True -def pf_callback_factory(map_sv_types, devices, add_devices, entity_class): +def pf_callback_factory(map_sv_types, devices, entity_class, add_devices=None): """Return a new callback for the platform.""" def mysensors_callback(gateway, node_id): """Callback for mysensors platform.""" @@ -187,7 +187,10 @@ def mysensors_callback(gateway, node_id): value_type not in map_sv_types[child.type]: continue if key in devices: - devices[key].update_ha_state(True) + if add_devices: + devices[key].update_ha_state(True) + else: + devices[key].update() continue name = '{} {} {}'.format( gateway.sensors[node_id].sketch_name, node_id, child.id) @@ -197,11 +200,12 @@ def mysensors_callback(gateway, node_id): device_class = entity_class devices[key] = device_class( gateway, node_id, child.id, name, value_type, child.type) - - _LOGGER.info('Adding new devices: %s', devices[key]) - add_devices([devices[key]]) - if key in devices: + if add_devices: + _LOGGER.info('Adding new devices: %s', devices[key]) + add_devices([devices[key]]) devices[key].update_ha_state(True) + else: + devices[key].update() return mysensors_callback diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py new file mode 100644 index 00000000000000..99bf3470d8cf6b --- /dev/null +++ b/homeassistant/components/notify/mysensors.py @@ -0,0 +1,60 @@ +""" +MySensors notification service. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/notify.mysensors/ +""" +from homeassistant.components import mysensors +from homeassistant.components.notify import (ATTR_TARGET, + BaseNotificationService) + +DEPENDENCIES = ['mysensors'] + + +def get_service(hass, config): + """Get the MySensors notification service.""" + platform_devices = [] + for gateway in mysensors.GATEWAYS.values(): + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + map_sv_types = { + pres.S_INFO: [set_req.V_TEXT], + } + devices = {} + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + map_sv_types, devices, MySensorsNotificationDevice)) + platform_devices.append(devices) + return MySensorsNotificationService(platform_devices) + + +class MySensorsNotificationDevice(mysensors.MySensorsDeviceEntity): + """Represent a MySensors Notification device.""" + + def send_msg(self, msg): + """Send a message.""" + for sub_msg in [msg[i:i + 25] for i in range(0, len(msg), 25)]: + # Max mysensors payload is 25 bytes. + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, sub_msg) + + +class MySensorsNotificationService(BaseNotificationService): + """Implement MySensors notification service.""" + + # pylint: disable=too-few-public-methods + + def __init__(self, platform_devices): + """Initialize the service.""" + self.platform_devices = platform_devices + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + target_devices = kwargs.get(ATTR_TARGET) + devices = [ + device for gw_devs in self.platform_devices + for device in gw_devs.values() + if target_devices is None or + device.name in target_devices] + + for device in devices: + device.send_msg(message) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 2742713cb24d1b..6c1f65606dac38 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -83,7 +83,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, MySensorsSensor)) + map_sv_types, devices, MySensorsSensor, add_devices)) class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 44bbfcb16d9b33..968166d4d65e21 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -89,7 +89,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = {} gateway.platform_callbacks.append(mysensors.pf_callback_factory( - map_sv_types, devices, add_devices, device_class_map)) + map_sv_types, devices, device_class_map, add_devices)) platform_devices.append(devices) def send_ir_code_service(service): From fe31c504756c484d96cb5186fba6b91e010c38fc Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Sat, 12 Nov 2016 19:51:00 +0100 Subject: [PATCH 2/5] Update mysensors gateway --- homeassistant/components/notify/mysensors.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 99bf3470d8cf6b..99bba0d71f95be 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -14,7 +14,11 @@ def get_service(hass, config): """Get the MySensors notification service.""" platform_devices = [] - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: pres = gateway.const.Presentation set_req = gateway.const.SetReq map_sv_types = { @@ -24,6 +28,7 @@ def get_service(hass, config): gateway.platform_callbacks.append(mysensors.pf_callback_factory( map_sv_types, devices, MySensorsNotificationDevice)) platform_devices.append(devices) + return MySensorsNotificationService(platform_devices) From 1553e8d0821a642f665de44c4faaf747dc7f286b Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Sun, 13 Nov 2016 15:44:54 +0100 Subject: [PATCH 3/5] Refactor notify setup * Enable discovery of notify platforms. * Use discovery to set up mysensors notify platform. --- homeassistant/components/mysensors.py | 9 ++++-- homeassistant/components/notify/__init__.py | 31 +++++++++++++++----- homeassistant/components/notify/mysensors.py | 2 -- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 537a5856f432c1..a90a910a3bbc8f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -9,10 +9,10 @@ import voluptuous as vol -from homeassistant.bootstrap import setup_component import homeassistant.helpers.config_validation as cv -from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC, - EVENT_HOMEASSISTANT_START, +from homeassistant.bootstrap import setup_component +from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_NAME, + CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) from homeassistant.helpers import discovery from homeassistant.loader import get_component @@ -169,6 +169,9 @@ def gw_start(event): 'cover']: discovery.load_platform(hass, component, DOMAIN, {}, config) + discovery.load_platform( + hass, 'notify', DOMAIN, {CONF_NAME: DOMAIN}, config) + return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index b9d595401b599c..4ac60fc9f08737 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file from homeassistant.const import CONF_NAME, CONF_PLATFORM -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, discovery from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -66,27 +66,31 @@ def send_message(hass, message, title=None, data=None): def setup(hass, config): """Setup the notify services.""" - success = False - descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) targets = {} - for platform, p_config in config_per_platform(config, DOMAIN): + def setup_notify_platform(platform, p_config=None, discovery_info=None): + """Set up a notify platform.""" + if p_config is None: + p_config = {} + if discovery_info is not None: + p_config = discovery_info + notify_implementation = bootstrap.prepare_setup_platform( hass, config, DOMAIN, platform) if notify_implementation is None: _LOGGER.error("Unknown notification service specified") - continue + return False notify_service = notify_implementation.get_service(hass, p_config) if notify_service is None: _LOGGER.error("Failed to initialize notification service %s", platform) - continue + return False def notify_message(notify_service, call): """Handle sending notification message service calls.""" @@ -127,9 +131,20 @@ def notify_message(notify_service, call): hass.services.register( DOMAIN, platform_name_slug, service_call_handler, descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA) - success = True - return success + return True + + for platform, p_config in config_per_platform(config, DOMAIN): + if not setup_notify_platform(platform, p_config): + return False + + def platform_discovered(platform, info): + """Callback to load a platform.""" + setup_notify_platform(platform, discovery_info=info) + + discovery.listen_platform(hass, DOMAIN, platform_discovered) + + return True class BaseNotificationService(object): diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 99bba0d71f95be..f86d701f23442a 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -8,8 +8,6 @@ from homeassistant.components.notify import (ATTR_TARGET, BaseNotificationService) -DEPENDENCIES = ['mysensors'] - def get_service(hass, config): """Get the MySensors notification service.""" From bd73c9104e998abfda9eb1b0c7d4a947a9184517 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Tue, 15 Nov 2016 15:08:49 +0100 Subject: [PATCH 4/5] Fix notify tests --- tests/common.py | 2 +- tests/components/notify/test_command_line.py | 16 +++++++++------- tests/components/notify/test_file.py | 7 ++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/common.py b/tests/common.py index 25a674dd995f38..514a4973202853 100644 --- a/tests/common.py +++ b/tests/common.py @@ -399,7 +399,7 @@ def assert_setup_component(count, domain=None): Use as a context manager aroung bootstrap.setup_component with assert_setup_component(0) as result_config: - setup_component(hass, start_config, domain) + setup_component(hass, domain, start_config) # using result_config is optional """ config = {} diff --git a/tests/components/notify/test_command_line.py b/tests/components/notify/test_command_line.py index 0d2235514f8634..7fcf4b65f841db 100644 --- a/tests/components/notify/test_command_line.py +++ b/tests/components/notify/test_command_line.py @@ -6,7 +6,7 @@ from homeassistant.bootstrap import setup_component import homeassistant.components.notify as notify -from tests.common import get_test_home_assistant +from tests.common import assert_setup_component, get_test_home_assistant class TestCommandLine(unittest.TestCase): @@ -31,12 +31,14 @@ def test_setup(self): def test_bad_config(self): """Test set up the platform with bad/missing configuration.""" - self.assertFalse(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'bad_platform', - } - })) + # Platform should not be set up, but component should be set up. + with assert_setup_component(0): + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'bad_platform', + } + })) def test_command_line_output(self): """Test the command line output.""" diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 08407b20a58025..1cc96f4fba3fe2 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -25,8 +25,9 @@ def tearDown(self): # pylint: disable=invalid-name def test_bad_config(self): """Test set up the platform with bad/missing config.""" + # Platform should not be set up, but component should be set up. with assert_setup_component(0): - assert not setup_component(self.hass, notify.DOMAIN, { + assert setup_component(self.hass, notify.DOMAIN, { 'notify': { 'name': 'test', 'platform': 'file', @@ -42,8 +43,8 @@ def test_notify_file(self, mock_utcnow, mock_stat): m_open = mock_open() with patch( - 'homeassistant.components.notify.file.open', - m_open, create=True + 'homeassistant.components.notify.file.open', + m_open, create=True ): filename = 'mock_file' message = 'one, two, testing, testing' From 4b994c6a4c09a875995be4aee7845cdbb0648f75 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Fri, 2 Dec 2016 21:55:59 +0100 Subject: [PATCH 5/5] Update notify tests * Continue setup of notify platforms if a platform fails setup. * Remove notify tests that check platform config. These tests are not needed when config validation is used. * Add config validation to APNS notify platform. --- homeassistant/components/notify/__init__.py | 3 +- homeassistant/components/notify/apns.py | 22 ++++++------ tests/components/notify/test_apns.py | 37 -------------------- tests/components/notify/test_command_line.py | 11 ------ tests/components/notify/test_file.py | 11 ------ 5 files changed, 12 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 4ac60fc9f08737..b13f4168e594c6 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -136,7 +136,8 @@ def notify_message(notify_service, call): for platform, p_config in config_per_platform(config, DOMAIN): if not setup_notify_platform(platform, p_config): - return False + _LOGGER.error("Failed to set up platform %s", platform) + continue def platform_discovered(platform, info): """Callback to load a platform.""" diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 26d20f3bc890ae..4346612b313df0 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -11,18 +11,27 @@ from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_DATA, BaseNotificationService) + ATTR_TARGET, ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_NAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template as template_helper DOMAIN = "apns" APNS_DEVICES = "apns.yaml" +CONF_CERTFILE = "cert_file" +CONF_TOPIC = "topic" DEVICE_TRACKER_DOMAIN = "device_tracker" SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" ATTR_NAME = "name" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CERTFILE): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_TOPIC): cv.string, +}) + REGISTER_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME, default=None): cv.string, @@ -37,19 +46,8 @@ def get_service(hass, config): os.path.join(os.path.dirname(__file__), 'services.yaml')) name = config.get("name") - if name is None: - logging.error("Name must be specified.") - return None - cert_file = config.get('cert_file') - if cert_file is None: - logging.error("Certificate must be specified.") - return None - topic = config.get('topic') - if topic is None: - logging.error("Topic must be specified.") - return None sandbox = bool(config.get('sandbox', False)) diff --git a/tests/components/notify/test_apns.py b/tests/components/notify/test_apns.py index 8be04de9b230a1..271dcb19e61661 100644 --- a/tests/components/notify/test_apns.py +++ b/tests/components/notify/test_apns.py @@ -29,43 +29,6 @@ def test_apns_setup_full(self): self.assertTrue(notify.setup(hass, config)) - def test_apns_setup_missing_name(self): - """Test setup with missing name.""" - config = { - 'notify': { - 'platform': 'apns', - 'sandbox': 'True', - 'topic': 'testapp.appname', - 'cert_file': 'test_app.pem' - } - } - hass = get_test_home_assistant() - self.assertFalse(notify.setup(hass, config)) - - def test_apns_setup_missing_certificate(self): - """Test setup with missing name.""" - config = { - 'notify': { - 'platform': 'apns', - 'topic': 'testapp.appname', - 'name': 'test_app' - } - } - hass = get_test_home_assistant() - self.assertFalse(notify.setup(hass, config)) - - def test_apns_setup_missing_topic(self): - """Test setup with missing topic.""" - config = { - 'notify': { - 'platform': 'apns', - 'cert_file': 'test_app.pem', - 'name': 'test_app' - } - } - hass = get_test_home_assistant() - self.assertFalse(notify.setup(hass, config)) - def test_register_new_device(self): """Test registering a new device with a name.""" config = { diff --git a/tests/components/notify/test_command_line.py b/tests/components/notify/test_command_line.py index 7fcf4b65f841db..69a3a6a921e266 100644 --- a/tests/components/notify/test_command_line.py +++ b/tests/components/notify/test_command_line.py @@ -29,17 +29,6 @@ def test_setup(self): 'command': 'echo $(cat); exit 1', }}) - def test_bad_config(self): - """Test set up the platform with bad/missing configuration.""" - # Platform should not be set up, but component should be set up. - with assert_setup_component(0): - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'bad_platform', - } - })) - def test_command_line_output(self): """Test the command line output.""" with tempfile.TemporaryDirectory() as tempdirname: diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 1cc96f4fba3fe2..bb9265bb4a12d3 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -23,17 +23,6 @@ def tearDown(self): # pylint: disable=invalid-name """"Stop down everything that was started.""" self.hass.stop() - def test_bad_config(self): - """Test set up the platform with bad/missing config.""" - # Platform should not be set up, but component should be set up. - with assert_setup_component(0): - assert setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - }, - }) - @patch('homeassistant.components.notify.file.os.stat') @patch('homeassistant.util.dt.utcnow') def test_notify_file(self, mock_utcnow, mock_stat):