Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Konnected Pro alarm panel, embrace async, leverage latest HA features/architecture #30894

Merged
merged 13 commits into from
Feb 11, 2020
Prev Previous commit
Next Next commit
format for import as part of config schema validation
  • Loading branch information
kit-klein committed Feb 6, 2020
commit 90428eebbb81edbeffe03b155cadce8ca6a45030
148 changes: 146 additions & 2 deletions homeassistant/components/konnected/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Support for Konnected devices."""
import asyncio
import copy
import hmac
import json
import logging
Expand All @@ -9,17 +10,22 @@
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ACCESS_TOKEN,
CONF_BINARY_SENSORS,
CONF_DEVICES,
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PIN,
CONF_PORT,
CONF_SENSORS,
CONF_SWITCHES,
CONF_TYPE,
CONF_ZONE,
HTTP_BAD_REQUEST,
HTTP_NOT_FOUND,
Expand All @@ -31,31 +37,169 @@
from homeassistant.helpers import config_validation as cv

from .config_flow import ( # Loading the config flow file will register the flow
DEVICE_SCHEMA_YAML,
CONF_DEFAULT_OPTIONS,
CONF_IO,
CONF_IO_BIN,
CONF_IO_DIG,
CONF_IO_SWI,
OPTIONS_SCHEMA,
)
from .const import (
CONF_ACTIVATION,
CONF_API_HOST,
CONF_BLINK,
CONF_DISCOVERY,
CONF_INVERSE,
CONF_MOMENTARY,
CONF_PAUSE,
CONF_POLL_INTERVAL,
CONF_REPEAT,
DOMAIN,
PIN_TO_ZONE,
STATE_HIGH,
STATE_LOW,
UPDATE_ENDPOINT,
ZONE_TO_PIN,
ZONES,
)
from .errors import CannotConnect
from .handlers import HANDLERS
from .panel import AlarmPanel

_LOGGER = logging.getLogger(__name__)


def ensure_pin(value):
"""Check if valid pin and coerce to string."""
if value is None:
raise vol.Invalid("pin value is None")

if PIN_TO_ZONE.get(str(value)) is None:
raise vol.Invalid("pin not valid")

return str(value)


def ensure_zone(value):
"""Check if valid zone and coerce to string."""
if value is None:
raise vol.Invalid("zone value is None")

if str(value) not in ZONES is None:
raise vol.Invalid("zone not valid")

return str(value)


def import_validator(config):
"""Validate zones and reformat for import."""
config = copy.deepcopy(config)
io_cfgs = {}
# Replace pins with zones
for zone in config.get(CONF_BINARY_SENSORS, []):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
if zone.get(CONF_PIN):
zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]]
del zone[CONF_PIN]
io_cfgs[zone[CONF_ZONE]] = CONF_IO_BIN
for zone in config.get(CONF_SENSORS, []):
if zone.get(CONF_PIN):
zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]]
del zone[CONF_PIN]
io_cfgs[zone[CONF_ZONE]] = CONF_IO_DIG
for zone in config.get(CONF_SWITCHES, []):
if zone.get(CONF_PIN):
zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]]
del zone[CONF_PIN]
io_cfgs[zone[CONF_ZONE]] = CONF_IO_SWI

# Migrate config_entry data into default_options structure
config[CONF_IO] = io_cfgs
config[CONF_DEFAULT_OPTIONS] = OPTIONS_SCHEMA(config)

# clean up fields migrated to options
config.pop(CONF_BINARY_SENSORS, None)
config.pop(CONF_SENSORS, None)
config.pop(CONF_SWITCHES, None)
config.pop(CONF_BLINK, None)
config.pop(CONF_DISCOVERY, None)
config.pop(CONF_IO, None)
return config


# configuration.yaml schemas (legacy)
BINARY_SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INVERSE, default=False): cv.boolean,
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
vol.Required(CONF_TYPE): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

SWITCH_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)
),
vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)),
vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)),
vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)),
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

DEVICE_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA_YAML]
),
vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA_YAML]),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA_YAML]),
vol.Inclusive(CONF_HOST, "host_info"): cv.string,
vol.Inclusive(CONF_PORT, "host_info"): cv.port,
vol.Optional(CONF_BLINK, default=True): cv.boolean,
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}
),
import_validator,
)

# pylint: disable=no-value-for-parameter
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_API_HOST): vol.Url(),
vol.Optional(CONF_DEVICES): [DEVICE_SCHEMA_YAML],
vol.Optional(CONF_DEVICES): vol.All(
cv.ensure_list, [DEVICE_SCHEMA_YAML]
),
}
)
},
Expand Down
118 changes: 4 additions & 114 deletions homeassistant/components/konnected/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
CONF_HOST,
CONF_ID,
CONF_NAME,
CONF_PIN,
CONF_PORT,
CONF_SENSORS,
CONF_SWITCHES,
Expand All @@ -41,7 +40,6 @@
CONF_POLL_INTERVAL,
CONF_REPEAT,
DOMAIN,
PIN_TO_ZONE,
STATE_HIGH,
STATE_LOW,
ZONES,
Expand Down Expand Up @@ -69,89 +67,6 @@
OPTIONS_IO_OUTPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_SWI])


def ensure_pin(value):
"""Check if valid pin and coerce to string."""
if value is None:
raise vol.Invalid("pin value is None")

if PIN_TO_ZONE.get(str(value)) is None:
raise vol.Invalid("pin not valid")

return str(value)


def ensure_zone(value):
"""Check if valid zone and coerce to string."""
if value is None:
raise vol.Invalid("zone value is None")

if str(value) not in ZONES is None:
raise vol.Invalid("zone not valid")

return str(value)


# configuration.yaml schemas (legacy)
BINARY_SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INVERSE, default=False): cv.boolean,
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

SENSOR_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
vol.Required(CONF_TYPE): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

SWITCH_SCHEMA_YAML = vol.All(
vol.Schema(
{
vol.Exclusive(CONF_ZONE, "s_zone"): ensure_zone,
vol.Exclusive(CONF_PIN, "s_pin"): ensure_pin,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)
),
vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)),
vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)),
vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)),
}
),
cv.has_at_least_one_key(CONF_PIN, CONF_ZONE),
)

DEVICE_SCHEMA_YAML = vol.Schema(
{
vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA_YAML]
),
vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA_YAML]),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA_YAML]),
vol.Inclusive(CONF_HOST, "host_info"): cv.string,
vol.Inclusive(CONF_PORT, "host_info"): cv.port,
vol.Optional(CONF_BLINK, default=True): cv.boolean,
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}
)

# Config entry schemas
IO_SCHEMA = vol.Schema(
{
Expand Down Expand Up @@ -209,15 +124,6 @@ def ensure_zone(value):
}
)


def pins_to_zones(config):
"""Swap out pin for zones in a io config."""
for zone in config:
if zone.get(CONF_PIN):
zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]]
del zone[CONF_PIN]


OPTIONS_SCHEMA = vol.Schema(
{
vol.Required(CONF_IO): IO_SCHEMA,
Expand All @@ -233,15 +139,16 @@ def pins_to_zones(config):
)

CONF_DEFAULT_OPTIONS = "default_options"
CONFIG_SCHEMA = vol.Schema(
CONFIG_ENTRY_SCHEMA = vol.Schema(
{
vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_MODEL): vol.Any(*KONN_PANEL_MODEL_NAMES),
vol.Required(CONF_ACCESS_TOKEN): cv.matches_regex("[a-zA-Z0-9]+"),
vol.Required(CONF_DEFAULT_OPTIONS): OPTIONS_SCHEMA,
}
},
extra=vol.REMOVE_EXTRA,
)


Expand Down Expand Up @@ -283,26 +190,9 @@ async def async_step_import(self, device_config):
"""
_LOGGER.debug(device_config)

# populate any options
device_config[CONF_IO] = {}
if device_config.get(CONF_BINARY_SENSORS):
pins_to_zones(device_config[CONF_BINARY_SENSORS])
for io_cfg in device_config[CONF_BINARY_SENSORS]:
device_config[CONF_IO][io_cfg[CONF_ZONE]] = CONF_IO_BIN

if device_config.get(CONF_SENSORS):
pins_to_zones(device_config[CONF_SENSORS])
for io_cfg in device_config[CONF_SENSORS]:
device_config[CONF_IO][io_cfg[CONF_ZONE]] = CONF_IO_DIG

if device_config.get(CONF_SWITCHES):
pins_to_zones(device_config[CONF_SWITCHES])
for io_cfg in device_config[CONF_SWITCHES]:
device_config[CONF_IO][io_cfg[CONF_ZONE]] = CONF_IO_SWI

# save the data and confirm connection via user step
await self.async_set_unique_id(device_config["id"])
self.options = OPTIONS_SCHEMA(device_config)
self.options = device_config[CONF_DEFAULT_OPTIONS]
if device_config.get(CONF_HOST) and device_config.get(CONF_PORT):
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
return await self.async_step_user(
user_input={
Expand Down
Loading