Skip to content

Commit

Permalink
Add ozw add-on discovery and mqtt client (home-assistant#43838)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinHjelmare authored Dec 2, 2020
1 parent 8efa9c5 commit 9043b7b
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 30 deletions.
13 changes: 13 additions & 0 deletions homeassistant/components/hassio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import timedelta
import logging
import os
from typing import Optional

import voluptuous as vol

Expand All @@ -23,6 +24,7 @@

from .addon_panel import async_setup_addon_panel
from .auth import async_setup_auth_view
from .const import ATTR_DISCOVERY
from .discovery import async_setup_discovery_view
from .handler import HassIO, HassioAPIError, api_data
from .http import HassIOView
Expand Down Expand Up @@ -200,6 +202,17 @@ async def async_set_addon_options(
return await hassio.send_command(command, payload=options)


@bind_hass
async def async_get_addon_discovery_info(
hass: HomeAssistantType, slug: str
) -> Optional[dict]:
"""Return discovery data for an add-on."""
hassio = hass.data[DOMAIN]
data = await hassio.retrieve_discovery_messages()
discovered_addons = data[ATTR_DISCOVERY]
return next((addon for addon in discovered_addons if addon["addon"] == slug), None)


@callback
@bind_hass
def get_info(hass):
Expand Down
82 changes: 69 additions & 13 deletions homeassistant/components/ozw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,25 @@
)
from openzwavemqtt.models.node import OZWNode
from openzwavemqtt.models.value import OZWValue
from openzwavemqtt.util.mqtt_client import MQTTClient
import voluptuous as vol

from homeassistant.components import mqtt
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
from homeassistant.helpers.dispatcher import async_dispatcher_send

from . import const
from .const import (
CONF_INTEGRATION_CREATED_ADDON,
CONF_USE_ADDON,
DATA_UNSUBSCRIBE,
DOMAIN,
MANAGER,
OPTIONS,
PLATFORMS,
TOPIC_OPENZWAVE,
)
Expand All @@ -50,13 +53,11 @@

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
DATA_DEVICES = "zwave-mqtt-devices"
DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client"


async def async_setup(hass: HomeAssistant, config: dict):
"""Initialize basic config of ozw component."""
if "mqtt" not in hass.config.components:
_LOGGER.error("MQTT integration is not set up")
return False
hass.data[DOMAIN] = {}
return True

Expand All @@ -69,16 +70,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
data_nodes = {}
data_values = {}
removed_nodes = []
manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"}

@callback
def send_message(topic, payload):
mqtt.async_publish(hass, topic, json.dumps(payload))
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)

if entry.data.get(CONF_USE_ADDON):
# Do not use MQTT integration. Use own MQTT client.
# Retrieve discovery info from the OpenZWave add-on.
discovery_info = await hass.components.hassio.async_get_addon_discovery_info(
"core_zwave"
)

options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/")
if not discovery_info:
_LOGGER.error("Failed to get add-on discovery info")
raise ConfigEntryNotReady

discovery_info_config = discovery_info["config"]

host = discovery_info_config["host"]
port = discovery_info_config["port"]
username = discovery_info_config["username"]
password = discovery_info_config["password"]
mqtt_client = MQTTClient(host, port, username=username, password=password)
manager_options["send_message"] = mqtt_client.send_message

else:
if "mqtt" not in hass.config.components:
_LOGGER.error("MQTT integration is not set up")
return False

@callback
def send_message(topic, payload):
mqtt.async_publish(hass, topic, json.dumps(payload))

manager_options["send_message"] = send_message

options = OZWOptions(**manager_options)
manager = OZWManager(options)

hass.data[DOMAIN][MANAGER] = manager
hass.data[DOMAIN][OPTIONS] = options

@callback
def async_node_added(node):
Expand Down Expand Up @@ -234,11 +265,29 @@ async def start_platforms():
for component in PLATFORMS
]
)
ozw_data[DATA_UNSUBSCRIBE].append(
await mqtt.async_subscribe(
hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message
if entry.data.get(CONF_USE_ADDON):
mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager))

async def async_stop_mqtt_client(event=None):
"""Stop the mqtt client.
Do not unsubscribe the manager topic.
"""
mqtt_client_task.cancel()
try:
await mqtt_client_task
except asyncio.CancelledError:
pass

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client)
ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client

else:
ozw_data[DATA_UNSUBSCRIBE].append(
await mqtt.async_subscribe(
hass, f"{manager.options.topic_prefix}#", async_receive_message
)
)
)

hass.async_create_task(start_platforms())

Expand All @@ -262,6 +311,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# unsubscribe all listeners
for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]:
unsubscribe_listener()

if entry.data.get(CONF_USE_ADDON):
async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][
DATA_STOP_MQTT_CLIENT
]
await async_stop_mqtt_client()

hass.data[DOMAIN].pop(entry.entry_id)

return True
Expand Down
34 changes: 27 additions & 7 deletions homeassistant/components/ozw/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow

from .const import CONF_INTEGRATION_CREATED_ADDON
from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON
from .const import DOMAIN # pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)
Expand All @@ -16,7 +16,6 @@
CONF_ADDON_NETWORK_KEY = "network_key"
CONF_NETWORK_KEY = "network_key"
CONF_USB_PATH = "usb_path"
CONF_USE_ADDON = "use_addon"
TITLE = "OpenZWave"

ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool})
Expand All @@ -43,17 +42,36 @@ async def async_step_user(self, user_input=None):
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")

# Currently all flow results need the MQTT integration.
# This will change when we have the direct MQTT client connection.
# When that is implemented, move this check to _async_use_mqtt_integration.
if "mqtt" not in self.hass.config.components:
return self.async_abort(reason="mqtt_required")
# Set a unique_id to make sure discovery flow is aborted on progress.
await self.async_set_unique_id(DOMAIN, raise_on_progress=False)

if not self.hass.components.hassio.is_hassio():
return self._async_use_mqtt_integration()

return await self.async_step_on_supervisor()

async def async_step_hassio(self, discovery_info):
"""Receive configuration from add-on discovery info.
This flow is triggered by the OpenZWave add-on.
"""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()

addon_config = await self._async_get_addon_config()
self.usb_path = addon_config[CONF_ADDON_DEVICE]
self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "")

return await self.async_step_hassio_confirm()

async def async_step_hassio_confirm(self, user_input=None):
"""Confirm the add-on discovery."""
if user_input is not None:
self.use_addon = True
return self._async_create_entry_from_vars()

return self.async_show_form(step_id="hassio_confirm")

def _async_create_entry_from_vars(self):
"""Return a config entry for the flow."""
return self.async_create_entry(
Expand All @@ -73,6 +91,8 @@ def _async_use_mqtt_integration(self):
This is the entry point for the logic that is needed
when this integration will depend on the MQTT integration.
"""
if "mqtt" not in self.hass.config.components:
return self.async_abort(reason="mqtt_required")
return self._async_create_entry_from_vars()

async def async_step_on_supervisor(self, user_input=None):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ozw/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DATA_UNSUBSCRIBE = "unsubscribe"

CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
CONF_USE_ADDON = "use_addon"

PLATFORMS = [
BINARY_SENSOR_DOMAIN,
Expand All @@ -24,7 +25,6 @@
SWITCH_DOMAIN,
]
MANAGER = "manager"
OPTIONS = "options"

# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/ozw/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ozw",
"requirements": [
"python-openzwave-mqtt==1.3.2"
"python-openzwave-mqtt[mqtt-client]==1.4.0"
],
"after_dependencies": [
"mqtt"
Expand All @@ -14,4 +14,4 @@
"@marcelveldt",
"@MartinHjelmare"
]
}
}
12 changes: 10 additions & 2 deletions homeassistant/components/ozw/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
"on_supervisor": {
"title": "Select connection method",
"description": "Do you want to use the OpenZWave Supervisor add-on?",
"data": {"use_addon": "Use the OpenZWave Supervisor add-on"}
"data": { "use_addon": "Use the OpenZWave Supervisor add-on" }
},
"install_addon": {
"title": "The OpenZWave add-on installation has started"
},
"start_addon": {
"title": "Enter the OpenZWave add-on configuration",
"data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"}
"data": {
"usb_path": "[%key:common::config_flow::data::usb_path%]",
"network_key": "Network Key"
}
},
"hassio_confirm": {
"title": "Set up OpenZWave integration with the OpenZWave add-on"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"addon_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.",
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/ozw/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"addon_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.",
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"mqtt_required": "The MQTT integration is not set up",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
Expand All @@ -14,6 +16,9 @@
"install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes."
},
"step": {
"hassio_confirm": {
"title": "Set up OpenZWave integration with the OpenZWave add-on"
},
"install_addon": {
"title": "The OpenZWave add-on installation has started"
},
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/ozw/websocket_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv

from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER, OPTIONS
from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
from .lock import ATTR_USERCODE

TYPE = "type"
Expand Down Expand Up @@ -461,7 +461,7 @@ def websocket_refresh_node_info(hass, connection, msg):
"""Tell OpenZWave to re-interview a node."""

manager = hass.data[DOMAIN][MANAGER]
options = hass.data[DOMAIN][OPTIONS]
options = manager.options

@callback
def forward_node(node):
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1794,7 +1794,7 @@ python-nest==4.1.0
python-nmap==0.6.1

# homeassistant.components.ozw
python-openzwave-mqtt==1.3.2
python-openzwave-mqtt[mqtt-client]==1.4.0

# homeassistant.components.qbittorrent
python-qbittorrent==0.4.1
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ python-miio==0.5.4
python-nest==4.1.0

# homeassistant.components.ozw
python-openzwave-mqtt==1.3.2
python-openzwave-mqtt[mqtt-client]==1.4.0

# homeassistant.components.songpal
python-songpal==0.12
Expand Down
9 changes: 9 additions & 0 deletions tests/components/ozw/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,12 @@ def mock_uninstall_addon():
"homeassistant.components.hassio.async_uninstall_addon"
) as uninstall_addon:
yield uninstall_addon


@pytest.fixture(name="get_addon_discovery_info")
def mock_get_addon_discovery_info():
"""Mock get add-on discovery info."""
with patch(
"homeassistant.components.hassio.async_get_addon_discovery_info"
) as get_addon_discovery_info:
yield get_addon_discovery_info
Loading

0 comments on commit 9043b7b

Please sign in to comment.