From d68fdbc283ed257684477975cea94d7a14031bbb Mon Sep 17 00:00:00 2001
From: Charles Garwood <cgarwood@gmail.com>
Date: Sun, 10 Jan 2021 18:08:25 -0500
Subject: [PATCH] Add zwave_js integration (#45020)

* Run zwave_js scaffold (#44891)

* Add zwave_js basic connection to zwave server (#44904)

* add the basic connection to zwave server

* fix name

* Fix requirements

* Fix things

* Version bump dep to 0.1.2

* fix pylint

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Bump zwave-js-server-python to 0.2.0

* Use zwave js server version check instead of fetching full state (#44943)

* Use version check instead of fetching full state

* Fix tests

* Use 0.3.0

* Also catch aiohttp client errors

* Update docstring

* Lint

* Unignore zwave_js

* Add zwave_js entity discovery basics and sensor platform (#44927)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Complete zwave_js typing (#44960)

* Type discovery

* Type init

* Type entity

* Type config flow

* Type sensor

* Require typing of zwave_js

* Complete zwave_js config flow test coverage (#44955)

* Correct zwave_js sensor device class (#44968)

* Fix zwave_js KeyError on entry setup timeout (#44966)

* Bump zwave-js-server-python to 0.5.0 (#44975)

* Remove stale callback signal from zwave_js (#44994)

* Add light platform to zwave_js integration (#44974)

* add light platform

* styling fix

* fix type hint

* Fix typing

* Update homeassistant/components/zwave_js/const.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/entity.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* color temp should be integer

* guard Nonetype error

* Update homeassistant/components/zwave_js/light.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/zwave_js/light.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* some fixes after merging

* add additional guards for None values

* adjustments for rgb lights

* Fix typing

* Fix black

* Bump zwave-js-server-python to 0.6.0

* guard value updated log

* remove value_id lookup as its no longer needed

* fiz sending white value

* Update homeassistant/components/zwave_js/light.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add zwave_js test foundation (#44983)

* Exclude text files from codespell

* Add basic dump fixture

* Add test foundation

* Fix test after rebase

* Exclude jsonl files from codespell

* Rename fixture file type to jsonl

* Update fixture path

* Fix stale docstring

* Add controller state json fixture

* Add multisensor 6 state json fixture

* Update fixtures

* Remove basic dump fixture

* Fix fixtures after library bump

* Update codeowner

* Minor cleanup Z-Wave JS (#45021)

* Update zwave_js device_info (#45023)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Marcel van der Veldt <m.vanderveldt@outlook.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
---
 .coveragerc                                   |    5 +
 .pre-commit-config.yaml                       |    1 +
 CODEOWNERS                                    |    1 +
 homeassistant/components/zwave_js/__init__.py |  179 ++
 .../components/zwave_js/config_flow.py        |   78 +
 homeassistant/components/zwave_js/const.py    |    9 +
 .../components/zwave_js/discovery.py          |  160 ++
 homeassistant/components/zwave_js/entity.py   |  151 ++
 homeassistant/components/zwave_js/light.py    |  322 +++
 .../components/zwave_js/manifest.json         |    8 +
 homeassistant/components/zwave_js/sensor.py   |  149 ++
 .../components/zwave_js/strings.json          |   20 +
 .../components/zwave_js/translations/en.json  |   20 +
 homeassistant/generated/config_flows.py       |    3 +-
 requirements_all.txt                          |    3 +
 requirements_test_all.txt                     |    3 +
 setup.cfg                                     |    2 +-
 tests/components/zwave_js/__init__.py         |    1 +
 tests/components/zwave_js/conftest.py         |   58 +
 tests/components/zwave_js/test_config_flow.py |   99 +
 tests/components/zwave_js/test_sensor.py      |   14 +
 tests/fixtures/zwave_js/controller_state.json |   98 +
 .../zwave_js/multisensor_6_state.json         | 1830 +++++++++++++++++
 23 files changed, 3212 insertions(+), 2 deletions(-)
 create mode 100644 homeassistant/components/zwave_js/__init__.py
 create mode 100644 homeassistant/components/zwave_js/config_flow.py
 create mode 100644 homeassistant/components/zwave_js/const.py
 create mode 100644 homeassistant/components/zwave_js/discovery.py
 create mode 100644 homeassistant/components/zwave_js/entity.py
 create mode 100644 homeassistant/components/zwave_js/light.py
 create mode 100644 homeassistant/components/zwave_js/manifest.json
 create mode 100644 homeassistant/components/zwave_js/sensor.py
 create mode 100644 homeassistant/components/zwave_js/strings.json
 create mode 100644 homeassistant/components/zwave_js/translations/en.json
 create mode 100644 tests/components/zwave_js/__init__.py
 create mode 100644 tests/components/zwave_js/conftest.py
 create mode 100644 tests/components/zwave_js/test_config_flow.py
 create mode 100644 tests/components/zwave_js/test_sensor.py
 create mode 100644 tests/fixtures/zwave_js/controller_state.json
 create mode 100644 tests/fixtures/zwave_js/multisensor_6_state.json

diff --git a/.coveragerc b/.coveragerc
index 2b08fc50336..9a8b062468f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1092,6 +1092,11 @@ omit =
     homeassistant/components/zoneminder/*
     homeassistant/components/supla/*
     homeassistant/components/zwave/util.py
+    homeassistant/components/zwave_js/__init__.py
+    homeassistant/components/zwave_js/discovery.py
+    homeassistant/components/zwave_js/entity.py
+    homeassistant/components/zwave_js/light.py
+    homeassistant/components/zwave_js/sensor.py
 
 [report]
 # Regexes for lines to exclude from consideration
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 087dc914035..00f2373f2db 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -21,6 +21,7 @@ repos:
           - --skip="./.*,*.csv,*.json"
           - --quiet-level=2
         exclude_types: [csv, json]
+        exclude: ^tests/fixtures/
   - repo: https://gitlab.com/pycqa/flake8
     rev: 3.8.4
     hooks:
diff --git a/CODEOWNERS b/CODEOWNERS
index 9f6e08216b5..d81247ce629 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -537,6 +537,7 @@ homeassistant/components/zodiac/* @JulienTant
 homeassistant/components/zone/* @home-assistant/core
 homeassistant/components/zoneminder/* @rohankapoorcom
 homeassistant/components/zwave/* @home-assistant/z-wave
+homeassistant/components/zwave_js/* @home-assistant/z-wave
 
 # Individual files
 homeassistant/components/demo/weather @fabaff
diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py
new file mode 100644
index 00000000000..605235f61ba
--- /dev/null
+++ b/homeassistant/components/zwave_js/__init__.py
@@ -0,0 +1,179 @@
+"""The Z-Wave JS integration."""
+import asyncio
+import logging
+
+from async_timeout import timeout
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.model.node import Node as ZwaveNode
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
+from homeassistant.core import Event, HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS
+from .discovery import async_discover_values
+
+LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+    """Set up the Z-Wave JS component."""
+    hass.data[DOMAIN] = {}
+    return True
+
+
+@callback
+def register_node_in_dev_reg(
+    entry: ConfigEntry,
+    dev_reg: device_registry.DeviceRegistry,
+    client: ZwaveClient,
+    node: ZwaveNode,
+) -> None:
+    """Register node in dev reg."""
+    dev_reg.async_get_or_create(
+        config_entry_id=entry.entry_id,
+        identifiers={(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")},
+        sw_version=node.firmware_version,
+        name=node.name or node.device_config.description,
+        model=node.device_config.label or str(node.product_type),
+        manufacturer=node.device_config.manufacturer or str(node.manufacturer_id),
+    )
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Set up Z-Wave JS from a config entry."""
+    client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass))
+    initialized = asyncio.Event()
+    dev_reg = await device_registry.async_get_registry(hass)
+
+    async def async_on_connect() -> None:
+        """Handle websocket is (re)connected."""
+        LOGGER.info("Connected to Zwave JS Server")
+        if initialized.is_set():
+            # update entity availability
+            async_dispatcher_send(hass, f"{DOMAIN}_connection_state")
+
+    async def async_on_disconnect() -> None:
+        """Handle websocket is disconnected."""
+        LOGGER.info("Disconnected from Zwave JS Server")
+        async_dispatcher_send(hass, f"{DOMAIN}_connection_state")
+
+    async def async_on_initialized() -> None:
+        """Handle initial full state received."""
+        LOGGER.info("Connection to Zwave JS Server initialized.")
+        initialized.set()
+
+    @callback
+    def async_on_node_ready(node: ZwaveNode) -> None:
+        """Handle node ready event."""
+        LOGGER.debug("Processing node %s", node)
+
+        # register (or update) node in device registry
+        register_node_in_dev_reg(entry, dev_reg, client, node)
+
+        # run discovery on all node values and create/update entities
+        for disc_info in async_discover_values(node):
+            LOGGER.debug("Discovered entity: %s", disc_info)
+            async_dispatcher_send(hass, f"{DOMAIN}_add_{disc_info.platform}", disc_info)
+
+    @callback
+    def async_on_node_added(node: ZwaveNode) -> None:
+        """Handle node added event."""
+        LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id)
+        # we only want to run discovery when the node has reached ready state,
+        # otherwise we'll have all kinds of missing info issues.
+        if node.ready:
+            async_on_node_ready(node)
+            return
+        # if node is not yet ready, register one-time callback for ready state
+        node.once(
+            "ready",
+            lambda event: async_on_node_ready(event["node"]),
+        )
+        # we do submit the node to device registry so user has
+        # some visual feedback that something is (in the process of) being added
+        register_node_in_dev_reg(entry, dev_reg, client, node)
+
+    async def handle_ha_shutdown(event: Event) -> None:
+        """Handle HA shutdown."""
+        await client.disconnect()
+
+    # register main event callbacks.
+    unsubs = [
+        client.register_on_initialized(async_on_initialized),
+        client.register_on_disconnect(async_on_disconnect),
+        client.register_on_connect(async_on_connect),
+        hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown),
+    ]
+
+    # connect and throw error if connection failed
+    asyncio.create_task(client.connect())
+    try:
+        async with timeout(10):
+            await initialized.wait()
+    except asyncio.TimeoutError as err:
+        for unsub in unsubs:
+            unsub()
+        await client.disconnect()
+        raise ConfigEntryNotReady from err
+
+    hass.data[DOMAIN][entry.entry_id] = {
+        DATA_CLIENT: client,
+        DATA_UNSUBSCRIBE: unsubs,
+    }
+
+    async def start_platforms() -> None:
+        """Start platforms and perform discovery."""
+        # wait until all required platforms are ready
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_setup(entry, component)
+                for component in PLATFORMS
+            ]
+        )
+
+        # run discovery on all ready nodes
+        for node in client.driver.controller.nodes.values():
+            if node.ready:
+                async_on_node_ready(node)
+                continue
+            # if node is not yet ready, register one-time callback for ready state
+            node.once(
+                "ready",
+                lambda event: async_on_node_ready(event["node"]),
+            )
+        # listen for new nodes being added to the mesh
+        client.driver.controller.on(
+            "node added", lambda event: async_on_node_added(event["node"])
+        )
+
+    hass.async_create_task(start_platforms())
+
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+    """Unload a config entry."""
+    unload_ok = all(
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_unload(entry, component)
+                for component in PLATFORMS
+            ]
+        )
+    )
+    if not unload_ok:
+        return False
+
+    info = hass.data[DOMAIN].pop(entry.entry_id)
+
+    for unsub in info[DATA_UNSUBSCRIBE]:
+        unsub()
+
+    await info[DATA_CLIENT].disconnect()
+
+    return True
diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py
new file mode 100644
index 00000000000..2edb7012878
--- /dev/null
+++ b/homeassistant/components/zwave_js/config_flow.py
@@ -0,0 +1,78 @@
+"""Config flow for Z-Wave JS integration."""
+import asyncio
+import logging
+from typing import Any, Dict, Optional
+
+import aiohttp
+from async_timeout import timeout
+import voluptuous as vol
+from zwave_js_server.version import VersionInfo, get_server_version
+
+from homeassistant import config_entries, core, exceptions
+from homeassistant.const import CONF_URL
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DOMAIN, NAME  # pylint:disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+STEP_USER_DATA_SCHEMA = vol.Schema({CONF_URL: str})
+
+
+async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo:
+    """Validate if the user input allows us to connect."""
+    ws_address = user_input[CONF_URL]
+
+    if not ws_address.startswith(("ws://", "wss://")):
+        raise InvalidInput("invalid_ws_url")
+
+    async with timeout(10):
+        try:
+            return await get_server_version(ws_address, async_get_clientsession(hass))
+        except (asyncio.TimeoutError, aiohttp.ClientError) as err:
+            raise InvalidInput("cannot_connect") from err
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for Z-Wave JS."""
+
+    VERSION = 1
+    CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+    async def async_step_user(
+        self, user_input: Optional[Dict[str, Any]] = None
+    ) -> Dict[str, Any]:
+        """Handle the initial step."""
+        if user_input is None:
+            return self.async_show_form(
+                step_id="user", data_schema=STEP_USER_DATA_SCHEMA
+            )
+
+        errors = {}
+
+        assert self.hass  # typing
+
+        try:
+            version_info = await validate_input(self.hass, user_input)
+        except InvalidInput as err:
+            errors["base"] = err.error
+        except Exception:  # pylint: disable=broad-except
+            _LOGGER.exception("Unexpected exception")
+            errors["base"] = "unknown"
+        else:
+            await self.async_set_unique_id(version_info.home_id)
+            self._abort_if_unique_id_configured(user_input)
+            return self.async_create_entry(title=NAME, data=user_input)
+
+        return self.async_show_form(
+            step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
+        )
+
+
+class InvalidInput(exceptions.HomeAssistantError):
+    """Error to indicate input data is invalid."""
+
+    def __init__(self, error: str) -> None:
+        """Initialize error."""
+        super().__init__()
+        self.error = error
diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py
new file mode 100644
index 00000000000..c2a9ac7b3cf
--- /dev/null
+++ b/homeassistant/components/zwave_js/const.py
@@ -0,0 +1,9 @@
+"""Constants for the Z-Wave JS integration."""
+
+
+DOMAIN = "zwave_js"
+NAME = "Z-Wave JS"
+PLATFORMS = ["light", "sensor"]
+
+DATA_CLIENT = "client"
+DATA_UNSUBSCRIBE = "unsubs"
diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py
new file mode 100644
index 00000000000..34f715a2d40
--- /dev/null
+++ b/homeassistant/components/zwave_js/discovery.py
@@ -0,0 +1,160 @@
+"""Map Z-Wave nodes and values to Home Assistant entities."""
+
+from dataclasses import dataclass
+from typing import Generator, Optional, Set, Union
+
+from zwave_js_server.const import CommandClass
+from zwave_js_server.model.node import Node as ZwaveNode
+from zwave_js_server.model.value import Value as ZwaveValue
+
+from homeassistant.core import callback
+
+
+@dataclass
+class ZwaveDiscoveryInfo:
+    """Info discovered from (primary) ZWave Value to create entity."""
+
+    node: ZwaveNode  # node to which the value(s) belongs
+    primary_value: ZwaveValue  # the value object itself for primary value
+    platform: str  # the home assistant platform for which an entity should be created
+    platform_hint: Optional[
+        str
+    ] = ""  # hint for the platform about this discovered entity
+
+    @property
+    def value_id(self) -> str:
+        """Return the unique value_id belonging to primary value."""
+        return f"{self.node.node_id}.{self.primary_value.value_id}"
+
+
+@dataclass
+class ZWaveDiscoverySchema:
+    """Z-Wave discovery schema.
+
+    The (primary) value for an entity must match these conditions.
+    Use the Z-Wave specifications to find out the values for these parameters:
+    https://github.com/zwave-js/node-zwave-js/tree/master/specs
+    """
+
+    # specify the hass platform for which this scheme applies (e.g. light, sensor)
+    platform: str
+    # [optional] hint for platform
+    hint: Optional[str] = None
+    # [optional] the node's basic device class must match ANY of these values
+    device_class_basic: Optional[Set[str]] = None
+    # [optional] the node's generic device class must match ANY of these values
+    device_class_generic: Optional[Set[str]] = None
+    # [optional] the node's specific device class must match ANY of these values
+    device_class_specific: Optional[Set[str]] = None
+    # [optional] the value's command class must match ANY of these values
+    command_class: Optional[Set[int]] = None
+    # [optional] the value's endpoint must match ANY of these values
+    endpoint: Optional[Set[int]] = None
+    # [optional] the value's property must match ANY of these values
+    property: Optional[Set[Union[str, int]]] = None
+    # [optional] the value's metadata_type must match ANY of these values
+    type: Optional[Set[str]] = None
+
+
+DISCOVERY_SCHEMAS = [
+    # light
+    # primary value is the currentValue (brightness)
+    ZWaveDiscoverySchema(
+        platform="light",
+        device_class_generic={"Multilevel Switch", "Remote Switch"},
+        device_class_specific={
+            "Multilevel Tunable Color Light",
+            "Binary Tunable Color Light",
+            "Multilevel Remote Switch",
+            "Multilevel Power Switch",
+        },
+        command_class={CommandClass.SWITCH_MULTILEVEL},
+        property={"currentValue"},
+        type={"number"},
+    ),
+    # generic text sensors
+    ZWaveDiscoverySchema(
+        platform="sensor",
+        hint="string_sensor",
+        command_class={
+            CommandClass.ALARM,
+            CommandClass.SENSOR_ALARM,
+            CommandClass.INDICATOR,
+            CommandClass.NOTIFICATION,
+        },
+        type={"string"},
+    ),
+    # generic numeric sensors
+    ZWaveDiscoverySchema(
+        platform="sensor",
+        hint="numeric_sensor",
+        command_class={
+            CommandClass.SENSOR_MULTILEVEL,
+            CommandClass.METER,
+            CommandClass.ALARM,
+            CommandClass.SENSOR_ALARM,
+            CommandClass.INDICATOR,
+            CommandClass.BATTERY,
+            CommandClass.NOTIFICATION,
+            CommandClass.BASIC,
+        },
+        type={"number"},
+    ),
+]
+
+
+@callback
+def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]:
+    """Run discovery on ZWave node and return matching (primary) values."""
+    for value in node.values.values():
+        disc_val = async_discover_value(value)
+        if disc_val:
+            yield disc_val
+
+
+@callback
+def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]:
+    """Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found."""
+    for schema in DISCOVERY_SCHEMAS:
+        # check device_class_basic
+        if (
+            schema.device_class_basic is not None
+            and value.node.device_class.basic not in schema.device_class_basic
+        ):
+            continue
+        # check device_class_generic
+        if (
+            schema.device_class_generic is not None
+            and value.node.device_class.generic not in schema.device_class_generic
+        ):
+            continue
+        # check device_class_specific
+        if (
+            schema.device_class_specific is not None
+            and value.node.device_class.specific not in schema.device_class_specific
+        ):
+            continue
+        # check command_class
+        if (
+            schema.command_class is not None
+            and value.command_class not in schema.command_class
+        ):
+            continue
+        # check endpoint
+        if schema.endpoint is not None and value.endpoint not in schema.endpoint:
+            continue
+        # check property
+        if schema.property is not None and value.property_ not in schema.property:
+            continue
+        # check metadata_type
+        if schema.type is not None and value.metadata.type not in schema.type:
+            continue
+        # all checks passed, this value belongs to an entity
+        return ZwaveDiscoveryInfo(
+            node=value.node,
+            primary_value=value,
+            platform=schema.platform,
+            platform_hint=schema.hint,
+        )
+
+    return None
diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py
new file mode 100644
index 00000000000..70630cbd89c
--- /dev/null
+++ b/homeassistant/components/zwave_js/entity.py
@@ -0,0 +1,151 @@
+"""Generic Z-Wave Entity Class."""
+
+import logging
+from typing import Optional, Union
+
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.model.value import Value as ZwaveValue, get_value_id
+
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+
+from .const import DOMAIN
+from .discovery import ZwaveDiscoveryInfo
+
+LOGGER = logging.getLogger(__name__)
+
+EVENT_VALUE_UPDATED = "value updated"
+
+
+class ZWaveBaseEntity(Entity):
+    """Generic Entity Class for a Z-Wave Device."""
+
+    def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None:
+        """Initialize a generic Z-Wave device entity."""
+        self.client = client
+        self.info = info
+        # entities requiring additional values, can add extra ids to this list
+        self.watched_value_ids = {self.info.primary_value.value_id}
+
+    @callback
+    def on_value_update(self) -> None:
+        """Call when one of the watched values change.
+
+        To be overridden by platforms needing this event.
+        """
+
+    async def async_added_to_hass(self) -> None:
+        """Call when entity is added."""
+        assert self.hass  # typing
+        # Add value_changed callbacks.
+        self.async_on_remove(
+            self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed)
+        )
+        self.async_on_remove(
+            async_dispatcher_connect(
+                self.hass, f"{DOMAIN}_connection_state", self.async_write_ha_state
+            )
+        )
+
+    @property
+    def device_info(self) -> dict:
+        """Return device information for the device registry."""
+        # device is precreated in main handler
+        return {
+            "identifiers": {
+                (
+                    DOMAIN,
+                    f"{self.client.driver.controller.home_id}-{self.info.node.node_id}",
+                )
+            },
+        }
+
+    @property
+    def name(self) -> str:
+        """Return default name from device name and value name combination."""
+        node_name = self.info.node.name or self.info.node.device_config.description
+        value_name = (
+            self.info.primary_value.metadata.label
+            or self.info.primary_value.property_key_name
+            or self.info.primary_value.property_name
+        )
+        return f"{node_name}: {value_name}"
+
+    @property
+    def unique_id(self) -> str:
+        """Return the unique_id of the entity."""
+        return f"{self.client.driver.controller.home_id}.{self.info.value_id}"
+
+    @property
+    def available(self) -> bool:
+        """Return entity availability."""
+        return self.client.connected and bool(self.info.node.ready)
+
+    @callback
+    def _value_changed(self, event_data: Union[dict, ZwaveValue]) -> None:
+        """Call when (one of) our watched values changes.
+
+        Should not be overridden by subclasses.
+        """
+        if isinstance(event_data, ZwaveValue):
+            value_id = event_data.value_id
+        else:
+            value_id = event_data["value"].value_id
+
+        if value_id not in self.watched_value_ids:
+            return
+
+        value = self.info.node.values[value_id]
+
+        LOGGER.debug(
+            "[%s] Value %s/%s changed to: %s",
+            self.entity_id,
+            value.property_,
+            value.property_key_name,
+            value.value,
+        )
+
+        self.on_value_update()
+        self.async_write_ha_state()
+
+    @callback
+    def get_zwave_value(
+        self,
+        value_property: Union[str, int],
+        command_class: Optional[int] = None,
+        endpoint: Optional[int] = None,
+        value_property_key_name: Optional[str] = None,
+        add_to_watched_value_ids: bool = True,
+    ) -> Optional[ZwaveValue]:
+        """Return specific ZwaveValue on this ZwaveNode."""
+        # use commandclass and endpoint from primary value if omitted
+        return_value = None
+        if command_class is None:
+            command_class = self.info.primary_value.command_class
+        if endpoint is None:
+            endpoint = self.info.primary_value.endpoint
+        # lookup value by value_id
+        value_id = get_value_id(
+            self.info.node,
+            {
+                "commandClass": command_class,
+                "endpoint": endpoint,
+                "property": value_property,
+                "propertyKeyName": value_property_key_name,
+            },
+        )
+        return_value = self.info.node.values.get(value_id)
+        # add to watched_ids list so we will be triggered when the value updates
+        if (
+            return_value
+            and return_value.value_id not in self.watched_value_ids
+            and add_to_watched_value_ids
+        ):
+            self.watched_value_ids.add(return_value.value_id)
+        return return_value
+
+    @property
+    def should_poll(self) -> bool:
+        """No polling needed."""
+        return False
diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py
new file mode 100644
index 00000000000..45bd68aef81
--- /dev/null
+++ b/homeassistant/components/zwave_js/light.py
@@ -0,0 +1,322 @@
+"""Support for Z-Wave lights."""
+import logging
+from typing import Any, Callable, List, Optional
+
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.const import CommandClass
+
+from homeassistant.components.light import (
+    ATTR_BRIGHTNESS,
+    ATTR_COLOR_TEMP,
+    ATTR_HS_COLOR,
+    ATTR_TRANSITION,
+    ATTR_WHITE_VALUE,
+    DOMAIN as LIGHT_DOMAIN,
+    SUPPORT_BRIGHTNESS,
+    SUPPORT_COLOR,
+    SUPPORT_COLOR_TEMP,
+    SUPPORT_TRANSITION,
+    SUPPORT_WHITE_VALUE,
+    LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+import homeassistant.util.color as color_util
+
+from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
+from .discovery import ZwaveDiscoveryInfo
+from .entity import ZWaveBaseEntity
+
+LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
+) -> None:
+    """Set up Z-Wave Light from Config Entry."""
+    client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
+
+    @callback
+    def async_add_light(info: ZwaveDiscoveryInfo) -> None:
+        """Add Z-Wave Light."""
+
+        light = ZwaveLight(client, info)
+        async_add_entities([light])
+
+    hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
+        async_dispatcher_connect(hass, f"{DOMAIN}_add_{LIGHT_DOMAIN}", async_add_light)
+    )
+
+
+def byte_to_zwave_brightness(value: int) -> int:
+    """Convert brightness in 0-255 scale to 0-99 scale.
+
+    `value` -- (int) Brightness byte value from 0-255.
+    """
+    if value > 0:
+        return max(1, round((value / 255) * 99))
+    return 0
+
+
+class ZwaveLight(ZWaveBaseEntity, LightEntity):
+    """Representation of a Z-Wave light."""
+
+    def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None:
+        """Initialize the light."""
+        super().__init__(client, info)
+        self._supports_color = False
+        self._supports_white_value = False
+        self._supports_color_temp = False
+        self._hs_color: Optional[List[float]] = None
+        self._white_value: Optional[int] = None
+        self._color_temp: Optional[int] = None
+        self._min_mireds = 153  # 6500K as a safe default
+        self._max_mireds = 370  # 2700K as a safe default
+        self._supported_features = SUPPORT_BRIGHTNESS
+
+        # get additional (optional) values and set features
+        self._target_value = self.get_zwave_value("targetValue")
+        self._dimming_duration = self.get_zwave_value("duration")
+        if self._dimming_duration is not None:
+            self._supported_features |= SUPPORT_TRANSITION
+        self._calculate_color_values()
+        if self._supports_color:
+            self._supported_features |= SUPPORT_COLOR
+        if self._supports_color_temp:
+            self._supported_features |= SUPPORT_COLOR_TEMP
+        if self._supports_white_value:
+            self._supported_features |= SUPPORT_WHITE_VALUE
+
+    @callback
+    def on_value_update(self) -> None:
+        """Call when a watched value is added or updated."""
+        self._calculate_color_values()
+
+    @property
+    def brightness(self) -> int:
+        """Return the brightness of this light between 0..255.
+
+        Z-Wave multilevel switches use a range of [0, 99] to control brightness.
+        """
+        if self._target_value is not None and self._target_value.value is not None:
+            return round((self._target_value.value / 99) * 255)
+        if self.info.primary_value.value is not None:
+            return round((self.info.primary_value.value / 99) * 255)
+        return 0
+
+    @property
+    def is_on(self) -> bool:
+        """Return true if device is on (brightness above 0)."""
+        return self.brightness > 0
+
+    @property
+    def hs_color(self) -> Optional[List[float]]:
+        """Return the hs color."""
+        return self._hs_color
+
+    @property
+    def white_value(self) -> Optional[int]:
+        """Return the white value of this light between 0..255."""
+        return self._white_value
+
+    @property
+    def color_temp(self) -> Optional[int]:
+        """Return the color temperature."""
+        return self._color_temp
+
+    @property
+    def min_mireds(self) -> int:
+        """Return the coldest color_temp that this light supports."""
+        return self._min_mireds
+
+    @property
+    def max_mireds(self) -> int:
+        """Return the warmest color_temp that this light supports."""
+        return self._max_mireds
+
+    @property
+    def supported_features(self) -> Optional[int]:
+        """Flag supported features."""
+        return self._supported_features
+
+    async def async_turn_on(self, **kwargs: Any) -> None:
+        """Turn the device on."""
+        # RGB/HS color
+        hs_color = kwargs.get(ATTR_HS_COLOR)
+        if hs_color is not None and self._supports_color:
+            # set white levels to 0 when setting rgb
+            await self._async_set_color("Warm White", 0)
+            await self._async_set_color("Cold White", 0)
+            red, green, blue = color_util.color_hs_to_RGB(*hs_color)
+            await self._async_set_color("Red", red)
+            await self._async_set_color("Green", green)
+            await self._async_set_color("Blue", blue)
+        else:
+            # turn off rgb when setting white values
+            await self._async_set_color("Red", 0)
+            await self._async_set_color("Green", 0)
+            await self._async_set_color("Blue", 0)
+
+        # Color temperature
+        color_temp = kwargs.get(ATTR_COLOR_TEMP)
+        if color_temp is not None and self._supports_color_temp:
+            # Limit color temp to min/max values
+            cold = max(
+                0,
+                min(
+                    255,
+                    round(
+                        (self._max_mireds - color_temp)
+                        / (self._max_mireds - self._min_mireds)
+                        * 255
+                    ),
+                ),
+            )
+            warm = 255 - cold
+            await self._async_set_color("Warm White", warm)
+            await self._async_set_color("Cold White", cold)
+
+        # White value
+        white_value = kwargs.get(ATTR_WHITE_VALUE)
+        if white_value is not None and self._supports_white_value:
+            await self._async_set_color("Warm White", white_value)
+
+        # set brightness
+        await self._async_set_brightness(
+            kwargs.get(ATTR_BRIGHTNESS), kwargs.get(ATTR_TRANSITION)
+        )
+
+    async def async_turn_off(self, **kwargs: Any) -> None:
+        """Turn the light off."""
+        await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION))
+
+    async def _async_set_color(self, color_name: str, new_value: int) -> None:
+        """Set defined color to given value."""
+        cur_zwave_value = self.get_zwave_value(
+            "currentColor",
+            CommandClass.SWITCH_COLOR,
+            value_property_key_name=color_name,
+        )
+        # guard for unsupported command
+        if cur_zwave_value is None:
+            return
+        # no need to send same value
+        if cur_zwave_value.value == new_value:
+            return
+        # actually set the new color value
+        target_zwave_value = self.get_zwave_value(
+            "targetColor",
+            CommandClass.SWITCH_COLOR,
+            value_property_key_name=color_name,
+        )
+        if target_zwave_value is None:
+            return
+        await self.info.node.async_set_value(target_zwave_value, new_value)
+
+    async def _async_set_brightness(
+        self, brightness: Optional[int], transition: Optional[int] = None
+    ) -> None:
+        """Set new brightness to light."""
+        if self.info.primary_value.value == brightness:
+            # no point in setting same brightness
+            return
+        if brightness is None and self.info.primary_value.value:
+            # there is no point in setting default brightness when light is already on
+            return
+        if brightness is None:
+            # Level 255 means to set it to previous value.
+            brightness = 255
+        else:
+            # Zwave multilevel switches use a range of [0, 99] to control brightness.
+            brightness = byte_to_zwave_brightness(brightness)
+        # set transition value before seinding new brightness
+        await self._async_set_transition_duration(transition)
+        # setting a value requires setting targetValue
+        await self.info.node.async_set_value(self._target_value, brightness)
+
+    async def _async_set_transition_duration(
+        self, duration: Optional[int] = None
+    ) -> None:
+        """Set the transition time for the brightness value."""
+        if self._dimming_duration is None:
+            return
+        # pylint: disable=fixme,unreachable
+        # TODO: setting duration needs to be fixed upstream
+        # https://github.com/zwave-js/node-zwave-js/issues/1321
+        return
+
+        if duration is None:  # type: ignore
+            # no transition specified by user, use defaults
+            duration = 7621  # anything over 7620 uses the factory default
+        else:
+            # transition specified by user
+            transition = duration
+            if transition <= 127:
+                duration = transition
+            else:
+                minutes = round(transition / 60)
+                LOGGER.debug(
+                    "Transition rounded to %d minutes for %s",
+                    minutes,
+                    self.entity_id,
+                )
+                duration = minutes + 128
+
+        # only send value if it differs from current
+        # this prevents sending a command for nothing
+        if self._dimming_duration.value != duration:
+            await self.info.node.async_set_value(self._dimming_duration, duration)
+
+    @callback
+    def _calculate_color_values(self) -> None:
+        """Calculate light colors."""
+
+        # RGB support
+        red_val = self.get_zwave_value(
+            "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red"
+        )
+        green_val = self.get_zwave_value(
+            "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green"
+        )
+        blue_val = self.get_zwave_value(
+            "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue"
+        )
+        if red_val and green_val and blue_val:
+            self._supports_color = True
+            # convert to HS
+            if (
+                red_val.value is not None
+                and green_val.value is not None
+                and blue_val.value is not None
+            ):
+                self._hs = color_util.color_RGB_to_hs(
+                    red_val.value, green_val.value, blue_val.value
+                )
+
+        # White colors
+        ww_val = self.get_zwave_value(
+            "currentColor",
+            CommandClass.SWITCH_COLOR,
+            value_property_key_name="Warm White",
+        )
+        cw_val = self.get_zwave_value(
+            "currentColor",
+            CommandClass.SWITCH_COLOR,
+            value_property_key_name="Cold White",
+        )
+        if ww_val and cw_val:
+            # Color temperature (CW + WW) Support
+            self._supports_color_temp = True
+            # Calculate color temps based on whites
+            cold_level = cw_val.value or 0
+            if cold_level or ww_val.value is not None:
+                self._color_temp = round(
+                    self._max_mireds
+                    - ((cold_level / 255) * (self._max_mireds - self._min_mireds))
+                )
+            else:
+                self._color_temp = None
+        elif ww_val or cw_val:
+            # only one white channel
+            self._supports_white_value = True
diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json
new file mode 100644
index 00000000000..718a4da2b85
--- /dev/null
+++ b/homeassistant/components/zwave_js/manifest.json
@@ -0,0 +1,8 @@
+{
+  "domain": "zwave_js",
+  "name": "Z-Wave JS",
+  "config_flow": true,
+  "documentation": "https://www.home-assistant.io/integrations/zwave_js",
+  "requirements": ["zwave-js-server-python==0.6.0"],
+  "codeowners": ["@home-assistant/z-wave"]
+}
diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py
new file mode 100644
index 00000000000..ef1a68bb7a7
--- /dev/null
+++ b/homeassistant/components/zwave_js/sensor.py
@@ -0,0 +1,149 @@
+"""Representation of Z-Wave sensors."""
+
+import logging
+from typing import Callable, Dict, List, Optional
+
+from zwave_js_server.client import Client as ZwaveClient
+from zwave_js_server.const import CommandClass
+
+from homeassistant.components.sensor import (
+    DEVICE_CLASS_BATTERY,
+    DEVICE_CLASS_ENERGY,
+    DEVICE_CLASS_POWER,
+    DOMAIN as SENSOR_DOMAIN,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN
+from .discovery import ZwaveDiscoveryInfo
+from .entity import ZWaveBaseEntity
+
+LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+    hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
+) -> None:
+    """Set up Z-Wave sensor from config entry."""
+    client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
+
+    @callback
+    def async_add_sensor(info: ZwaveDiscoveryInfo) -> None:
+        """Add Z-Wave Sensor."""
+        entities: List[ZWaveBaseEntity] = []
+
+        if info.platform_hint == "string_sensor":
+            entities.append(ZWaveStringSensor(client, info))
+        elif info.platform_hint == "numeric_sensor":
+            entities.append(ZWaveNumericSensor(client, info))
+        else:
+            LOGGER.warning(
+                "Sensor not implemented for %s/%s",
+                info.platform_hint,
+                info.primary_value.propertyname,
+            )
+            return
+
+        async_add_entities(entities)
+
+    hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append(
+        async_dispatcher_connect(
+            hass, f"{DOMAIN}_add_{SENSOR_DOMAIN}", async_add_sensor
+        )
+    )
+
+
+class ZwaveSensorBase(ZWaveBaseEntity):
+    """Basic Representation of a Z-Wave sensor."""
+
+    @property
+    def device_class(self) -> Optional[str]:
+        """Return the device class of the sensor."""
+        if self.info.primary_value.command_class == CommandClass.BATTERY:
+            return DEVICE_CLASS_BATTERY
+        if self.info.primary_value.command_class == CommandClass.METER:
+            return DEVICE_CLASS_POWER
+        if self.info.primary_value.property_key_name == "W_Consumed":
+            return DEVICE_CLASS_POWER
+        if self.info.primary_value.property_key_name == "kWh_Consumed":
+            return DEVICE_CLASS_ENERGY
+        if self.info.primary_value.property_ == "Air temperature":
+            return DEVICE_CLASS_TEMPERATURE
+        return None
+
+    @property
+    def entity_registry_enabled_default(self) -> bool:
+        """Return if the entity should be enabled when first added to the entity registry."""
+        # We hide some of the more advanced sensors by default to not overwhelm users
+        if self.info.primary_value.command_class in [
+            CommandClass.BASIC,
+            CommandClass.INDICATOR,
+            CommandClass.NOTIFICATION,
+        ]:
+            return False
+        return True
+
+    @property
+    def force_update(self) -> bool:
+        """Force updates."""
+        return True
+
+
+class ZWaveStringSensor(ZwaveSensorBase):
+    """Representation of a Z-Wave String sensor."""
+
+    @property
+    def state(self) -> Optional[str]:
+        """Return state of the sensor."""
+        if self.info.primary_value.value is None:
+            return None
+        return str(self.info.primary_value.value)
+
+    @property
+    def unit_of_measurement(self) -> Optional[str]:
+        """Return unit of measurement the value is expressed in."""
+        if self.info.primary_value.metadata.unit is None:
+            return None
+        return str(self.info.primary_value.metadata.unit)
+
+
+class ZWaveNumericSensor(ZwaveSensorBase):
+    """Representation of a Z-Wave Numeric sensor."""
+
+    @property
+    def state(self) -> float:
+        """Return state of the sensor."""
+        if self.info.primary_value.value is None:
+            return 0
+        return round(float(self.info.primary_value.value), 2)
+
+    @property
+    def unit_of_measurement(self) -> Optional[str]:
+        """Return unit of measurement the value is expressed in."""
+        if self.info.primary_value.metadata.unit is None:
+            return None
+        if self.info.primary_value.metadata.unit == "C":
+            return TEMP_CELSIUS
+        if self.info.primary_value.metadata.unit == "F":
+            return TEMP_FAHRENHEIT
+
+        return str(self.info.primary_value.metadata.unit)
+
+    @property
+    def device_state_attributes(self) -> Optional[Dict[str, str]]:
+        """Return the device specific state attributes."""
+        if (
+            self.info.primary_value.value is None
+            or not self.info.primary_value.metadata.states
+        ):
+            return None
+        # add the value's label as property for multi-value (list) items
+        label = self.info.primary_value.metadata.states.get(
+            self.info.primary_value.value
+        ) or self.info.primary_value.metadata.states.get(
+            str(self.info.primary_value.value)
+        )
+        return {"label": label}
diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json
new file mode 100644
index 00000000000..29136a03f48
--- /dev/null
+++ b/homeassistant/components/zwave_js/strings.json
@@ -0,0 +1,20 @@
+{
+  "title": "Z-Wave JS",
+  "config": {
+    "step": {
+      "user": {
+        "data": {
+          "url": "[%key:common::config_flow::data::url%]"
+        }
+      }
+    },
+    "error": {
+      "invalid_ws_url": "Invalid websocket URL",
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+      "unknown": "[%key:common::config_flow::error::unknown%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+    }
+  }
+}
diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json
new file mode 100644
index 00000000000..13b2e736bae
--- /dev/null
+++ b/homeassistant/components/zwave_js/translations/en.json
@@ -0,0 +1,20 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Device is already configured"
+        },
+        "error": {
+            "cannot_connect": "Failed to connect",
+            "invalid_ws_url": "Invalid websocket URL",
+            "unknown": "Unexpected error"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "url": "URL"
+                }
+            }
+        }
+    },
+    "title": "Z-Wave JS"
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index b696e29964a..3218ab4baa5 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -240,5 +240,6 @@ FLOWS = [
     "yeelight",
     "zerproc",
     "zha",
-    "zwave"
+    "zwave",
+    "zwave_js"
 ]
diff --git a/requirements_all.txt b/requirements_all.txt
index a6d094d1592..40b29e579d5 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -2370,3 +2370,6 @@ zigpy==0.29.0
 
 # homeassistant.components.zoneminder
 zm-py==0.5.2
+
+# homeassistant.components.zwave_js
+zwave-js-server-python==0.6.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 6dc4f724a7b..b6a004aed2c 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1169,3 +1169,6 @@ zigpy-znp==0.3.0
 
 # homeassistant.components.zha
 zigpy==0.29.0
+
+# homeassistant.components.zwave_js
+zwave-js-server-python==0.6.0
diff --git a/setup.cfg b/setup.cfg
index 1fc973ef21c..4137554257f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,7 +42,7 @@ warn_redundant_casts = true
 warn_unused_configs = true
 
 
-[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
+[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
 strict = true
 ignore_errors = false
 warn_unreachable = true
diff --git a/tests/components/zwave_js/__init__.py b/tests/components/zwave_js/__init__.py
new file mode 100644
index 00000000000..bd4b740c856
--- /dev/null
+++ b/tests/components/zwave_js/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Z-Wave JS integration."""
diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py
new file mode 100644
index 00000000000..61e50d86a2d
--- /dev/null
+++ b/tests/components/zwave_js/conftest.py
@@ -0,0 +1,58 @@
+"""Provide common Z-Wave JS fixtures."""
+import json
+from unittest.mock import patch
+
+import pytest
+from zwave_js_server.model.driver import Driver
+from zwave_js_server.model.node import Node
+
+from tests.common import MockConfigEntry, load_fixture
+
+
+@pytest.fixture(name="controller_state", scope="session")
+def controller_state_fixture():
+    """Load the controller state fixture data."""
+    return json.loads(load_fixture("zwave_js/controller_state.json"))
+
+
+@pytest.fixture(name="multisensor_6_state", scope="session")
+def multisensor_6_state_fixture():
+    """Load the multisensor 6 node state fixture data."""
+    return json.loads(load_fixture("zwave_js/multisensor_6_state.json"))
+
+
+@pytest.fixture(name="client")
+def mock_client_fixture(controller_state):
+    """Mock a client."""
+    with patch(
+        "homeassistant.components.zwave_js.ZwaveClient", autospec=True
+    ) as client_class:
+        driver = Driver(client_class.return_value, controller_state)
+        client_class.return_value.driver = driver
+        yield client_class.return_value
+
+
+@pytest.fixture(name="multisensor_6")
+def multisensor_6_fixture(client, multisensor_6_state):
+    """Mock a multisensor 6 node."""
+    node = Node(client, multisensor_6_state)
+    client.driver.controller.nodes[node.node_id] = node
+    return node
+
+
+@pytest.fixture(name="integration")
+async def integration_fixture(hass, client):
+    """Set up the zwave_js integration."""
+    entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"})
+    entry.add_to_hass(hass)
+
+    def initialize_client(async_on_initialized):
+        """Init the client."""
+        hass.async_create_task(async_on_initialized())
+
+    client.register_on_initialized.side_effect = initialize_client
+
+    await hass.config_entries.async_setup(entry.entry_id)
+    await hass.async_block_till_done()
+
+    return entry
diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py
new file mode 100644
index 00000000000..c6885377490
--- /dev/null
+++ b/tests/components/zwave_js/test_config_flow.py
@@ -0,0 +1,99 @@
+"""Test the Z-Wave JS config flow."""
+import asyncio
+from unittest.mock import patch
+
+from zwave_js_server.version import VersionInfo
+
+from homeassistant import config_entries, setup
+from homeassistant.components.zwave_js.const import DOMAIN
+
+
+async def test_user_step_full(hass):
+    """Test we create an entry with user step."""
+    await setup.async_setup_component(hass, "persistent_notification", {})
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+    assert result["type"] == "form"
+
+    with patch(
+        "homeassistant.components.zwave_js.config_flow.get_server_version",
+        return_value=VersionInfo(
+            driver_version="mock-driver-version",
+            server_version="mock-server-version",
+            home_id=1234,
+        ),
+    ), patch(
+        "homeassistant.components.zwave_js.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.zwave_js.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {
+                "url": "ws://localhost:3000",
+            },
+        )
+        await hass.async_block_till_done()
+
+    assert result2["type"] == "create_entry"
+    assert result2["title"] == "Z-Wave JS"
+    assert result2["data"] == {
+        "url": "ws://localhost:3000",
+    }
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1
+    assert result2["result"].unique_id == 1234
+
+
+async def test_user_step_invalid_input(hass):
+    """Test we handle invalid auth in the user step."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    with patch(
+        "homeassistant.components.zwave_js.config_flow.get_server_version",
+        side_effect=asyncio.TimeoutError,
+    ):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {
+                "url": "ws://localhost:3000",
+            },
+        )
+
+    assert result2["type"] == "form"
+    assert result2["errors"] == {"base": "cannot_connect"}
+
+    result3 = await hass.config_entries.flow.async_configure(
+        result["flow_id"],
+        {
+            "url": "not-ws-url",
+        },
+    )
+
+    assert result3["type"] == "form"
+    assert result3["errors"] == {"base": "invalid_ws_url"}
+
+
+async def test_user_step_unexpected_exception(hass):
+    """Test we handle unexpected exception."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_USER}
+    )
+
+    with patch(
+        "homeassistant.components.zwave_js.config_flow.get_server_version",
+        side_effect=Exception("Boom"),
+    ):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {
+                "url": "ws://localhost:3000",
+            },
+        )
+
+    assert result2["type"] == "form"
+    assert result2["errors"] == {"base": "unknown"}
diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py
new file mode 100644
index 00000000000..79876a5b453
--- /dev/null
+++ b/tests/components/zwave_js/test_sensor.py
@@ -0,0 +1,14 @@
+"""Test the Z-Wave JS sensor platform."""
+from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS
+
+AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature"
+
+
+async def test_numeric_sensor(hass, multisensor_6, integration):
+    """Test the numeric sensor."""
+    state = hass.states.get(AIR_TEMPERATURE_SENSOR)
+
+    assert state
+    assert state.state == "9.0"
+    assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS
+    assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE
diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json
new file mode 100644
index 00000000000..df026e8fd2c
--- /dev/null
+++ b/tests/fixtures/zwave_js/controller_state.json
@@ -0,0 +1,98 @@
+{
+    "controller": {
+        "libraryVersion": "Z-Wave 3.95",
+        "type": 1,
+        "homeId": 3245146787,
+        "ownNodeId": 1,
+        "isSecondary": false,
+        "isUsingHomeIdFromOtherNetwork": false,
+        "isSISPresent": true,
+        "wasRealPrimary": true,
+        "isStaticUpdateController": true,
+        "isSlave": false,
+        "serialApiVersion": "1.0",
+        "manufacturerId": 134,
+        "productType": 257,
+        "productId": 90,
+        "supportedFunctionTypes": [
+            2,
+            3,
+            4,
+            5,
+            6,
+            7,
+            8,
+            9,
+            16,
+            17,
+            18,
+            19,
+            20,
+            21,
+            22,
+            23,
+            24,
+            28,
+            32,
+            33,
+            34,
+            35,
+            36,
+            39,
+            41,
+            42,
+            43,
+            44,
+            45,
+            65,
+            66,
+            68,
+            69,
+            70,
+            71,
+            72,
+            73,
+            74,
+            75,
+            76,
+            77,
+            80,
+            81,
+            83,
+            84,
+            85,
+            86,
+            87,
+            94,
+            96,
+            97,
+            98,
+            99,
+            102,
+            103,
+            128,
+            144,
+            146,
+            147,
+            152,
+            180,
+            182,
+            183,
+            184,
+            185,
+            186,
+            189,
+            190,
+            191,
+            210,
+            211,
+            212,
+            238,
+            239
+        ],
+        "sucNodeId": 1,
+        "supportsTimers": false
+    },
+    "nodes": [
+    ]
+}
diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json
new file mode 100644
index 00000000000..3c508ffd3ff
--- /dev/null
+++ b/tests/fixtures/zwave_js/multisensor_6_state.json
@@ -0,0 +1,1830 @@
+{
+    "nodeId": 52,
+    "index": 0,
+    "installerIcon": 3079,
+    "userIcon": 3079,
+    "status": 1,
+    "ready": true,
+    "deviceClass": {
+        "basic": "Static Controller",
+        "generic": "Multilevel Sensor",
+        "specific": "Routing Multilevel Sensor",
+        "mandatorySupportedCCs": [
+            "Basic",
+            "Multilevel Sensor"
+        ],
+        "mandatoryControlCCs": []
+    },
+    "isListening": true,
+    "isFrequentListening": false,
+    "isRouting": true,
+    "maxBaudRate": 40000,
+    "isSecure": false,
+    "version": 4,
+    "isBeaming": true,
+    "manufacturerId": 134,
+    "productId": 100,
+    "productType": 258,
+    "firmwareVersion": "1.12",
+    "zwavePlusVersion": 1,
+    "nodeType": 0,
+    "roleType": 5,
+    "deviceConfig": {
+        "manufacturerId": 134,
+        "manufacturer": "AEON Labs",
+        "label": "ZW100",
+        "description": "Multisensor 6",
+        "devices": [
+            {
+                "productType": "0x0002",
+                "productId": "0x0064"
+            },
+            {
+                "productType": "0x0102",
+                "productId": "0x0064"
+            },
+            {
+                "productType": "0x0202",
+                "productId": "0x0064"
+            }
+        ],
+        "firmwareVersion": {
+            "min": "1.10",
+            "max": "255.255"
+        },
+        "paramInformation": {
+            "_map": {}
+        }
+    },
+    "label": "ZW100",
+    "neighbors": [
+        1,
+        32
+    ],
+    "interviewAttempts": 1,
+    "endpoints": [
+        {
+            "nodeId": 52,
+            "index": 0,
+            "installerIcon": 3079,
+            "userIcon": 3079
+        }
+    ],
+    "values": [
+        {
+            "commandClassName": "Basic",
+            "commandClass": 32,
+            "endpoint": 0,
+            "property": "currentValue",
+            "propertyName": "currentValue",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 99,
+                "label": "Current value"
+            },
+            "value": 255
+        },
+        {
+            "commandClassName": "Basic",
+            "commandClass": 32,
+            "endpoint": 0,
+            "property": "targetValue",
+            "propertyName": "targetValue",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "min": 0,
+                "max": 99,
+                "label": "Target value"
+            }
+        },
+        {
+            "commandClassName": "Binary Sensor",
+            "commandClass": 48,
+            "endpoint": 0,
+            "property": "Any",
+            "propertyName": "Any",
+            "metadata": {
+                "type": "boolean",
+                "readable": true,
+                "writeable": false,
+                "label": "Any",
+                "ccSpecific": {
+                    "sensorType": 255
+                }
+            },
+            "value": false
+        },
+        {
+            "commandClassName": "Multilevel Sensor",
+            "commandClass": 49,
+            "endpoint": 0,
+            "property": "Air temperature",
+            "propertyName": "Air temperature",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "unit": "°C",
+                "label": "Air temperature",
+                "ccSpecific": {
+                    "sensorType": 1,
+                    "scale": 0
+                }
+            },
+            "value": 9
+        },
+        {
+            "commandClassName": "Multilevel Sensor",
+            "commandClass": 49,
+            "endpoint": 0,
+            "property": "Illuminance",
+            "propertyName": "Illuminance",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "unit": "Lux",
+                "label": "Illuminance",
+                "ccSpecific": {
+                    "sensorType": 3,
+                    "scale": 1
+                }
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Multilevel Sensor",
+            "commandClass": 49,
+            "endpoint": 0,
+            "property": "Humidity",
+            "propertyName": "Humidity",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "unit": "%",
+                "label": "Humidity",
+                "ccSpecific": {
+                    "sensorType": 5,
+                    "scale": 0
+                }
+            },
+            "value": 65
+        },
+        {
+            "commandClassName": "Multilevel Sensor",
+            "commandClass": 49,
+            "endpoint": 0,
+            "property": "Ultraviolet",
+            "propertyName": "Ultraviolet",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "label": "Ultraviolet",
+                "ccSpecific": {
+                    "sensorType": 27,
+                    "scale": 0
+                }
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 2,
+            "propertyName": "Stay Awake in Battery Mode",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Disable",
+                    "1": "Enable"
+                },
+                "label": "Stay Awake in Battery Mode",
+                "description": "Stay awake for 10 minutes at power on",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 3,
+            "propertyName": "Motion Sensor reset timeout",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 10,
+                "max": 3600,
+                "default": 240,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Motion Sensor reset timeout",
+                "description": "Motion Sensor reset timeout",
+                "isFromConfig": true
+            },
+            "value": 240
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 4,
+            "propertyName": "Motion sensor sensitivity",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 255,
+                "default": 5,
+                "format": 1,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Disable",
+                    "1": "Enable, sensitivity level 1 (minimum)",
+                    "2": "Enable, sensitivity level 2",
+                    "3": "Enable, sensitivity level 3",
+                    "4": "Enable, sensitivity level 4",
+                    "5": "Enable, sensitivity level 5 (maximum)"
+                },
+                "label": "Motion sensor sensitivity",
+                "description": "Sensitivity level of PIR sensor (1=minimum, 5=maximum)",
+                "isFromConfig": true
+            },
+            "value": 5
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 5,
+            "propertyName": "Motion Sensor Triggered Command",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 255,
+                "default": 1,
+                "format": 1,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Send Basic Set CC",
+                    "2": "Send Sensor Binary Report CC"
+                },
+                "label": "Motion Sensor Triggered Command",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 8,
+            "propertyName": "Timeout after wake up",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 8,
+                "max": 255,
+                "default": 30,
+                "format": 1,
+                "allowManualEntry": true,
+                "label": "Timeout after wake up",
+                "description": "Set the timeout of awake after the Wake Up CC is sent out...",
+                "isFromConfig": true
+            },
+            "value": 15
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 39,
+            "propertyName": "Low Battery Report",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 10,
+                "max": 50,
+                "default": 20,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Low Battery Report",
+                "description": "Report Low Battery if below this value",
+                "isFromConfig": true
+            },
+            "value": 20
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 40,
+            "propertyName": "Selective Reporting",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Disable",
+                    "1": "Enable"
+                },
+                "label": "Selective Reporting",
+                "description": "Select to report on thresholds",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 42,
+            "propertyName": "Humidity Threshold",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 100,
+                "default": 10,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Humidity Threshold",
+                "description": "Humidity percent change threshold",
+                "isFromConfig": true
+            },
+            "value": 10
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 43,
+            "propertyName": "Luminance Threshold",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 0,
+                "max": 1000,
+                "default": 100,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Luminance Threshold",
+                "description": "Luminance change threshold",
+                "isFromConfig": true
+            },
+            "value": 100
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 44,
+            "propertyName": "Battery Threshold",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 100,
+                "default": 10,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Battery Threshold",
+                "description": "Battery level threshold",
+                "isFromConfig": true
+            },
+            "value": 10
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 45,
+            "propertyName": "Ultraviolet Threshold",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 100,
+                "default": 2,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Ultraviolet Threshold",
+                "description": "Ultraviolet change threshold",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 46,
+            "propertyName": "Send Alarm Report if low temperature",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 255,
+                "default": 0,
+                "format": 1,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Disable",
+                    "1": "Enable"
+                },
+                "label": "Send Alarm Report if low temperature",
+                "description": "Send an alarm report if temperature is less than -15 °C",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 48,
+            "propertyName": "Send a report if the measurement is out of limits",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 255,
+                "default": 0,
+                "format": 1,
+                "allowManualEntry": true,
+                "label": "Send a report if the measurement is out of limits",
+                "description": "Send report when measurement is at upper/lower limit",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 51,
+            "propertyName": "Upper limit value of humidity sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 100,
+                "default": 60,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Upper limit value of humidity sensor",
+                "description": "Upper limit value of humidity sensor",
+                "isFromConfig": true
+            },
+            "value": 60
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 52,
+            "propertyName": "Lower limit value of humidity sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 100,
+                "default": 50,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Lower limit value of humidity sensor",
+                "description": "Lower limit value of humidity sensor",
+                "isFromConfig": true
+            },
+            "value": 50
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 53,
+            "propertyName": "Upper limit value of Lighting sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 0,
+                "max": 30000,
+                "default": 1000,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Upper limit value of Lighting sensor",
+                "description": "Upper limit value of Lighting sensor",
+                "isFromConfig": true
+            },
+            "value": 1000
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 54,
+            "propertyName": "Lower limit value of Lighting sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 0,
+                "max": 30000,
+                "default": 100,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Lower limit value of Lighting sensor",
+                "description": "Lower limit value of Lighting sensor",
+                "isFromConfig": true
+            },
+            "value": 100
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 55,
+            "propertyName": "Upper limit value of ultraviolet sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 11,
+                "default": 8,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Upper limit value of ultraviolet sensor",
+                "description": "Upper limit value of ultraviolet sensor",
+                "isFromConfig": true
+            },
+            "value": 8
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 56,
+            "propertyName": "Lower limit value of ultraviolet sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 11,
+                "default": 4,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Lower limit value of ultraviolet sensor",
+                "description": "Lower limit value of ultraviolet sensor",
+                "isFromConfig": true
+            },
+            "value": 4
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 57,
+            "propertyName": "Recover limit value of temperature sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 0,
+                "max": 65535,
+                "default": 0,
+                "format": 1,
+                "allowManualEntry": true,
+                "label": "Recover limit value of temperature sensor",
+                "description": "Recover limit value of temperature sensor",
+                "isFromConfig": true
+            },
+            "value": 5122
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 58,
+            "propertyName": "Recover limit value of humidity sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 50,
+                "default": 5,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Recover limit value of humidity sensor",
+                "description": "Recover limit value of humidity sensor",
+                "isFromConfig": true
+            },
+            "value": 5
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 59,
+            "propertyName": "Recover limit value of Lighting sensor.",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 255,
+                "default": 10,
+                "format": 1,
+                "allowManualEntry": true,
+                "label": "Recover limit value of Lighting sensor.",
+                "description": "Recover limit value of Lighting sensor.",
+                "isFromConfig": true
+            },
+            "value": 10
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 60,
+            "propertyName": "Recover limit value of Ultraviolet sensor",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 5,
+                "default": 2,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Recover limit value of Ultraviolet sensor",
+                "description": "Recover limit value of Ultraviolet sensor",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 61,
+            "propertyName": "Out-of-limit state of the Sensors",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "valueSize": 1,
+                "min": 0,
+                "max": 255,
+                "default": 0,
+                "format": 1,
+                "allowManualEntry": true,
+                "label": "Out-of-limit state of the Sensors",
+                "description": "Out-of-limit state of the Sensors",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 64,
+            "propertyName": "Default unit of the automatic temperature report",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 1,
+                "max": 2,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Default unit of the automatic temperature report",
+                "description": "Default unit of the automatic temperature report",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 81,
+            "propertyName": "LED function",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 2,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Enable LED blinking",
+                    "1": "Disable PIR LED",
+                    "2": "Disable ALL"
+                },
+                "label": "LED function",
+                "description": "Disable/Enable LED function",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 111,
+            "propertyName": "Group 1 Report Interval",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 5,
+                "max": 2678400,
+                "default": 3600,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1 Report Interval",
+                "description": "How often to update Group 1",
+                "isFromConfig": true
+            },
+            "value": 3600
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 112,
+            "propertyName": "Group 2 Report Interval",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 5,
+                "max": 2678400,
+                "default": 3600,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2 Report Interval",
+                "description": "Group 2 Report Interval",
+                "isFromConfig": true
+            },
+            "value": 3600
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 113,
+            "propertyName": "Group 3 Report Interval",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 5,
+                "max": 2678400,
+                "default": 3600,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3 Report Interval",
+                "description": "Group 3 Report Interval",
+                "isFromConfig": true
+            },
+            "value": 3600
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 202,
+            "propertyName": "Humidity Sensor Calibration",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": -50,
+                "max": 50,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Humidity Sensor Calibration",
+                "description": "Humidity Sensor Calibration",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 203,
+            "propertyName": "Luminance Sensor Calibration",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": -1000,
+                "max": 1000,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Luminance Sensor Calibration",
+                "description": "Luminance Sensor Calibration",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 204,
+            "propertyName": "Ultraviolet Sensor Calibration",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": -10,
+                "max": 10,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Ultraviolet Sensor Calibration",
+                "description": "Ultraviolet Sensor Calibration",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 252,
+            "propertyName": "Disable/Enable Configuration Lock",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Disable",
+                    "1": "Enable"
+                },
+                "label": "Disable/Enable Configuration Lock",
+                "description": "Disable/Enable Configuration Lock (0=Disable, 1=Enable)",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 101,
+            "propertyKey": 1,
+            "propertyName": "Group 1: Send battery reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1: Send battery reports",
+                "description": "Include battery information in periodic reports to Group 1",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 101,
+            "propertyKey": 16,
+            "propertyName": "Group 1: Send ultraviolet reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1: Send ultraviolet reports",
+                "description": "Include ultraviolet information in periodic reports to Group 1",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 101,
+            "propertyKey": 32,
+            "propertyName": "Group 1: Send temperature reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1: Send temperature reports",
+                "description": "Include temperature information in periodic reports to Group 1",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 101,
+            "propertyKey": 64,
+            "propertyName": "Group 1: Send humidity reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1: Send humidity reports",
+                "description": "Include humidity information in periodic reports to Group 1",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 101,
+            "propertyKey": 128,
+            "propertyName": "Group 1: Send luminance reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 1: Send luminance reports",
+                "description": "Include luminance information in periodic reports to Group 1",
+                "isFromConfig": true
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 102,
+            "propertyKey": 1,
+            "propertyName": "Group 2: Send battery reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2: Send battery reports",
+                "description": "Include battery information in periodic reports to Group 2",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 102,
+            "propertyKey": 16,
+            "propertyName": "Group 2: Send ultraviolet reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2: Send ultraviolet reports",
+                "description": "Include ultraviolet information in periodic reports to Group 2",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 102,
+            "propertyKey": 32,
+            "propertyName": "Group 2: Send temperature reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2: Send temperature reports",
+                "description": "Include temperature information in periodic reports to Group 2",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 102,
+            "propertyKey": 64,
+            "propertyName": "Group 2: Send humidity reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2: Send humidity reports",
+                "description": "Include humidity information in periodic reports to Group 2",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 102,
+            "propertyKey": 128,
+            "propertyName": "Group 2: Send luminance reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 2: Send luminance reports",
+                "description": "Include luminance information in periodic reports to Group 2",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 103,
+            "propertyKey": 1,
+            "propertyName": "Group 3: Send battery reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3: Send battery reports",
+                "description": "Include battery information in periodic reports to Group 3",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 103,
+            "propertyKey": 16,
+            "propertyName": "Group 3: Send ultraviolet reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3: Send ultraviolet reports",
+                "description": "Include ultraviolet information in periodic reports to Group 3",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 103,
+            "propertyKey": 32,
+            "propertyName": "Group 3: Send temperature reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3: Send temperature reports",
+                "description": "Include temperature information in periodic reports to Group 3",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 103,
+            "propertyKey": 64,
+            "propertyName": "Group 3: Send humidity reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3: Send humidity reports",
+                "description": "Include humidity information in periodic reports to Group 3",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 103,
+            "propertyKey": 128,
+            "propertyName": "Group 3: Send luminance reports",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Group 3: Send luminance reports",
+                "description": "Include luminance information in periodic reports to Group 3",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 9,
+            "propertyKey": 1,
+            "propertyName": "Sleep State",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "valueSize": 2,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "Asleep",
+                    "1": "Awake"
+                },
+                "label": "Sleep State",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 9,
+            "propertyKey": 256,
+            "propertyName": "Power Mode",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "valueSize": 2,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "0": "USB",
+                    "1": "Battery"
+                },
+                "label": "Power Mode",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 41,
+            "propertyKey": 15,
+            "propertyName": "Temperature Threshold (Unit)",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 3,
+                "min": 1,
+                "max": 2,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Celsius",
+                    "2": "Fahrenheit"
+                },
+                "label": "Temperature Threshold (Unit)",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 41,
+            "propertyKey": 16776960,
+            "propertyName": "Temperature Threshold",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 3,
+                "min": 0,
+                "max": 100,
+                "default": 20,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Temperature Threshold",
+                "description": "Threshold change in temperature to induce an automatic report.",
+                "isFromConfig": true
+            },
+            "value": 5122
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 49,
+            "propertyKey": 65280,
+            "propertyName": "Upper temperature limit (Unit)",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 1,
+                "max": 2,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Celsius",
+                    "2": "Fahrenheit"
+                },
+                "label": "Upper temperature limit (Unit)",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 49,
+            "propertyKey": 4294901760,
+            "propertyName": "Upper temperature limit",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": -400,
+                "max": 2120,
+                "default": 280,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Upper temperature limit",
+                "isFromConfig": true
+            },
+            "value": 824
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 50,
+            "propertyKey": 65280,
+            "propertyName": "Lower temperature limit (Unit)",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 1,
+                "max": 2,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Celsius",
+                    "2": "Fahrenheit"
+                },
+                "label": "Lower temperature limit (Unit)",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 50,
+            "propertyKey": 4294901760,
+            "propertyName": "Lower temperature limit",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 4,
+                "min": -400,
+                "max": 2120,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Lower temperature limit",
+                "isFromConfig": true
+            },
+            "value": 320
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 201,
+            "propertyKey": 255,
+            "propertyName": "Temperature Calibration (Unit)",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": 1,
+                "max": 2,
+                "default": 1,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Celsius",
+                    "2": "Fahrenheit"
+                },
+                "label": "Temperature Calibration (Unit)",
+                "isFromConfig": true
+            },
+            "value": 2
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 201,
+            "propertyKey": 65280,
+            "propertyName": "Temperature Calibration",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": true,
+                "valueSize": 2,
+                "min": -127,
+                "max": 127,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Temperature Calibration",
+                "isFromConfig": true
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 100,
+            "propertyName": "Set parameters 101-103 to default.",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Set parameters 101-103 to default.",
+                "description": "Reset 101-103 to defaults",
+                "isFromConfig": true
+            }
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 110,
+            "propertyName": "Set parameters 111-113 to default.",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "valueSize": 1,
+                "min": 0,
+                "max": 1,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": true,
+                "label": "Set parameters 111-113 to default.",
+                "description": "Set parameters 111-113 to default.",
+                "isFromConfig": true
+            }
+        },
+        {
+            "commandClassName": "Configuration",
+            "commandClass": 112,
+            "endpoint": 0,
+            "property": 255,
+            "propertyName": "Reset to default factory settings",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "valueSize": 4,
+                "min": 0,
+                "max": 1431655765,
+                "default": 0,
+                "format": 0,
+                "allowManualEntry": false,
+                "states": {
+                    "1": "Resets all configuration parameters to defaults",
+                    "1431655765": "Reset to default factory settings and be excluded"
+                },
+                "label": "Reset to default factory settings",
+                "isFromConfig": true
+            }
+        },
+        {
+            "commandClassName": "Notification",
+            "commandClass": 113,
+            "endpoint": 0,
+            "property": "Home Security",
+            "propertyKey": "Cover status",
+            "propertyName": "Home Security",
+            "propertyKeyName": "Cover status",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 255,
+                "label": "Cover status",
+                "states": {
+                    "0": "idle",
+                    "3": "Tampering, product cover removed"
+                },
+                "ccSpecific": {
+                    "notificationType": 7
+                }
+            },
+            "value": 0
+        },
+        {
+            "commandClassName": "Notification",
+            "commandClass": 113,
+            "endpoint": 0,
+            "property": "Home Security",
+            "propertyKey": "Motion sensor status",
+            "propertyName": "Home Security",
+            "propertyKeyName": "Motion sensor status",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 255,
+                "label": "Motion sensor status",
+                "states": {
+                    "0": "idle",
+                    "8": "Motion detection"
+                },
+                "ccSpecific": {
+                    "notificationType": 7
+                }
+            },
+            "value": 8
+        },
+        {
+            "commandClassName": "Manufacturer Specific",
+            "commandClass": 114,
+            "endpoint": 0,
+            "property": "manufacturerId",
+            "propertyName": "manufacturerId",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 65535,
+                "label": "Manufacturer ID"
+            },
+            "value": 134
+        },
+        {
+            "commandClassName": "Manufacturer Specific",
+            "commandClass": 114,
+            "endpoint": 0,
+            "property": "productType",
+            "propertyName": "productType",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 65535,
+                "label": "Product type"
+            },
+            "value": 258
+        },
+        {
+            "commandClassName": "Manufacturer Specific",
+            "commandClass": 114,
+            "endpoint": 0,
+            "property": "productId",
+            "propertyName": "productId",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 65535,
+                "label": "Product ID"
+            },
+            "value": 100
+        },
+        {
+            "commandClassName": "Battery",
+            "commandClass": 128,
+            "endpoint": 0,
+            "property": "level",
+            "propertyName": "level",
+            "metadata": {
+                "type": "number",
+                "readable": true,
+                "writeable": false,
+                "min": 0,
+                "max": 100,
+                "unit": "%",
+                "label": "Battery level"
+            },
+            "value": 100
+        },
+        {
+            "commandClassName": "Battery",
+            "commandClass": 128,
+            "endpoint": 0,
+            "property": "isLow",
+            "propertyName": "isLow",
+            "metadata": {
+                "type": "boolean",
+                "readable": true,
+                "writeable": false,
+                "label": "Low battery level"
+            },
+            "value": false
+        },
+        {
+            "commandClassName": "Wake Up",
+            "commandClass": 132,
+            "endpoint": 0,
+            "property": "wakeUpInterval",
+            "propertyName": "wakeUpInterval",
+            "metadata": {
+                "type": "number",
+                "readable": false,
+                "writeable": true,
+                "min": 240,
+                "max": 3600,
+                "label": "Wake Up interval",
+                "steps": 60,
+                "default": 3600
+            },
+            "value": 3600
+        },
+        {
+            "commandClassName": "Wake Up",
+            "commandClass": 132,
+            "endpoint": 0,
+            "property": "controllerNodeId",
+            "propertyName": "controllerNodeId",
+            "metadata": {
+                "type": "any",
+                "readable": true,
+                "writeable": false,
+                "label": "Node ID of the controller"
+            },
+            "value": 1
+        },
+        {
+            "commandClassName": "Version",
+            "commandClass": 134,
+            "endpoint": 0,
+            "property": "libraryType",
+            "propertyName": "libraryType",
+            "metadata": {
+                "type": "any",
+                "readable": true,
+                "writeable": false,
+                "label": "Libary type"
+            },
+            "value": 3
+        },
+        {
+            "commandClassName": "Version",
+            "commandClass": 134,
+            "endpoint": 0,
+            "property": "protocolVersion",
+            "propertyName": "protocolVersion",
+            "metadata": {
+                "type": "any",
+                "readable": true,
+                "writeable": false,
+                "label": "Z-Wave protocol version"
+            },
+            "value": "4.54"
+        },
+        {
+            "commandClassName": "Version",
+            "commandClass": 134,
+            "endpoint": 0,
+            "property": "firmwareVersions",
+            "propertyName": "firmwareVersions",
+            "metadata": {
+                "type": "any",
+                "readable": true,
+                "writeable": false,
+                "label": "Z-Wave chip firmware versions"
+            },
+            "value": [
+                "1.12"
+            ]
+        },
+        {
+            "commandClassName": "Version",
+            "commandClass": 134,
+            "endpoint": 0,
+            "property": "hardwareVersion",
+            "propertyName": "hardwareVersion",
+            "metadata": {
+                "type": "any",
+                "readable": true,
+                "writeable": false,
+                "label": "Z-Wave chip hardware version"
+            }
+        }
+    ]
+}
-- 
GitLab