diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 9ce4b6b166dd4a..b412a5d44bb0c3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -474,6 +474,60 @@ def validator(config): return validator +def is_mac_address(value=None, separators=None, allow_lowercase=True, + allow_uppercase=True, chunk=2): + """Validate that a value is a MAC address.""" + if not allow_lowercase and not allow_uppercase: + raise vol.Invalid("Must not disallow upper and lowercase") + + if separators is None: + separators = ['', '.', ':', '-'] + elif not isinstance(separators, list): + raise vol.Invalid('separators must be a list') + + if 12 % chunk != 0: + raise vol.Invalid("Invalid chunk size for MAC address") + + characters = ["0-9"] + if allow_lowercase: + characters.append("a-z") + if allow_uppercase: + characters.append("A-Z") + + repeat = int(12 / chunk) + character_class = "[%s]{%s}" % ("".join(characters), chunk) + format_list = [] + + for s in separators: + if s == '': + format_list.append(s.join([character_class] * repeat)) + else: + sep = "[%s]{1}" % re.escape(s) + format_list.append(sep.join([character_class] * repeat)) + + regex_list = [] + for f in format_list: + regex_list.append(re.compile("^%s$" % f)) + + def validator(value: Any) -> str: + """Validate that the value is a MAC address.""" + if not isinstance(value, str): + raise vol.Invalid('not a string value: {}'.format(value)) + + matches = False + for r in regex_list: + if r.match(value): + matches = True + break + + if not matches: + err = "value {} is not a permitted MAC address".format(value) + raise vol.Invalid(err) + + return value + return validator + + # Validator helpers def key_dependency(key, dependency): diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cfd84dbc3b3b0a..a68f5ecca4340f 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -584,3 +584,74 @@ def test_is_regex(): valid_re = ".*" schema(valid_re) + + +def test_is_mac_address(): + """Test the mac_address validator.""" + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(separators=True)) + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(chunk=11)) + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(allow_lowercase=False, + allow_uppercase=False)) + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(allow_uppercase=False)) + schema('0123456789AB') + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(allow_lowercase=False)) + schema('0123456789ab') + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(separators=[':'])) + schema('01-23-45-67-89-ab') + + with pytest.raises(vol.Invalid): + schema = vol.Schema(cv.is_mac_address(separators=[':'])) + schema('not a mac!') + + test_2_chunk = [ + '01:23:45:67:89:AB', + '01:23:45:67:89:ab', + '01.23.45.67.89.AB', + '01.23.45.67.89.ab', + '01-23-45-67-89-AB', + '01-23-45-67-89-ab', + '0123456789AB', + '0123456789ab', + ] + schema = vol.Schema(cv.is_mac_address) + for mac in test_2_chunk: + schema(mac) + + test_4_chunk = [ + '0123:4567:89AB', + '0123:4567:89ab', + '0123.4567.89AB', + '0123.4567.89ab', + '0123-4567-89AB', + '0123-4567-89ab', + '0123456789AB', + '0123456789ab', + ] + schema = vol.Schema(cv.is_mac_address(chunk=4)) + for mac in test_4_chunk: + schema(mac) + + test_6_chunk = [ + '012345:6789AB', + '012345:6789ab', + '012345.6789AB', + '012345.6789ab', + '012345-6789AB', + '012345-6789ab', + '0123456789AB', + '0123456789ab', + ] + schema = vol.Schema(cv.is_mac_address(chunk=6)) + for mac in test_6_chunk: + schema(mac)