From df38c59dc85fbd2e7c0d8b40a74f9287e4872299 Mon Sep 17 00:00:00 2001
From: Pawel <pszafer@gmail.com>
Date: Sat, 7 Nov 2020 18:15:29 +0100
Subject: [PATCH] Add config flow to epson and fix timeouts (#39697)

---
 CODEOWNERS                                    |   1 +
 homeassistant/components/epson/__init__.py    |  71 +++++++++++-
 homeassistant/components/epson/config_flow.py |  45 ++++++++
 homeassistant/components/epson/const.py       |   6 +-
 homeassistant/components/epson/exceptions.py  |   6 +
 homeassistant/components/epson/manifest.json  |   9 +-
 .../components/epson/media_player.py          | 103 ++++++++----------
 homeassistant/components/epson/strings.json   |  20 ++++
 .../components/epson/translations/en.json     |  20 ++++
 homeassistant/generated/config_flows.py       |   1 +
 requirements_all.txt                          |   2 +-
 requirements_test_all.txt                     |   3 +
 tests/components/epson/__init__.py            |   1 +
 tests/components/epson/test_config_flow.py    |  91 ++++++++++++++++
 14 files changed, 309 insertions(+), 70 deletions(-)
 create mode 100644 homeassistant/components/epson/config_flow.py
 create mode 100644 homeassistant/components/epson/exceptions.py
 create mode 100644 homeassistant/components/epson/strings.json
 create mode 100644 homeassistant/components/epson/translations/en.json
 create mode 100644 tests/components/epson/__init__.py
 create mode 100644 tests/components/epson/test_config_flow.py

diff --git a/CODEOWNERS b/CODEOWNERS
index a3ce257eafc..e0096e7f217 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -134,6 +134,7 @@ homeassistant/components/enocean/* @bdurrer
 homeassistant/components/entur_public_transport/* @hfurubotten
 homeassistant/components/environment_canada/* @michaeldavie
 homeassistant/components/ephember/* @ttroy50
+homeassistant/components/epson/* @pszafer
 homeassistant/components/epsonworkforce/* @ThaStealth
 homeassistant/components/eq3btsmart/* @rytilahti
 homeassistant/components/esphome/* @OttoWinter
diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py
index eed342f77f9..0e0cf8fb247 100644
--- a/homeassistant/components/epson/__init__.py
+++ b/homeassistant/components/epson/__init__.py
@@ -1 +1,70 @@
-"""The epson component."""
+"""The epson integration."""
+import asyncio
+import logging
+
+from epson_projector import Projector
+from epson_projector.const import POWER
+
+from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_PLATFORM
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import DOMAIN
+from .exceptions import CannotConnect
+
+PLATFORMS = [MEDIA_PLAYER_PLATFORM]
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def validate_projector(hass: HomeAssistant, host, port):
+    """Validate the given host and port allows us to connect."""
+    epson_proj = Projector(
+        host=host,
+        websession=async_get_clientsession(hass, verify_ssl=False),
+        port=port,
+    )
+    _power = await epson_proj.get_property(POWER)
+    if not _power or _power == STATE_UNAVAILABLE:
+        raise CannotConnect
+    return epson_proj
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+    """Set up the epson component."""
+    hass.data.setdefault(DOMAIN, {})
+    return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Set up epson from a config entry."""
+    try:
+        projector = await validate_projector(
+            hass, entry.data[CONF_HOST], entry.data[CONF_PORT]
+        )
+    except CannotConnect:
+        _LOGGER.warning("Cannot connect to projector %s", entry.data[CONF_HOST])
+        return False
+    hass.data[DOMAIN][entry.entry_id] = projector
+    for component in PLATFORMS:
+        hass.async_create_task(
+            hass.config_entries.async_forward_entry_setup(entry, component)
+        )
+    return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+    """Unload a config entry."""
+    unload_ok = all(
+        await asyncio.gather(
+            *[
+                hass.config_entries.async_forward_entry_unload(entry, component)
+                for component in PLATFORMS
+            ]
+        )
+    )
+    if unload_ok:
+        hass.data[DOMAIN].pop(entry.entry_id)
+    return unload_ok
diff --git a/homeassistant/components/epson/config_flow.py b/homeassistant/components/epson/config_flow.py
new file mode 100644
index 00000000000..516ea1402b5
--- /dev/null
+++ b/homeassistant/components/epson/config_flow.py
@@ -0,0 +1,45 @@
+"""Config flow for epson integration."""
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+
+from . import validate_projector
+from .const import DOMAIN
+from .exceptions import CannotConnect
+
+DATA_SCHEMA = vol.Schema(
+    {
+        vol.Required(CONF_HOST): str,
+        vol.Required(CONF_NAME, default=DOMAIN): str,
+        vol.Required(CONF_PORT, default=80): int,
+    }
+)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+    """Handle a config flow for epson."""
+
+    VERSION = 1
+    CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+    async def async_step_import(self, import_config):
+        """Import a config entry from configuration.yaml."""
+        return await self.async_step_user(import_config)
+
+    async def async_step_user(self, user_input=None):
+        """Handle the initial step."""
+        errors = {}
+        if user_input is not None:
+            try:
+                await validate_projector(
+                    self.hass, user_input[CONF_HOST], user_input[CONF_PORT]
+                )
+                return self.async_create_entry(
+                    title=user_input.pop(CONF_NAME), data=user_input
+                )
+            except CannotConnect:
+                errors["base"] = "cannot_connect"
+        return self.async_show_form(
+            step_id="user", data_schema=DATA_SCHEMA, errors=errors
+        )
diff --git a/homeassistant/components/epson/const.py b/homeassistant/components/epson/const.py
index 23f3b081d01..cb227047f45 100644
--- a/homeassistant/components/epson/const.py
+++ b/homeassistant/components/epson/const.py
@@ -1,10 +1,8 @@
-"""Constants for the Epson projector component."""
+"""Constants for the epson integration."""
+
 DOMAIN = "epson"
 SERVICE_SELECT_CMODE = "select_cmode"
 
 ATTR_CMODE = "cmode"
 
-DATA_EPSON = "epson"
 DEFAULT_NAME = "EPSON Projector"
-
-SUPPORT_CMODE = 33001
diff --git a/homeassistant/components/epson/exceptions.py b/homeassistant/components/epson/exceptions.py
new file mode 100644
index 00000000000..d781a74f7c1
--- /dev/null
+++ b/homeassistant/components/epson/exceptions.py
@@ -0,0 +1,6 @@
+"""The errors of Epson integration."""
+from homeassistant import exceptions
+
+
+class CannotConnect(exceptions.HomeAssistantError):
+    """Error to indicate we cannot connect."""
diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json
index 909efd5893e..90f4baf24d2 100644
--- a/homeassistant/components/epson/manifest.json
+++ b/homeassistant/components/epson/manifest.json
@@ -1,7 +1,8 @@
 {
   "domain": "epson",
-  "name": "Epson",
+  "name": "epson",
+  "config_flow": true,
   "documentation": "https://www.home-assistant.io/integrations/epson",
-  "requirements": ["epson-projector==0.1.3"],
-  "codeowners": []
-}
+  "requirements": ["epson-projector==0.2.3"],
+  "codeowners": ["@pszafer"]
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py
index 235caa8d1a3..c7e4b4faed8 100644
--- a/homeassistant/components/epson/media_player.py
+++ b/homeassistant/components/epson/media_player.py
@@ -1,7 +1,6 @@
 """Support for Epson projector."""
 import logging
 
-import epson_projector as epson
 from epson_projector.const import (
     BACK,
     BUSY,
@@ -36,26 +35,19 @@ from homeassistant.components.media_player.const import (
     SUPPORT_VOLUME_MUTE,
     SUPPORT_VOLUME_STEP,
 )
+from homeassistant.config_entries import SOURCE_IMPORT
 from homeassistant.const import (
-    ATTR_ENTITY_ID,
     CONF_HOST,
     CONF_NAME,
     CONF_PORT,
-    CONF_SSL,
     STATE_OFF,
     STATE_ON,
+    STATE_UNAVAILABLE,
 )
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers import entity_platform
 import homeassistant.helpers.config_validation as cv
 
-from .const import (
-    ATTR_CMODE,
-    DATA_EPSON,
-    DEFAULT_NAME,
-    DOMAIN,
-    SERVICE_SELECT_CMODE,
-    SUPPORT_CMODE,
-)
+from .const import ATTR_CMODE, DEFAULT_NAME, DOMAIN, SERVICE_SELECT_CMODE
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -63,86 +55,70 @@ SUPPORT_EPSON = (
     SUPPORT_TURN_ON
     | SUPPORT_TURN_OFF
     | SUPPORT_SELECT_SOURCE
-    | SUPPORT_CMODE
     | SUPPORT_VOLUME_MUTE
     | SUPPORT_VOLUME_STEP
     | SUPPORT_NEXT_TRACK
     | SUPPORT_PREVIOUS_TRACK
 )
 
-MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids})
-
 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
     {
         vol.Required(CONF_HOST): cv.string,
         vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
         vol.Optional(CONF_PORT, default=80): cv.port,
-        vol.Optional(CONF_SSL, default=False): cv.boolean,
     }
 )
 
 
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
-    """Set up the Epson media player platform."""
-    if DATA_EPSON not in hass.data:
-        hass.data[DATA_EPSON] = []
-
-    name = config.get(CONF_NAME)
-    host = config.get(CONF_HOST)
-    port = config.get(CONF_PORT)
-    ssl = config[CONF_SSL]
-
-    epson_proj = EpsonProjector(
-        async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    """Set up the Epson projector from a config entry."""
+    unique_id = config_entry.entry_id
+    projector = hass.data[DOMAIN][unique_id]
+    projector_entity = EpsonProjectorMediaPlayer(
+        projector, config_entry.title, unique_id
     )
-
-    hass.data[DATA_EPSON].append(epson_proj)
-    async_add_entities([epson_proj], update_before_add=True)
-
-    async def async_service_handler(service):
-        """Handle for services."""
-        entity_ids = service.data.get(ATTR_ENTITY_ID)
-        if entity_ids:
-            devices = [
-                device
-                for device in hass.data[DATA_EPSON]
-                if device.entity_id in entity_ids
-            ]
-        else:
-            devices = hass.data[DATA_EPSON]
-        for device in devices:
-            if service.service == SERVICE_SELECT_CMODE:
-                cmode = service.data.get(ATTR_CMODE)
-                await device.select_cmode(cmode)
-            device.async_schedule_update_ha_state(True)
-
-    epson_schema = MEDIA_PLAYER_SCHEMA.extend(
-        {vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))}
+    async_add_entities([projector_entity], True)
+    platform = entity_platform.current_platform.get()
+    platform.async_register_entity_service(
+        SERVICE_SELECT_CMODE,
+        {vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))},
+        SERVICE_SELECT_CMODE,
     )
-    hass.services.async_register(
-        DOMAIN, SERVICE_SELECT_CMODE, async_service_handler, schema=epson_schema
+    return True
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+    """Set up the Epson projector."""
+    hass.async_create_task(
+        hass.config_entries.flow.async_init(
+            DOMAIN, context={"source": SOURCE_IMPORT}, data=config
+        )
     )
 
 
-class EpsonProjector(MediaPlayerEntity):
+class EpsonProjectorMediaPlayer(MediaPlayerEntity):
     """Representation of Epson Projector Device."""
 
-    def __init__(self, websession, name, host, port, encryption):
+    def __init__(self, projector, name, unique_id):
         """Initialize entity to control Epson projector."""
         self._name = name
-        self._projector = epson.Projector(host, websession=websession, port=port)
+        self._projector = projector
         self._cmode = None
         self._source_list = list(DEFAULT_SOURCES.values())
         self._source = None
         self._volume = None
         self._state = None
+        self._unique_id = unique_id
 
     async def async_update(self):
         """Update state of device."""
-        is_turned_on = await self._projector.get_property(POWER)
-        _LOGGER.debug("Project turn on/off status: %s", is_turned_on)
-        if is_turned_on and is_turned_on == EPSON_CODES[POWER]:
+        power_state = await self._projector.get_property(POWER)
+        _LOGGER.debug("Projector status: %s", power_state)
+        if not power_state:
+            return
+        if power_state == EPSON_CODES[POWER]:
             self._state = STATE_ON
+            self._source_list = list(DEFAULT_SOURCES.values())
             cmode = await self._projector.get_property(CMODE)
             self._cmode = CMODE_LIST.get(cmode, self._cmode)
             source = await self._projector.get_property(SOURCE)
@@ -150,8 +126,10 @@ class EpsonProjector(MediaPlayerEntity):
             volume = await self._projector.get_property(VOLUME)
             if volume:
                 self._volume = volume
-        elif is_turned_on == BUSY:
+        elif power_state == BUSY:
             self._state = STATE_ON
+        elif power_state == STATE_UNAVAILABLE:
+            self._state = STATE_UNAVAILABLE
         else:
             self._state = STATE_OFF
 
@@ -160,6 +138,11 @@ class EpsonProjector(MediaPlayerEntity):
         """Return the name of the device."""
         return self._name
 
+    @property
+    def unique_id(self):
+        """Return unique ID."""
+        return self._unique_id
+
     @property
     def state(self):
         """Return the state of the device."""
diff --git a/homeassistant/components/epson/strings.json b/homeassistant/components/epson/strings.json
new file mode 100644
index 00000000000..358e3549f85
--- /dev/null
+++ b/homeassistant/components/epson/strings.json
@@ -0,0 +1,20 @@
+{
+  "title": "Epson Projector",
+  "config": {
+    "step": {
+      "user": {
+        "data": {
+          "host": "[%key:common::config_flow::data::host%]",
+          "name": "[%key:common::config_flow::data::name%]",
+          "port": "[%key:common::config_flow::data::port%]"
+        }
+      }
+    },
+    "error": {
+      "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+    },
+    "abort": {
+      "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+    }
+  }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json
new file mode 100644
index 00000000000..98d9bb73482
--- /dev/null
+++ b/homeassistant/components/epson/translations/en.json
@@ -0,0 +1,20 @@
+{
+    "config": {
+        "abort": {
+            "already_configured": "Device is already configured"
+        },
+        "error": {
+            "cannot_connect": "Failed to connect"
+        },
+        "step": {
+            "user": {
+                "data": {
+                    "host": "Host",
+                    "name": "Name",
+                    "port": "Port"
+                }
+            }
+        }
+    },
+    "title": "Epson Projector"
+}
\ No newline at end of file
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index f94c76417b4..3ac7fbae020 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -55,6 +55,7 @@ FLOWS = [
     "elkm1",
     "emulated_roku",
     "enocean",
+    "epson",
     "esphome",
     "flick_electric",
     "flo",
diff --git a/requirements_all.txt b/requirements_all.txt
index 712935ba599..92295f2677d 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -565,7 +565,7 @@ envoy_reader==0.16.2
 ephem==3.7.7.0
 
 # homeassistant.components.epson
-epson-projector==0.1.3
+epson-projector==0.2.3
 
 # homeassistant.components.epsonworkforce
 epsonprinter==0.0.9
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e2275978515..b118d7424f8 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -295,6 +295,9 @@ enocean==0.50
 # homeassistant.components.season
 ephem==3.7.7.0
 
+# homeassistant.components.epson
+epson-projector==0.2.3
+
 # homeassistant.components.feedreader
 feedparser-homeassistant==5.2.2.dev1
 
diff --git a/tests/components/epson/__init__.py b/tests/components/epson/__init__.py
new file mode 100644
index 00000000000..545400bf3e6
--- /dev/null
+++ b/tests/components/epson/__init__.py
@@ -0,0 +1 @@
+"""Tests for the epson integration."""
diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py
new file mode 100644
index 00000000000..4353971c669
--- /dev/null
+++ b/tests/components/epson/test_config_flow.py
@@ -0,0 +1,91 @@
+"""Test the epson config flow."""
+from homeassistant import config_entries, setup
+from homeassistant.components.epson.const import DOMAIN
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE
+
+from tests.async_mock import patch
+
+
+async def test_form(hass):
+    """Test we get the form."""
+    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"
+    assert result["errors"] == {}
+    assert result["step_id"] == config_entries.SOURCE_USER
+
+    with patch(
+        "homeassistant.components.epson.Projector.get_property",
+        return_value="04",
+    ), patch(
+        "homeassistant.components.epson.async_setup", return_value=True
+    ) as mock_setup, patch(
+        "homeassistant.components.epson.async_setup_entry",
+        return_value=True,
+    ) as mock_setup_entry:
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80},
+        )
+    assert result2["type"] == "create_entry"
+    assert result2["title"] == "test-epson"
+    assert result2["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80}
+    await hass.async_block_till_done()
+    assert len(mock_setup.mock_calls) == 1
+    assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_cannot_connect(hass):
+    """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.epson.Projector.get_property",
+        return_value=STATE_UNAVAILABLE,
+    ):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80},
+        )
+
+    assert result2["type"] == "form"
+    assert result2["errors"] == {"base": "cannot_connect"}
+
+
+async def test_import(hass):
+    """Test config.yaml import."""
+    with patch(
+        "homeassistant.components.epson.Projector.get_property",
+        return_value="04",
+    ):
+        result = await hass.config_entries.flow.async_init(
+            DOMAIN,
+            context={"source": config_entries.SOURCE_IMPORT},
+            data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80},
+        )
+        assert result["type"] == "create_entry"
+        assert result["title"] == "test-epson"
+        assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80}
+
+
+async def test_import_cannot_connect(hass):
+    """Test we handle cannot connect error with import."""
+    result = await hass.config_entries.flow.async_init(
+        DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
+    )
+
+    with patch(
+        "homeassistant.components.epson.Projector.get_property",
+        return_value=STATE_UNAVAILABLE,
+    ):
+        result2 = await hass.config_entries.flow.async_configure(
+            result["flow_id"],
+            {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80},
+        )
+
+    assert result2["type"] == "form"
+    assert result2["errors"] == {"base": "cannot_connect"}
-- 
GitLab