Add SmartStart provisioning support to zwave_js WS API (#59037)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
694f6d0abe
commit
eeafa36abc
2 changed files with 1213 additions and 27 deletions
|
@ -15,7 +15,10 @@ from zwave_js_server.const import (
|
|||
CommandClass,
|
||||
InclusionStrategy,
|
||||
LogLevel,
|
||||
Protocols,
|
||||
QRCodeVersion,
|
||||
SecurityClass,
|
||||
ZwaveFeature,
|
||||
)
|
||||
from zwave_js_server.exceptions import (
|
||||
BaseZwaveJSServerError,
|
||||
|
@ -25,7 +28,12 @@ from zwave_js_server.exceptions import (
|
|||
SetValueFailed,
|
||||
)
|
||||
from zwave_js_server.firmware import begin_firmware_update
|
||||
from zwave_js_server.model.controller import ControllerStatistics, InclusionGrant
|
||||
from zwave_js_server.model.controller import (
|
||||
ControllerStatistics,
|
||||
InclusionGrant,
|
||||
ProvisioningEntry,
|
||||
QRProvisioningInformation,
|
||||
)
|
||||
from zwave_js_server.model.firmware import (
|
||||
FirmwareUpdateFinished,
|
||||
FirmwareUpdateProgress,
|
||||
|
@ -33,12 +41,14 @@ from zwave_js_server.model.firmware import (
|
|||
from zwave_js_server.model.log_config import LogConfig
|
||||
from zwave_js_server.model.log_message import LogMessage
|
||||
from zwave_js_server.model.node import Node, NodeStatistics
|
||||
from zwave_js_server.model.utils import async_parse_qr_code_string
|
||||
from zwave_js_server.util.node import async_set_config_parameter
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||
from homeassistant.components.websocket_api.const import (
|
||||
ERR_INVALID_FORMAT,
|
||||
ERR_NOT_FOUND,
|
||||
ERR_NOT_SUPPORTED,
|
||||
ERR_UNKNOWN_ERROR,
|
||||
|
@ -80,8 +90,6 @@ TYPE = "type"
|
|||
PROPERTY = "property"
|
||||
PROPERTY_KEY = "property_key"
|
||||
VALUE = "value"
|
||||
INCLUSION_STRATEGY = "inclusion_strategy"
|
||||
PIN = "pin"
|
||||
|
||||
# constants for log config commands
|
||||
CONFIG = "config"
|
||||
|
@ -106,6 +114,113 @@ CLIENT_SIDE_AUTH = "client_side_auth"
|
|||
# constants for migration
|
||||
DRY_RUN = "dry_run"
|
||||
|
||||
# constants for inclusion
|
||||
INCLUSION_STRATEGY = "inclusion_strategy"
|
||||
PIN = "pin"
|
||||
FORCE_SECURITY = "force_security"
|
||||
PLANNED_PROVISIONING_ENTRY = "planned_provisioning_entry"
|
||||
QR_PROVISIONING_INFORMATION = "qr_provisioning_information"
|
||||
QR_CODE_STRING = "qr_code_string"
|
||||
|
||||
DSK = "dsk"
|
||||
|
||||
VERSION = "version"
|
||||
GENERIC_DEVICE_CLASS = "generic_device_class"
|
||||
SPECIFIC_DEVICE_CLASS = "specific_device_class"
|
||||
INSTALLER_ICON_TYPE = "installer_icon_type"
|
||||
MANUFACTURER_ID = "manufacturer_id"
|
||||
PRODUCT_TYPE = "product_type"
|
||||
PRODUCT_ID = "product_id"
|
||||
APPLICATION_VERSION = "application_version"
|
||||
MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval"
|
||||
UUID = "uuid"
|
||||
SUPPORTED_PROTOCOLS = "supported_protocols"
|
||||
|
||||
FEATURE = "feature"
|
||||
UNPROVISION = "unprovision"
|
||||
|
||||
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/security/QR.ts#L41
|
||||
MINIMUM_QR_STRING_LENGTH = 52
|
||||
|
||||
|
||||
def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry:
|
||||
"""Handle provisioning entry dict to ProvisioningEntry."""
|
||||
info = ProvisioningEntry(
|
||||
dsk=info[DSK],
|
||||
security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]],
|
||||
additional_properties={
|
||||
k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES)
|
||||
},
|
||||
)
|
||||
return info
|
||||
|
||||
|
||||
def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation:
|
||||
"""Convert QR provisioning information dict to QRProvisioningInformation."""
|
||||
protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])]
|
||||
info = QRProvisioningInformation(
|
||||
version=QRCodeVersion(info[VERSION]),
|
||||
security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]],
|
||||
dsk=info[DSK],
|
||||
generic_device_class=info[GENERIC_DEVICE_CLASS],
|
||||
specific_device_class=info[SPECIFIC_DEVICE_CLASS],
|
||||
installer_icon_type=info[INSTALLER_ICON_TYPE],
|
||||
manufacturer_id=info[MANUFACTURER_ID],
|
||||
product_type=info[PRODUCT_TYPE],
|
||||
product_id=info[PRODUCT_ID],
|
||||
application_version=info[APPLICATION_VERSION],
|
||||
max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL),
|
||||
uuid=info.get(UUID),
|
||||
supported_protocols=protocols if protocols else None,
|
||||
)
|
||||
return info
|
||||
|
||||
|
||||
# Helper schemas
|
||||
PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(DSK): str,
|
||||
vol.Required(SECURITY_CLASSES): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(SecurityClass)],
|
||||
),
|
||||
},
|
||||
# Provisioning entries can have extra keys for SmartStart
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
),
|
||||
convert_planned_provisioning_entry,
|
||||
)
|
||||
|
||||
QR_PROVISIONING_INFORMATION_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(VERSION): vol.Coerce(QRCodeVersion),
|
||||
vol.Required(SECURITY_CLASSES): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(SecurityClass)],
|
||||
),
|
||||
vol.Required(DSK): str,
|
||||
vol.Required(GENERIC_DEVICE_CLASS): int,
|
||||
vol.Required(SPECIFIC_DEVICE_CLASS): int,
|
||||
vol.Required(INSTALLER_ICON_TYPE): int,
|
||||
vol.Required(MANUFACTURER_ID): int,
|
||||
vol.Required(PRODUCT_TYPE): int,
|
||||
vol.Required(PRODUCT_ID): int,
|
||||
vol.Required(APPLICATION_VERSION): str,
|
||||
vol.Optional(MAX_INCLUSION_REQUEST_INTERVAL): vol.Any(int, None),
|
||||
vol.Optional(UUID): vol.Any(str, None),
|
||||
vol.Optional(SUPPORTED_PROTOCOLS): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(Protocols)],
|
||||
),
|
||||
}
|
||||
),
|
||||
convert_qr_provisioning_information,
|
||||
)
|
||||
|
||||
QR_CODE_STRING_SCHEMA = vol.All(str, vol.Length(min=MINIMUM_QR_STRING_LENGTH))
|
||||
|
||||
|
||||
def async_get_entry(orig_func: Callable) -> Callable:
|
||||
"""Decorate async function to get entry."""
|
||||
|
@ -194,6 +309,11 @@ def async_register_api(hass: HomeAssistant) -> None:
|
|||
websocket_api.async_register_command(hass, websocket_add_node)
|
||||
websocket_api.async_register_command(hass, websocket_grant_security_classes)
|
||||
websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin)
|
||||
websocket_api.async_register_command(hass, websocket_provision_smart_start_node)
|
||||
websocket_api.async_register_command(hass, websocket_unprovision_smart_start_node)
|
||||
websocket_api.async_register_command(hass, websocket_get_provisioning_entries)
|
||||
websocket_api.async_register_command(hass, websocket_parse_qr_code_string)
|
||||
websocket_api.async_register_command(hass, websocket_supports_feature)
|
||||
websocket_api.async_register_command(hass, websocket_stop_inclusion)
|
||||
websocket_api.async_register_command(hass, websocket_stop_exclusion)
|
||||
websocket_api.async_register_command(hass, websocket_remove_node)
|
||||
|
@ -434,9 +554,24 @@ async def websocket_ping_node(
|
|||
{
|
||||
vol.Required(TYPE): "zwave_js/add_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.In(
|
||||
[strategy.value for strategy in InclusionStrategy]
|
||||
vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.In(
|
||||
[
|
||||
strategy.value
|
||||
for strategy in InclusionStrategy
|
||||
if strategy != InclusionStrategy.SMART_START
|
||||
]
|
||||
),
|
||||
),
|
||||
vol.Optional(FORCE_SECURITY): bool,
|
||||
vol.Exclusive(
|
||||
PLANNED_PROVISIONING_ENTRY, "options"
|
||||
): PLANNED_PROVISIONING_ENTRY_SCHEMA,
|
||||
vol.Exclusive(
|
||||
QR_PROVISIONING_INFORMATION, "options"
|
||||
): QR_PROVISIONING_INFORMATION_SCHEMA,
|
||||
vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
@ -452,6 +587,12 @@ async def websocket_add_node(
|
|||
"""Add a node to the Z-Wave network."""
|
||||
controller = client.driver.controller
|
||||
inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY])
|
||||
force_security = msg.get(FORCE_SECURITY)
|
||||
provisioning = (
|
||||
msg.get(PLANNED_PROVISIONING_ENTRY)
|
||||
or msg.get(QR_PROVISIONING_INFORMATION)
|
||||
or msg.get(QR_CODE_STRING)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_cleanup() -> None:
|
||||
|
@ -542,7 +683,18 @@ async def websocket_add_node(
|
|||
),
|
||||
]
|
||||
|
||||
result = await controller.async_begin_inclusion(inclusion_strategy)
|
||||
try:
|
||||
result = await controller.async_begin_inclusion(
|
||||
inclusion_strategy, force_security=force_security, provisioning=provisioning
|
||||
)
|
||||
except ValueError as err:
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
err.args[0],
|
||||
)
|
||||
return
|
||||
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
result,
|
||||
|
@ -554,9 +706,10 @@ async def websocket_add_node(
|
|||
{
|
||||
vol.Required(TYPE): "zwave_js/grant_security_classes",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required(SECURITY_CLASSES): [
|
||||
vol.In([sec_cls.value for sec_cls in SecurityClass])
|
||||
],
|
||||
vol.Required(SECURITY_CLASSES): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Coerce(SecurityClass)],
|
||||
),
|
||||
vol.Optional(CLIENT_SIDE_AUTH, default=False): bool,
|
||||
}
|
||||
)
|
||||
|
@ -570,7 +723,7 @@ async def websocket_grant_security_classes(
|
|||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Add a node to the Z-Wave network."""
|
||||
"""Choose SecurityClass grants as part of S2 inclusion process."""
|
||||
inclusion_grant = InclusionGrant(
|
||||
[SecurityClass(sec_cls) for sec_cls in msg[SECURITY_CLASSES]],
|
||||
msg[CLIENT_SIDE_AUTH],
|
||||
|
@ -597,11 +750,179 @@ async def websocket_validate_dsk_and_enter_pin(
|
|||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Add a node to the Z-Wave network."""
|
||||
"""Validate DSK and enter PIN as part of S2 inclusion process."""
|
||||
await client.driver.controller.async_validate_dsk_and_enter_pin(msg[PIN])
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/provision_smart_start_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Exclusive(
|
||||
PLANNED_PROVISIONING_ENTRY, "options"
|
||||
): PLANNED_PROVISIONING_ENTRY_SCHEMA,
|
||||
vol.Exclusive(
|
||||
QR_PROVISIONING_INFORMATION, "options"
|
||||
): QR_PROVISIONING_INFORMATION_SCHEMA,
|
||||
vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_provision_smart_start_node(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Pre-provision a smart start node."""
|
||||
try:
|
||||
cv.has_at_least_one_key(
|
||||
PLANNED_PROVISIONING_ENTRY, QR_PROVISIONING_INFORMATION, QR_CODE_STRING
|
||||
)(msg)
|
||||
except vol.Invalid as err:
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
err.args[0],
|
||||
)
|
||||
return
|
||||
|
||||
provisioning_info = (
|
||||
msg.get(PLANNED_PROVISIONING_ENTRY)
|
||||
or msg.get(QR_PROVISIONING_INFORMATION)
|
||||
or msg[QR_CODE_STRING]
|
||||
)
|
||||
|
||||
if (
|
||||
QR_PROVISIONING_INFORMATION in msg
|
||||
and provisioning_info.version == QRCodeVersion.S2
|
||||
):
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
"QR code version S2 is not supported for this command",
|
||||
)
|
||||
return
|
||||
await client.driver.controller.async_provision_smart_start_node(provisioning_info)
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/unprovision_smart_start_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Exclusive(DSK, "input"): str,
|
||||
vol.Exclusive(NODE_ID, "input"): int,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_unprovision_smart_start_node(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Unprovision a smart start node."""
|
||||
try:
|
||||
cv.has_at_least_one_key(DSK, NODE_ID)(msg)
|
||||
except vol.Invalid as err:
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
err.args[0],
|
||||
)
|
||||
return
|
||||
dsk_or_node_id = msg.get(DSK) or msg[NODE_ID]
|
||||
await client.driver.controller.async_unprovision_smart_start_node(dsk_or_node_id)
|
||||
connection.send_result(msg[ID])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/get_provisioning_entries",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_get_provisioning_entries(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Get provisioning entries (entries that have been pre-provisioned)."""
|
||||
provisioning_entries = (
|
||||
await client.driver.controller.async_get_provisioning_entries()
|
||||
)
|
||||
connection.send_result(
|
||||
msg[ID], [dataclasses.asdict(entry) for entry in provisioning_entries]
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/parse_qr_code_string",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required(QR_CODE_STRING): QR_CODE_STRING_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_parse_qr_code_string(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Parse a QR Code String and return QRProvisioningInformation dict."""
|
||||
qr_provisioning_information = await async_parse_qr_code_string(
|
||||
client, msg[QR_CODE_STRING]
|
||||
)
|
||||
connection.send_result(msg[ID], dataclasses.asdict(qr_provisioning_information))
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required(TYPE): "zwave_js/supports_feature",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required(FEATURE): vol.Coerce(ZwaveFeature),
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@async_handle_failed_command
|
||||
@async_get_entry
|
||||
async def websocket_supports_feature(
|
||||
hass: HomeAssistant,
|
||||
connection: ActiveConnection,
|
||||
msg: dict,
|
||||
entry: ConfigEntry,
|
||||
client: Client,
|
||||
) -> None:
|
||||
"""Check if controller supports a particular feature."""
|
||||
supported = await client.driver.controller.async_supports_feature(msg[FEATURE])
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
{"supported": supported},
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
|
@ -659,6 +980,7 @@ async def websocket_stop_exclusion(
|
|||
{
|
||||
vol.Required(TYPE): "zwave_js/remove_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Optional(UNPROVISION): bool,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
@ -707,7 +1029,7 @@ async def websocket_remove_node(
|
|||
controller.on("node removed", node_removed),
|
||||
]
|
||||
|
||||
result = await controller.async_begin_exclusion()
|
||||
result = await controller.async_begin_exclusion(msg.get(UNPROVISION))
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
result,
|
||||
|
@ -720,9 +1042,24 @@ async def websocket_remove_node(
|
|||
vol.Required(TYPE): "zwave_js/replace_failed_node",
|
||||
vol.Required(ENTRY_ID): str,
|
||||
vol.Required(NODE_ID): int,
|
||||
vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.In(
|
||||
[strategy.value for strategy in InclusionStrategy]
|
||||
vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.In(
|
||||
[
|
||||
strategy.value
|
||||
for strategy in InclusionStrategy
|
||||
if strategy != InclusionStrategy.SMART_START
|
||||
]
|
||||
),
|
||||
),
|
||||
vol.Optional(FORCE_SECURITY): bool,
|
||||
vol.Exclusive(
|
||||
PLANNED_PROVISIONING_ENTRY, "options"
|
||||
): PLANNED_PROVISIONING_ENTRY_SCHEMA,
|
||||
vol.Exclusive(
|
||||
QR_PROVISIONING_INFORMATION, "options"
|
||||
): QR_PROVISIONING_INFORMATION_SCHEMA,
|
||||
vol.Exclusive(QR_CODE_STRING, "options"): QR_CODE_STRING_SCHEMA,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
|
@ -739,6 +1076,12 @@ async def websocket_replace_failed_node(
|
|||
controller = client.driver.controller
|
||||
node_id = msg[NODE_ID]
|
||||
inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY])
|
||||
force_security = msg.get(FORCE_SECURITY)
|
||||
provisioning = (
|
||||
msg.get(PLANNED_PROVISIONING_ENTRY)
|
||||
or msg.get(QR_PROVISIONING_INFORMATION)
|
||||
or msg.get(QR_CODE_STRING)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_cleanup() -> None:
|
||||
|
@ -842,7 +1185,21 @@ async def websocket_replace_failed_node(
|
|||
),
|
||||
]
|
||||
|
||||
result = await controller.async_replace_failed_node(node_id, inclusion_strategy)
|
||||
try:
|
||||
result = await controller.async_replace_failed_node(
|
||||
node_id,
|
||||
inclusion_strategy,
|
||||
force_security=force_security,
|
||||
provisioning=provisioning,
|
||||
)
|
||||
except ValueError as err:
|
||||
connection.send_error(
|
||||
msg[ID],
|
||||
ERR_INVALID_FORMAT,
|
||||
err.args[0],
|
||||
)
|
||||
return
|
||||
|
||||
connection.send_result(
|
||||
msg[ID],
|
||||
result,
|
||||
|
@ -1309,13 +1666,12 @@ async def websocket_subscribe_log_updates(
|
|||
{
|
||||
vol.Optional(ENABLED): cv.boolean,
|
||||
vol.Optional(LEVEL): vol.All(
|
||||
cv.string,
|
||||
str,
|
||||
vol.Lower,
|
||||
vol.In([log_level.value for log_level in LogLevel]),
|
||||
lambda val: LogLevel(val), # pylint: disable=unnecessary-lambda
|
||||
vol.Coerce(LogLevel),
|
||||
),
|
||||
vol.Optional(LOG_TO_FILE): cv.boolean,
|
||||
vol.Optional(FILENAME): cv.string,
|
||||
vol.Optional(FILENAME): str,
|
||||
vol.Optional(FORCE_CONSOLE): cv.boolean,
|
||||
}
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue