Add DHCP support to goalzero (#50425)

pull/50623/head
tkdrob 2021-05-14 14:12:46 -04:00 committed by GitHub
parent 4d55290932
commit 40993f3ebb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 164 additions and 32 deletions

View File

@ -42,7 +42,7 @@ async def async_setup_entry(hass, entry):
try:
await api.get_state()
except exceptions.ConnectError as err:
raise UpdateFailed(f"Failed to communicating with API: {err}") from err
raise UpdateFailed(f"Failed to communicating with device {err}") from err
coordinator = DataUpdateCoordinator(
hass,
@ -84,21 +84,16 @@ class YetiEntity(CoordinatorEntity):
@property
def device_info(self):
"""Return the device information of the entity."""
if self.api.data:
sw_version = self.api.data["firmwareVersion"]
else:
sw_version = None
if self.api.sysdata:
model = self.api.sysdata["model"]
else:
model = model or None
return {
info = {
"identifiers": {(DOMAIN, self._server_unique_id)},
"manufacturer": "Goal Zero",
"model": model,
"name": self._name,
"sw_version": sw_version,
}
if self.api.sysdata:
info["model"] = self.api.sysdata["model"]
if self.api.data:
info["sw_version"] = self.api.data["firmwareVersion"]
return info
@property
def device_class(self):

View File

@ -1,12 +1,18 @@
"""Config flow for Goal Zero Yeti integration."""
from __future__ import annotations
import logging
from typing import Any
from goalzero import Yeti, exceptions
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import DEFAULT_NAME, DOMAIN
@ -20,32 +26,63 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
"""Initialize a Goal Zero Yeti flow."""
self.ip_address = None
async def async_step_dhcp(self, discovery_info):
"""Handle dhcp discovery."""
self.ip_address = discovery_info[IP_ADDRESS]
await self.async_set_unique_id(discovery_info[MAC_ADDRESS])
self._abort_if_unique_id_configured(updates={CONF_HOST: self.ip_address})
self._async_abort_entries_match({CONF_HOST: self.ip_address})
_, error = await self._async_try_connect(self.ip_address)
if error is None:
return await self.async_step_confirm_discovery()
return self.async_abort(reason=error)
async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Allow the user to confirm adding the device."""
if user_input is not None:
return self.async_create_entry(
title="Goal Zero",
data={
CONF_HOST: self.ip_address,
CONF_NAME: DEFAULT_NAME,
},
)
self._set_confirm_only()
return self.async_show_form(
step_id="confirm_discovery",
description_placeholders={
CONF_HOST: self.ip_address,
CONF_NAME: DEFAULT_NAME,
},
)
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
if user_input is not None:
host = user_input[CONF_HOST]
name = user_input[CONF_NAME]
self._async_abort_entries_match({CONF_HOST: host})
try:
await self._async_try_connect(host)
except exceptions.ConnectError:
errors["base"] = "cannot_connect"
_LOGGER.error("Error connecting to device at %s", host)
except exceptions.InvalidHost:
errors["base"] = "invalid_host"
_LOGGER.error("Invalid host at %s", host)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
mac_address, error = await self._async_try_connect(host)
if error is None:
await self.async_set_unique_id(format_mac(mac_address))
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return self.async_create_entry(
title=name,
data={CONF_HOST: host, CONF_NAME: name},
)
errors["base"] = error
user_input = user_input or {}
return self.async_show_form(
@ -64,6 +101,18 @@ class GoalZeroFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
)
async def _async_try_connect(self, host):
session = async_get_clientsession(self.hass)
api = Yeti(host, self.hass.loop, session)
await api.get_state()
"""Try connecting to Goal Zero Yeti."""
try:
session = async_get_clientsession(self.hass)
api = Yeti(host, self.hass.loop, session)
await api.sysinfo()
except exceptions.ConnectError:
_LOGGER.error("Error connecting to device at %s", host)
return None, "cannot_connect"
except exceptions.InvalidHost:
_LOGGER.error("Invalid host at %s", host)
return None, "invalid_host"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return None, "unknown"
return str(api.sysdata["macAddress"]), None

View File

@ -4,6 +4,9 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/goalzero",
"requirements": ["goalzero==0.1.7"],
"dhcp": [
{"hostname": "yeti*"}
],
"codeowners": ["@tkdrob"],
"iot_class": "local_polling"
}

View File

@ -3,11 +3,15 @@
"step": {
"user": {
"title": "Goal Zero Yeti",
"description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.",
"description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"name": "[%key:common::config_flow::data::name%]"
}
},
"confirm_discovery": {
"title": "Goal Zero Yeti",
"description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual."
}
},
"error": {
@ -16,7 +20,9 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -14,7 +14,7 @@
"host": "Host",
"name": "Name"
},
"description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wifi network. DHCP reservation must be set up in your router settings for the device to ensure the host IP does not change. Refer to your router's user manual.",
"description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Set it up in your router settings for the device. Refer to your router's user manual.",
"title": "Goal Zero Yeti"
}
}

View File

@ -66,6 +66,10 @@ DHCP = [
"domain": "flume",
"hostname": "flume-gw-*"
},
{
"domain": "goalzero",
"hostname": "yeti*"
},
{
"domain": "gogogate2",
"hostname": "ismartgate*"

View File

@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, patch
from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS
from homeassistant.const import CONF_HOST, CONF_NAME
HOST = "1.2.3.4"
@ -17,6 +18,12 @@ CONF_CONFIG_FLOW = {
CONF_NAME: NAME,
}
CONF_DHCP_FLOW = {
IP_ADDRESS: "1.1.1.1",
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
HOSTNAME: "any",
}
async def _create_mocked_yeti(raise_exception=False):
mocked_yeti = AsyncMock()

View File

@ -4,16 +4,18 @@ from unittest.mock import patch
from goalzero import exceptions
from homeassistant.components.goalzero.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from homeassistant.setup import async_setup_component
from . import (
CONF_CONFIG_FLOW,
CONF_DATA,
CONF_DHCP_FLOW,
CONF_HOST,
CONF_NAME,
NAME,
@ -114,3 +116,69 @@ async def test_flow_user_unknown_error(hass):
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": "unknown"}
async def test_dhcp_discovery(hass):
"""Test we can process the discovery from dhcp."""
await async_setup_component(hass, "persistent_notification", {})
mocked_yeti = await _create_mocked_yeti()
with _patch_config_flow_yeti(mocked_yeti), _patch_setup():
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=CONF_DHCP_FLOW,
)
assert result["type"] == "form"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{},
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_NAME: "Yeti",
}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=CONF_DHCP_FLOW,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_dhcp_discovery_failed(hass):
"""Test failed setup from dhcp."""
mocked_yeti = await _create_mocked_yeti(True)
with _patch_config_flow_yeti(mocked_yeti) as yetimock:
yetimock.side_effect = exceptions.ConnectError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=CONF_DHCP_FLOW,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "cannot_connect"
with _patch_config_flow_yeti(mocked_yeti) as yetimock:
yetimock.side_effect = exceptions.InvalidHost
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=CONF_DHCP_FLOW,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "invalid_host"
with _patch_config_flow_yeti(mocked_yeti) as yetimock:
yetimock.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=CONF_DHCP_FLOW,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "unknown"