diff --git a/CODEOWNERS b/CODEOWNERS index c8a391fd7dc..3c3c7170870 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -517,6 +517,10 @@ build.json @home-assistant/supervisor /tests/components/goalzero/ @tkdrob /homeassistant/components/gogogate2/ @vangorra /tests/components/gogogate2/ @vangorra +/homeassistant/components/goecharger/ @0xFEEDC0DE64 +/tests/components/goecharger/ @0xFEEDC0DE64 +/homeassistant/components/goecontroller/ @0xFEEDC0DE64 +/tests/components/goecontroller/ @0xFEEDC0DE64 /homeassistant/components/goodwe/ @mletenay @starkillerOG /tests/components/goodwe/ @mletenay @starkillerOG /homeassistant/components/google/ @allenporter @@ -1547,6 +1551,8 @@ build.json @home-assistant/supervisor /homeassistant/components/water_heater/ @home-assistant/core /tests/components/water_heater/ @home-assistant/core /homeassistant/components/watson_tts/ @rutkai +/homeassistant/components/wattpilot/ @0xFEEDC0DE64 +/tests/components/wattpilot/ @0xFEEDC0DE64 /homeassistant/components/watttime/ @bachya /tests/components/watttime/ @bachya /homeassistant/components/waze_travel_time/ @eifinger diff --git a/homeassistant/components/goecharger/__init__.py b/homeassistant/components/goecharger/__init__.py new file mode 100644 index 00000000000..df504f14195 --- /dev/null +++ b/homeassistant/components/goecharger/__init__.py @@ -0,0 +1,49 @@ +"""The go-eCharger integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS: list[Platform] = [Platform.LIGHT] + + +async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the go-eCharger integration.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up go-eCharger from a config entry.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + hass.data.setdefault(DOMAIN, {}) + + # TODO 1. Create API instance + # TODO 2. Validate the API connection (and authentication) + # TODO 3. Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/goecharger/config_flow.py b/homeassistant/components/goecharger/config_flow.py new file mode 100644 index 00000000000..aa6c9fa71fe --- /dev/null +++ b/homeassistant/components/goecharger/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for go-eCharger integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +class PlaceholderHub: + """Placeholder class to make tests pass. + + TODO Remove this placeholder class and replace with things from your PyPI package. + """ + + def __init__(self, host: str) -> None: + """Initialize.""" + self.host = host + + async def authenticate(self, username: str, password: str) -> bool: + """Test if we can authenticate with the host.""" + return True + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + hub = PlaceholderHub(data["host"]) + + if not await hub.authenticate(data["username"], data["password"]): + raise InvalidAuth + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": "Name of the device"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for go-eCharger.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + serial = discovery_info.properties.get("serial") + if serial is None: + return self.async_abort(reason="mdns_missing_serial") + + self.context["title_placeholders"] = {"name": serial} + + # Check if already configured + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={}) + + return self.async_show_form( + step_id="user", description_placeholders={"serial": serial} + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/goecharger/const.py b/homeassistant/components/goecharger/const.py new file mode 100644 index 00000000000..f1ac976d120 --- /dev/null +++ b/homeassistant/components/goecharger/const.py @@ -0,0 +1,3 @@ +"""Constants for the go-eCharger integration.""" + +DOMAIN = "goecharger" diff --git a/homeassistant/components/goecharger/manifest.json b/homeassistant/components/goecharger/manifest.json new file mode 100644 index 00000000000..fc2733cd7ec --- /dev/null +++ b/homeassistant/components/goecharger/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "goecharger", + "name": "go-eCharger", + "after_dependencies": ["zeroconf"], + "codeowners": ["@0xFEEDC0DE64"], + "config_flow": true, + "dependencies": [], + "documentation": "https://www.home-assistant.io/integrations/goecharger", + "integration_type": "device", + "iot_class": "local_push", + "requirements": [], + "zeroconf": [ + "_go-e_go-eCharger_V4._tcp.local.", + "_go-e_go-eCharger._tcp.local." + ] +} diff --git a/homeassistant/components/goecharger/strings.json b/homeassistant/components/goecharger/strings.json new file mode 100644 index 00000000000..bcdace30cb0 --- /dev/null +++ b/homeassistant/components/goecharger/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "flow_title": "{serial}", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "mdns_missing_serial": "Missing serial number in MDNS properties." + } + } +} diff --git a/homeassistant/components/goecontroller/__init__.py b/homeassistant/components/goecontroller/__init__.py new file mode 100644 index 00000000000..f482fe8aa8a --- /dev/null +++ b/homeassistant/components/goecontroller/__init__.py @@ -0,0 +1,49 @@ +"""The go-e Controller integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS: list[Platform] = [Platform.LIGHT] + + +async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the go-eCharger integration.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up go-e Controller from a config entry.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + hass.data.setdefault(DOMAIN, {}) + + # TODO 1. Create API instance + # TODO 2. Validate the API connection (and authentication) + # TODO 3. Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/goecontroller/config_flow.py b/homeassistant/components/goecontroller/config_flow.py new file mode 100644 index 00000000000..539d3f691cf --- /dev/null +++ b/homeassistant/components/goecontroller/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for go-e Controller integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +class PlaceholderHub: + """Placeholder class to make tests pass. + + TODO Remove this placeholder class and replace with things from your PyPI package. + """ + + def __init__(self, host: str) -> None: + """Initialize.""" + self.host = host + + async def authenticate(self, username: str, password: str) -> bool: + """Test if we can authenticate with the host.""" + return True + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + hub = PlaceholderHub(data["host"]) + + if not await hub.authenticate(data["username"], data["password"]): + raise InvalidAuth + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": "Name of the device"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for go-e Controller.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + serial = discovery_info.properties.get("serial") + if serial is None: + return self.async_abort(reason="mdns_missing_serial") + + self.context["title_placeholders"] = {"name": serial} + + # Check if already configured + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={}) + + return self.async_show_form( + step_id="user", description_placeholders={"serial": serial} + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/goecontroller/const.py b/homeassistant/components/goecontroller/const.py new file mode 100644 index 00000000000..009c976ae6c --- /dev/null +++ b/homeassistant/components/goecontroller/const.py @@ -0,0 +1,3 @@ +"""Constants for the go-e Controller integration.""" + +DOMAIN = "goecontroller" diff --git a/homeassistant/components/goecontroller/manifest.json b/homeassistant/components/goecontroller/manifest.json new file mode 100644 index 00000000000..f2d9defdca5 --- /dev/null +++ b/homeassistant/components/goecontroller/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "goecontroller", + "name": "go-e Controller", + "after_dependencies": ["zeroconf"], + "codeowners": ["@0xFEEDC0DE64"], + "config_flow": true, + "dependencies": [], + "documentation": "https://www.home-assistant.io/integrations/goecontroller", + "integration_type": "device", + "iot_class": "local_push", + "requirements": [], + "zeroconf": [ + "_go-e_controller._tcp.local.", + "_go-e_controller-lite._tcp._tcp.local." + ] +} diff --git a/homeassistant/components/goecontroller/strings.json b/homeassistant/components/goecontroller/strings.json new file mode 100644 index 00000000000..bcdace30cb0 --- /dev/null +++ b/homeassistant/components/goecontroller/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "flow_title": "{serial}", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "mdns_missing_serial": "Missing serial number in MDNS properties." + } + } +} diff --git a/homeassistant/components/wattpilot/__init__.py b/homeassistant/components/wattpilot/__init__.py new file mode 100644 index 00000000000..30f12a32dfc --- /dev/null +++ b/homeassistant/components/wattpilot/__init__.py @@ -0,0 +1,48 @@ +"""The Fronius Wattpilot integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS: list[Platform] = [Platform.LIGHT] + + +async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the go-eCharger integration.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Fronius Wattpilot from a config entry.""" + + # zeroconf_instance = await zeroconf.async_get_instance(hass) + + hass.data.setdefault(DOMAIN, {}) + # TODO 1. Create API instance + # TODO 2. Validate the API connection (and authentication) + # TODO 3. Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/wattpilot/config_flow.py b/homeassistant/components/wattpilot/config_flow.py new file mode 100644 index 00000000000..86c9691964f --- /dev/null +++ b/homeassistant/components/wattpilot/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for Fronius Wattpilot integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("host"): str, + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +class PlaceholderHub: + """Placeholder class to make tests pass. + + TODO Remove this placeholder class and replace with things from your PyPI package. + """ + + def __init__(self, host: str) -> None: + """Initialize.""" + self.host = host + + async def authenticate(self, username: str, password: str) -> bool: + """Test if we can authenticate with the host.""" + return True + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + hub = PlaceholderHub(data["host"]) + + if not await hub.authenticate(data["username"], data["password"]): + raise InvalidAuth + + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": "Name of the device"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fronius Wattpilot.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + serial = discovery_info.properties.get("serial") + if serial is None: + return self.async_abort(reason="mdns_missing_serial") + + self.context["title_placeholders"] = {"name": serial} + + # Check if already configured + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={}) + + return self.async_show_form( + step_id="user", description_placeholders={"serial": serial} + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/wattpilot/const.py b/homeassistant/components/wattpilot/const.py new file mode 100644 index 00000000000..8ca929a4443 --- /dev/null +++ b/homeassistant/components/wattpilot/const.py @@ -0,0 +1,3 @@ +"""Constants for the Fronius Wattpilot integration.""" + +DOMAIN = "wattpilot" diff --git a/homeassistant/components/wattpilot/manifest.json b/homeassistant/components/wattpilot/manifest.json new file mode 100644 index 00000000000..ba020c91244 --- /dev/null +++ b/homeassistant/components/wattpilot/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "wattpilot", + "name": "Fronius Wattpilot", + "after_dependencies": ["zeroconf"], + "codeowners": ["@0xFEEDC0DE64"], + "config_flow": true, + "dependencies": [], + "documentation": "https://www.home-assistant.io/integrations/wattpilot", + "integration_type": "device", + "iot_class": "local_push", + "requirements": [], + "zeroconf": [ + "_fronius_wattpilot._tcp.local.", + "_fronius_wattpilot_V2._tcp.local." + ] +} diff --git a/homeassistant/components/wattpilot/strings.json b/homeassistant/components/wattpilot/strings.json new file mode 100644 index 00000000000..bcdace30cb0 --- /dev/null +++ b/homeassistant/components/wattpilot/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "flow_title": "{serial}", + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "mdns_missing_serial": "Missing serial number in MDNS properties." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6f6ce237904..c06f3eda1b9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -201,6 +201,8 @@ FLOWS = { "github", "glances", "goalzero", + "goecharger", + "goecontroller", "gogogate2", "goodwe", "google", @@ -599,6 +601,7 @@ FLOWS = { "vulcan", "wallbox", "waqi", + "wattpilot", "watttime", "waze_travel_time", "weatherflow", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e6a103989d1..e358c97f7fa 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2222,6 +2222,18 @@ "config_flow": true, "iot_class": "local_polling" }, + "goecharger": { + "name": "go-eCharger", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, + "goecontroller": { + "name": "go-e Controller", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "gogogate2": { "name": "Gogogate2 and ismartgate", "integration_type": "hub", @@ -6704,6 +6716,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "wattpilot": { + "name": "Fronius Wattpilot", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "watttime": { "name": "WattTime", "integration_type": "service", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 7b1bbff9de0..61a1ae7d0f8 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -465,6 +465,36 @@ ZEROCONF = { "domain": "freebox", }, ], + "_fronius_wattpilot._tcp.local.": [ + { + "domain": "wattpilot", + }, + ], + "_fronius_wattpilot_V2._tcp.local.": [ + { + "domain": "wattpilot", + }, + ], + "_go-e_controller-lite._tcp._tcp.local.": [ + { + "domain": "goecontroller", + }, + ], + "_go-e_controller._tcp.local.": [ + { + "domain": "goecontroller", + }, + ], + "_go-e_go-eCharger._tcp.local.": [ + { + "domain": "goecharger", + }, + ], + "_go-e_go-eCharger_V4._tcp.local.": [ + { + "domain": "goecharger", + }, + ], "_googlecast._tcp.local.": [ { "domain": "cast", diff --git a/tests/components/goecharger/__init__.py b/tests/components/goecharger/__init__.py new file mode 100644 index 00000000000..07f70ccfcfb --- /dev/null +++ b/tests/components/goecharger/__init__.py @@ -0,0 +1 @@ +"""Tests for the go-eCharger integration.""" diff --git a/tests/components/goecharger/conftest.py b/tests/components/goecharger/conftest.py new file mode 100644 index 00000000000..f2670430eb8 --- /dev/null +++ b/tests/components/goecharger/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the go-eCharger tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.goecharger.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/goecharger/test_config_flow.py b/tests/components/goecharger/test_config_flow.py new file mode 100644 index 00000000000..30a3cd66abb --- /dev/null +++ b/tests/components/goecharger/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the go-eCharger config flow.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.goecharger.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.goecharger.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.goecharger.config_flow.PlaceholderHub.authenticate", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Name of the device" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.goecharger.config_flow.PlaceholderHub.authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.goecharger.config_flow.PlaceholderHub.authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/goecontroller/__init__.py b/tests/components/goecontroller/__init__.py new file mode 100644 index 00000000000..f0e95ef5b04 --- /dev/null +++ b/tests/components/goecontroller/__init__.py @@ -0,0 +1 @@ +"""Tests for the go-e Controller integration.""" diff --git a/tests/components/goecontroller/conftest.py b/tests/components/goecontroller/conftest.py new file mode 100644 index 00000000000..3c894bdeb33 --- /dev/null +++ b/tests/components/goecontroller/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the go-e Controller tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.goecontroller.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/goecontroller/test_config_flow.py b/tests/components/goecontroller/test_config_flow.py new file mode 100644 index 00000000000..c27926cb808 --- /dev/null +++ b/tests/components/goecontroller/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the go-e Controller config flow.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.goecontroller.config_flow import ( + CannotConnect, + InvalidAuth, +) +from homeassistant.components.goecontroller.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.goecontroller.config_flow.PlaceholderHub.authenticate", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Name of the device" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.goecontroller.config_flow.PlaceholderHub.authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.goecontroller.config_flow.PlaceholderHub.authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/wattpilot/__init__.py b/tests/components/wattpilot/__init__.py new file mode 100644 index 00000000000..bdf623f3b4a --- /dev/null +++ b/tests/components/wattpilot/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fronius Wattpilot integration.""" diff --git a/tests/components/wattpilot/conftest.py b/tests/components/wattpilot/conftest.py new file mode 100644 index 00000000000..77584285d52 --- /dev/null +++ b/tests/components/wattpilot/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the Fronius Wattpilot tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.wattpilot.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/wattpilot/test_config_flow.py b/tests/components/wattpilot/test_config_flow.py new file mode 100644 index 00000000000..0b583dfd157 --- /dev/null +++ b/tests/components/wattpilot/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the Fronius Wattpilot config flow.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.wattpilot.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.wattpilot.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.wattpilot.config_flow.PlaceholderHub.authenticate", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Name of the device" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wattpilot.config_flow.PlaceholderHub.authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wattpilot.config_flow.PlaceholderHub.authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"}