diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index fbe3621a4af..f9272b4ab59 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -1,5 +1,5 @@ """MySensors constants.""" -import homeassistant.helpers.config_validation as cv +from collections import defaultdict ATTR_DEVICES = 'devices' @@ -25,117 +25,102 @@ NODE_CALLBACK = 'mysensors_node_callback_{}_{}' TYPE = 'type' UPDATE_DELAY = 0.1 -# MySensors const schemas -BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} -CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} -LIGHT_DIMMER_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_DIMMER', - SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} -LIGHT_PERCENTAGE_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_PERCENTAGE', - SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGB_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { - 'V_RGB': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGBW_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { - 'V_RGBW': cv.string, 'V_STATUS': cv.string}} -NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} -DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} -DUST_SCHEMA = [ - {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] -SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} -SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} -MYSENSORS_CONST_SCHEMA = { - 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SPRINKLER': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_WATER_LEAK': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SOUND': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_VIBRATION': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOISTURE': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_HVAC': [CLIMATE_SCHEMA], - 'S_COVER': [ - {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, - {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, - {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, - {PLATFORM: 'cover', TYPE: 'V_STATUS'}], - 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], - 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], - 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], - 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], - 'S_GPS': [ - DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], - 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], - 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], - 'S_BARO': [ - {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, - {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], - 'S_WIND': [ - {PLATFORM: 'sensor', TYPE: 'V_WIND'}, - {PLATFORM: 'sensor', TYPE: 'V_GUST'}, - {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], - 'S_RAIN': [ - {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, - {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], - 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], - 'S_WEIGHT': [ - {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_POWER': [ - {PLATFORM: 'sensor', TYPE: 'V_WATT'}, - {PLATFORM: 'sensor', TYPE: 'V_KWH'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR'}, - {PLATFORM: 'sensor', TYPE: 'V_VA'}, - {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], - 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], - 'S_LIGHT_LEVEL': [ - {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], - 'S_IR': [ - {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, - {PLATFORM: 'switch', TYPE: 'V_IR_SEND', - SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], - 'S_WATER': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_CUSTOM': [ - {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, - {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], - 'S_SCENE_CONTROLLER': [ - {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, - {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], - 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], - 'S_MULTIMETER': [ - {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, - {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_GAS': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_WATER_QUALITY': [ - {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, - {PLATFORM: 'sensor', TYPE: 'V_PH'}, - {PLATFORM: 'sensor', TYPE: 'V_ORP'}, - {PLATFORM: 'sensor', TYPE: 'V_EC'}, - {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_AIR_QUALITY': DUST_SCHEMA, - 'S_DUST': DUST_SCHEMA, - 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], - 'S_BINARY': [SWITCH_STATUS_SCHEMA], - 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], +BINARY_SENSOR_TYPES = { + 'S_DOOR': 'V_TRIPPED', + 'S_MOTION': 'V_TRIPPED', + 'S_SMOKE': 'V_TRIPPED', + 'S_SPRINKLER': 'V_TRIPPED', + 'S_WATER_LEAK': 'V_TRIPPED', + 'S_SOUND': 'V_TRIPPED', + 'S_VIBRATION': 'V_TRIPPED', + 'S_MOISTURE': 'V_TRIPPED', } + +CLIMATE_TYPES = { + 'S_HVAC': 'V_HVAC_FLOW_STATE', +} + +COVER_TYPES = { + 'S_COVER': ['V_DIMMER', 'V_PERCENTAGE', 'V_LIGHT', 'V_STATUS'], +} + +DEVICE_TRACKER_TYPES = { + 'S_GPS': 'V_POSITION', +} + +LIGHT_TYPES = { + 'S_DIMMER': ['V_DIMMER', 'V_PERCENTAGE'], + 'S_RGB_LIGHT': 'V_RGB', + 'S_RGBW_LIGHT': 'V_RGBW', +} + +NOTIFY_TYPES = { + 'S_INFO': 'V_TEXT', +} + +SENSOR_TYPES = { + 'S_SOUND': 'V_LEVEL', + 'S_VIBRATION': 'V_LEVEL', + 'S_MOISTURE': 'V_LEVEL', + 'S_INFO': 'V_TEXT', + 'S_GPS': 'V_POSITION', + 'S_TEMP': 'V_TEMP', + 'S_HUM': 'V_HUM', + 'S_BARO': ['V_PRESSURE', 'V_FORECAST'], + 'S_WIND': ['V_WIND', 'V_GUST', 'V_DIRECTION'], + 'S_RAIN': ['V_RAIN', 'V_RAINRATE'], + 'S_UV': 'V_UV', + 'S_WEIGHT': ['V_WEIGHT', 'V_IMPEDANCE'], + 'S_POWER': ['V_WATT', 'V_KWH', 'V_VAR', 'V_VA', 'V_POWER_FACTOR'], + 'S_DISTANCE': 'V_DISTANCE', + 'S_LIGHT_LEVEL': ['V_LIGHT_LEVEL', 'V_LEVEL'], + 'S_IR': 'V_IR_RECEIVE', + 'S_WATER': ['V_FLOW', 'V_VOLUME'], + 'S_CUSTOM': ['V_VAR1', 'V_VAR2', 'V_VAR3', 'V_VAR4', 'V_VAR5', 'V_CUSTOM'], + 'S_SCENE_CONTROLLER': ['V_SCENE_ON', 'V_SCENE_OFF'], + 'S_COLOR_SENSOR': 'V_RGB', + 'S_MULTIMETER': ['V_VOLTAGE', 'V_CURRENT', 'V_IMPEDANCE'], + 'S_GAS': ['V_FLOW', 'V_VOLUME'], + 'S_WATER_QUALITY': ['V_TEMP', 'V_PH', 'V_ORP', 'V_EC'], + 'S_AIR_QUALITY': ['V_DUST_LEVEL', 'V_LEVEL'], + 'S_DUST': ['V_DUST_LEVEL', 'V_LEVEL'], +} + +SWITCH_TYPES = { + 'S_LIGHT': 'V_LIGHT', + 'S_BINARY': 'V_STATUS', + 'S_DOOR': 'V_ARMED', + 'S_MOTION': 'V_ARMED', + 'S_SMOKE': 'V_ARMED', + 'S_SPRINKLER': 'V_STATUS', + 'S_WATER_LEAK': 'V_ARMED', + 'S_SOUND': 'V_ARMED', + 'S_VIBRATION': 'V_ARMED', + 'S_MOISTURE': 'V_ARMED', + 'S_IR': 'V_IR_SEND', + 'S_LOCK': 'V_LOCK_STATUS', + 'S_WATER_QUALITY': 'V_STATUS', +} + + +PLATFORM_TYPES = { + 'binary_sensor': BINARY_SENSOR_TYPES, + 'climate': CLIMATE_TYPES, + 'cover': COVER_TYPES, + 'device_tracker': DEVICE_TRACKER_TYPES, + 'light': LIGHT_TYPES, + 'notify': NOTIFY_TYPES, + 'sensor': SENSOR_TYPES, + 'switch': SWITCH_TYPES, +} + +FLAT_PLATFORM_TYPES = { + (platform, s_type_name): v_type_name + for platform, platform_types in PLATFORM_TYPES.items() + for s_type_name, v_type_name in platform_types.items() +} + +TYPE_TO_PLATFORMS = defaultdict(list) +for platform, platform_types in PLATFORM_TYPES.items(): + for s_type_name in platform_types: + TYPE_TO_PLATFORMS[s_type_name].append(platform) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 62ea20cbb91..19f8b82a669 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -20,7 +20,7 @@ from .const import ( CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, DOMAIN, MYSENSORS_GATEWAY_READY, MYSENSORS_GATEWAYS) from .handler import HANDLERS -from .helpers import discover_mysensors_platform, validate_child +from .helpers import discover_mysensors_platform, validate_child, validate_node _LOGGER = logging.getLogger(__name__) @@ -161,6 +161,8 @@ async def _discover_persistent_devices(hass, hass_config, gateway): tasks = [] new_devices = defaultdict(list) for node_id in gateway.sensors: + if not validate_node(gateway, node_id): + continue node = gateway.sensors[node_id] for child in node.children.values(): validated = validate_child(gateway, node_id, child) diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 886660baffe..cf936b84905 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -7,26 +7,17 @@ from homeassistant.util import decorator from .const import MYSENSORS_GATEWAY_READY, CHILD_CALLBACK, NODE_CALLBACK from .device import get_mysensors_devices -from .helpers import discover_mysensors_platform, validate_child +from .helpers import discover_mysensors_platform, validate_set_msg _LOGGER = logging.getLogger(__name__) HANDLERS = decorator.Registry() -@HANDLERS.register('presentation') -async def handle_presentation(hass, hass_config, msg): - """Handle a mysensors presentation message.""" - # Handle both node and child presentation. - from mysensors.const import SYSTEM_CHILD_ID - if msg.child_id == SYSTEM_CHILD_ID: - return - _handle_child_update(hass, hass_config, msg) - - @HANDLERS.register('set') async def handle_set(hass, hass_config, msg): """Handle a mysensors set message.""" - _handle_child_update(hass, hass_config, msg) + validated = validate_set_msg(msg) + _handle_child_update(hass, hass_config, validated) @HANDLERS.register('internal') @@ -77,14 +68,12 @@ async def handle_gateway_ready(hass, hass_config, msg): @callback -def _handle_child_update(hass, hass_config, msg): +def _handle_child_update(hass, hass_config, validated): """Handle a child update.""" - child = msg.gateway.sensors[msg.node_id].children[msg.child_id] signals = [] # Update all platforms for the device via dispatcher. - # Add/update entity if schema validates to true. - validated = validate_child(msg.gateway, msg.node_id, child) + # Add/update entity for validated children. for platform, dev_ids in validated.items(): devices = get_mysensors_devices(hass, platform) new_dev_ids = [] diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index a49967cf835..24e1cbc91c9 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -8,11 +8,12 @@ from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.util.decorator import Registry -from .const import ( - ATTR_DEVICES, DOMAIN, MYSENSORS_CONST_SCHEMA, PLATFORM, SCHEMA, TYPE) +from .const import ATTR_DEVICES, DOMAIN, FLAT_PLATFORM_TYPES, TYPE_TO_PLATFORMS _LOGGER = logging.getLogger(__name__) +SCHEMAS = Registry() @callback @@ -24,58 +25,116 @@ def discover_mysensors_platform(hass, hass_config, platform, new_devices): return task -def validate_child(gateway, node_id, child): - """Validate that a child has the correct values according to schema. +def default_schema(gateway, child, value_type_name): + """Return a default validation schema for value types.""" + schema = {value_type_name: cv.string} + return get_child_schema(gateway, child, value_type_name, schema) - Return a dict of platform with a list of device ids for validated devices. - """ - validated = defaultdict(list) - if not child.values: - _LOGGER.debug( - "No child values for node %s child %s", node_id, child.id) - return validated - if gateway.sensors[node_id].sketch_name is None: - _LOGGER.debug("Node %s is missing sketch name", node_id) - return validated +@SCHEMAS.register(('light', 'V_DIMMER')) +def light_dimmer_schema(gateway, child, value_type_name): + """Return a validation schema for V_DIMMER.""" + schema = {'V_DIMMER': cv.string, 'V_LIGHT': cv.string} + return get_child_schema(gateway, child, value_type_name, schema) + + +@SCHEMAS.register(('light', 'V_PERCENTAGE')) +def light_percentage_schema(gateway, child, value_type_name): + """Return a validation schema for V_PERCENTAGE.""" + schema = {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string} + return get_child_schema(gateway, child, value_type_name, schema) + + +@SCHEMAS.register(('light', 'V_RGB')) +def light_rgb_schema(gateway, child, value_type_name): + """Return a validation schema for V_RGB.""" + schema = {'V_RGB': cv.string, 'V_STATUS': cv.string} + return get_child_schema(gateway, child, value_type_name, schema) + + +@SCHEMAS.register(('light', 'V_RGBW')) +def light_rgbw_schema(gateway, child, value_type_name): + """Return a validation schema for V_RGBW.""" + schema = {'V_RGBW': cv.string, 'V_STATUS': cv.string} + return get_child_schema(gateway, child, value_type_name, schema) + + +@SCHEMAS.register(('switch', 'V_IR_SEND')) +def switch_ir_send_schema(gateway, child, value_type_name): + """Return a validation schema for V_IR_SEND.""" + schema = {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string} + return get_child_schema(gateway, child, value_type_name, schema) + + +def get_child_schema(gateway, child, value_type_name, schema): + """Return a child schema.""" + set_req = gateway.const.SetReq + child_schema = child.get_schema(gateway.protocol_version) + schema = child_schema.extend( + {vol.Required( + set_req[name].value, msg=invalid_msg(gateway, child, name)): + child_schema.schema.get(set_req[name].value, valid) + for name, valid in schema.items()}, + extra=vol.ALLOW_EXTRA) + return schema + + +def invalid_msg(gateway, child, value_type_name): + """Return a message for an invalid child during schema validation.""" pres = gateway.const.Presentation set_req = gateway.const.SetReq - s_name = next( + return "{} requires value_type {}".format( + pres(child.type).name, set_req[value_type_name].name) + + +def validate_set_msg(msg): + """Validate a set message.""" + if not validate_node(msg.gateway, msg.node_id): + return {} + child = msg.gateway.sensors[msg.node_id].children[msg.child_id] + return validate_child(msg.gateway, msg.node_id, child, msg.sub_type) + + +def validate_node(gateway, node_id): + """Validate a node.""" + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.debug("Node %s is missing sketch name", node_id) + return False + return True + + +def validate_child(gateway, node_id, child, value_type=None): + """Validate a child.""" + validated = defaultdict(list) + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + child_type_name = next( (member.name for member in pres if member.value == child.type), None) - if s_name not in MYSENSORS_CONST_SCHEMA: - _LOGGER.warning("Child type %s is not supported", s_name) + value_types = [value_type] if value_type else [*child.values] + value_type_names = [ + member.name for member in set_req if member.value in value_types] + platforms = TYPE_TO_PLATFORMS.get(child_type_name, []) + if not platforms: + _LOGGER.warning("Child type %s is not supported", child.type) return validated - child_schemas = MYSENSORS_CONST_SCHEMA[s_name] - def msg(name): - """Return a message for an invalid schema.""" - return "{} requires value_type {}".format( - pres(child.type).name, set_req[name].name) + for platform in platforms: + v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] + if not isinstance(v_names, list): + v_names = [v_names] + v_names = [v_name for v_name in v_names if v_name in value_type_names] + + for v_name in v_names: + child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) + child_schema = child_schema_gen(gateway, child, v_name) + try: + child_schema(child.values) + except vol.Invalid as exc: + _LOGGER.warning( + "Invalid %s on node %s, %s platform: %s", + child, node_id, platform, exc) + continue + dev_id = id(gateway), node_id, child.id, set_req[v_name].value + validated[platform].append(dev_id) - for schema in child_schemas: - platform = schema[PLATFORM] - v_name = schema[TYPE] - value_type = next( - (member.value for member in set_req if member.name == v_name), - None) - if value_type is None: - continue - _child_schema = child.get_schema(gateway.protocol_version) - vol_schema = _child_schema.extend( - {vol.Required(set_req[key].value, msg=msg(key)): - _child_schema.schema.get(set_req[key].value, val) - for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, - extra=vol.ALLOW_EXTRA) - try: - vol_schema(child.values) - except vol.Invalid as exc: - level = (logging.WARNING if value_type in child.values - else logging.DEBUG) - _LOGGER.log( - level, - "Invalid values: %s: %s platform: node %s child %s: %s", - child.values, platform, node_id, child.id, exc) - continue - dev_id = id(gateway), node_id, child.id, value_type - validated[platform].append(dev_id) return validated