Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,42 @@ It uses the https://pypi.org/project/pytechnicolor/ to retrieve devices
# configuration

Install as usual and then follow UI configuration to input your router IP address and credentials.

## Multiple routers

This integration supports configuring **more than one router**:

- **UI**: Add the integration multiple times (one config entry per router).
- **YAML import**: You can configure a single router (dictionary) or multiple routers (list). On startup, each router will be imported as a separate config entry.

### YAML examples

Single router:

```yaml
technicolor:
host: 192.168.0.1
port: 80
use_ssl: false
verify_ssl: true
username: admin
password: your_password
```

Multiple routers:

```yaml
technicolor:
- host: 192.168.1.1
port: 80
use_ssl: false
verify_ssl: true
username: admin
password: your_password
- host: 192.168.2.1
port: 80
use_ssl: false
verify_ssl: true
username: admin
password: your_password
```
36 changes: 25 additions & 11 deletions custom_components/technicolor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ async def async_setup(hass, config):
options = {}
hass.data[DOMAIN] = {"yaml_options": options}

# check if already configured
domains_list = hass.config_entries.async_domains()
if DOMAIN in domains_list:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
# Support importing one or multiple routers from YAML.
conf_list = conf if isinstance(conf, list) else [conf]
for router_conf in conf_list:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=router_conf
)
)
)

return True


async def _async_config_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Technicolor platform."""

Expand All @@ -43,13 +45,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if not entry.options and yaml_options:
hass.config_entries.async_update_entry(entry, options=yaml_options)

entry.async_on_unload(entry.add_update_listener(_async_config_entry_update))

technicolor_router = TechnicolorRouter(hass, entry)
await technicolor_router.setup()

hass.async_create_task(hass.config_entries.async_forward_entry_setups(entry, PLATFORMS))

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
DOMAIN: technicolor_router,
}

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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
router = hass.data[DOMAIN].pop(entry.entry_id, {}).get(DOMAIN)
if router:
router.async_unload()
return unload_ok
49 changes: 45 additions & 4 deletions custom_components/technicolor/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import voluptuous as vol

from homeassistant import config_entries
from .const import DOMAIN
from .const import CONF_USE_SSL, CONF_VERIFY_SSL, DOMAIN
from homeassistant.const import (
CONF_HOST,
CONF_PORT,
CONF_PASSWORD,
CONF_USERNAME,
)
Expand Down Expand Up @@ -40,6 +41,11 @@ def _show_setup_form(self, user_input=None, errors=None):
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
vol.Optional(CONF_PORT, default=user_input.get(CONF_PORT, 80)): int,
vol.Optional(CONF_USE_SSL, default=user_input.get(CONF_USE_SSL, False)): bool,
vol.Optional(
CONF_VERIFY_SSL, default=user_input.get(CONF_VERIFY_SSL, True)
): bool,
vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")): str,
vol.Required(CONF_PASSWORD): str,
}
Expand All @@ -49,13 +55,12 @@ def _show_setup_form(self, user_input=None, errors=None):

async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")

if user_input is None:
return self._show_setup_form(user_input)

self._host = user_input[CONF_HOST]
await self.async_set_unique_id(self._host)
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=self._host,
Expand All @@ -65,3 +70,39 @@ async def async_step_user(self, user_input=None):
async def async_step_import(self, user_input=None):
"""Import a config entry."""
return await self.async_step_user(user_input)

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> "TechnicolorOptionsFlowHandler":
return TechnicolorOptionsFlowHandler()


def _config_schema_defaults(entry: config_entries.ConfigEntry) -> dict:
merged = {**entry.data, **entry.options}
return {
CONF_PORT: merged.get(CONF_PORT, 80),
CONF_USE_SSL: merged.get(CONF_USE_SSL, False),
CONF_VERIFY_SSL: merged.get(CONF_VERIFY_SSL, True),
}


class TechnicolorOptionsFlowHandler(config_entries.OptionsFlow):
"""Options flow to change port / SSL for an existing config entry."""

async def async_step_init(self, user_input: dict | None = None):
if user_input is not None:
return self.async_create_entry(data=user_input)

defaults = _config_schema_defaults(self.config_entry)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(CONF_PORT, default=defaults[CONF_PORT]): int,
vol.Optional(CONF_USE_SSL, default=defaults[CONF_USE_SSL]): bool,
vol.Optional(CONF_VERIFY_SSL, default=defaults[CONF_VERIFY_SSL]): bool,
}
),
)
2 changes: 2 additions & 0 deletions custom_components/technicolor/const.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
"""Technicolor component constants."""
DOMAIN = "technicolor"
CONF_USE_SSL = "use_ssl"
CONF_VERIFY_SSL = "verify_ssl"
65 changes: 49 additions & 16 deletions custom_components/technicolor/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
CONF_DEVICES,
CONF_EXCLUDE,
CONF_HOST,
CONF_PORT,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
from .const import CONF_USE_SSL, CONF_VERIFY_SSL, DOMAIN
from homeassistant.helpers.dispatcher import async_dispatcher_connect

DEFAULT_DEVICE_NAME = "Unknown device"
Expand All @@ -26,15 +27,45 @@

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]),
}
),
DOMAIN: vol.Any(
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=80): int,
vol.Optional(CONF_USE_SSL, default=False): bool,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEVICES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
}
),
vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=80): int,
vol.Optional(CONF_USE_SSL, default=False): bool,
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEVICES, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
}
)
],
),
)
},
extra=vol.ALLOW_EXTRA,
)
Expand Down Expand Up @@ -63,7 +94,7 @@ def update_router():
@callback
def add_entities(router, async_add_entities, tracked):
"""Add new tracker entities from the gateway."""
_LOGGER.info(f"add_entities tracked ${tracked}")
_LOGGER.debug("add_entities tracked %s", tracked)
new_tracked = []

for mac, device in router.devices.items():
Expand All @@ -72,7 +103,7 @@ def add_entities(router, async_add_entities, tracked):

new_tracked.append(TechnicolorDeviceScanner(router, device))
tracked.add(mac)
_LOGGER.info(f"add_entities {mac}")
_LOGGER.debug("add_entities %s", mac)

if new_tracked:
async_add_entities(new_tracked, True)
Expand All @@ -93,13 +124,15 @@ def async_update_state(self) -> None:
"""Update the Technicolor device."""
device = self._router.devices[self._mac]
self._device['ip'] = device['ip']
_LOGGER.info(f"updating state for ${self._mac} with ip ${self._device['ip']}")
_LOGGER.debug(
"updating state for %s with ip %s", self._mac, self._device["ip"]
)
self._active = self._device['ip'] is not None and self._device['ip'] != ""

@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._device['mac']
return f"{self._router.entry_id}_{self._device['mac']}"

@property
def name(self) -> str:
Expand Down Expand Up @@ -149,13 +182,13 @@ def should_poll(self) -> bool:
@callback
def async_on_demand_update(self):
"""Update state."""
_LOGGER.info("in async_on_demand_update")
_LOGGER.debug("in async_on_demand_update")
self.async_update_state()
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Register state update callback."""
_LOGGER.info("in async_added_to_hass")
_LOGGER.debug("in async_added_to_hass")
self.async_update_state()
self.async_on_remove(
async_dispatcher_connect(
Expand Down
9 changes: 5 additions & 4 deletions custom_components/technicolor/manifest.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"domain": "technicolor",
"name": "Technicolor",
"documentation": "https://github.com/shaiu/technicolor",
"documentation": "https://github.com/arnonm/technicolor",
"requirements": ["pytechnicolor==1.1.12"],
"dependencies": [],
"codeowners": [
"@shaiu"
"@shaiu",
"@arnonm"
],
"iot_class": "local_polling",
"config_flow": true,
"version": "1.0.0",
"issue_tracker": "https://github.com/shaiu/technicolor/issues"
"version": "1.0.1",
"issue_tracker": "https://github.com/arnonm/technicolor/issues"
}
Loading