Skip to content

Commit

Permalink
Add UI config flow
Browse files Browse the repository at this point in the history
Restore force_update returning False.
  • Loading branch information
pnbruckner committed Jan 12, 2024
1 parent bb58125 commit 4247e93
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 4 deletions.
257 changes: 253 additions & 4 deletions custom_components/composite/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
"""Config flow for Composite integration."""
from __future__ import annotations

from abc import abstractmethod
from typing import Any

from homeassistant.config_entries import ConfigFlow
import voluptuous as vol

from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN
from homeassistant.config_entries import (
SOURCE_IMPORT,
ConfigEntry,
ConfigFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_ENTITY_ID, CONF_ID, CONF_NAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowHandler, FlowResult
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.selector import (
BooleanSelector,
EntitySelector,
EntitySelectorConfig,
TextSelector,
)
from homeassistant.util import slugify

from .const import CONF_REQ_MOVEMENT, DOMAIN
from .const import (
CONF_ALL_STATES,
CONF_ENTITY,
CONF_REQ_MOVEMENT,
CONF_USE_PICTURE,
DOMAIN,
)


def split_conf(conf: dict[str, Any]) -> dict[str, dict[str, Any]]:
Expand All @@ -21,11 +45,174 @@ def split_conf(conf: dict[str, Any]) -> dict[str, dict[str, Any]]:
}


class CompositeConfigFlow(ConfigFlow, domain=DOMAIN):
class CompositeFlow(FlowHandler):
"""Composite flow mixin."""

_existing_entries: list[ConfigEntry] | None = None

@property
def _entries(self) -> list[ConfigEntry]:
"""Get existing config entries."""
if self._existing_entries is None:
self._existing_entries = self.hass.config_entries.async_entries(DOMAIN)
return self._existing_entries

@property
@abstractmethod
def options(self) -> dict[str, Any]:
"""Return mutable copy of options."""

@property
def _entity_ids(self) -> list[str]:
"""Get currently configured entity IDs."""
return [cfg[CONF_ENTITY] for cfg in self.options[CONF_ENTITY_ID]]

async def async_step_options(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Get config options."""
errors = {}

if user_input is not None:
self.options[CONF_REQ_MOVEMENT] = user_input[CONF_REQ_MOVEMENT]
prv_cfgs = {
cfg[CONF_ENTITY]: cfg for cfg in self.options.get(CONF_ENTITY_ID, [])
}
new_cfgs: list[dict[str, Any]] = []
for entity_id in user_input[CONF_ENTITY_ID]:
new_cfgs.append(
prv_cfgs.get(
entity_id,
{
CONF_ENTITY: entity_id,
CONF_USE_PICTURE: False,
CONF_ALL_STATES: False,
},
)
)
self.options[CONF_ENTITY_ID] = new_cfgs
if new_cfgs:
return await self.async_step_use_picture()
errors[CONF_ENTITY_ID] = "at_least_one_entity"

data_schema = vol.Schema(
{
vol.Required(CONF_ENTITY_ID): EntitySelector(
EntitySelectorConfig(
domain=["binary_sensor", "device_tracker"],
multiple=True,
)
),
vol.Required(CONF_REQ_MOVEMENT): BooleanSelector(),
}
)
if CONF_ENTITY_ID in self.options:
data_schema = self.add_suggested_values_to_schema(
data_schema,
{
CONF_ENTITY_ID: self._entity_ids,
CONF_REQ_MOVEMENT: self.options[CONF_REQ_MOVEMENT],
},
)
return self.async_show_form(
step_id="options", data_schema=data_schema, errors=errors
)

async def async_step_use_picture(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Specify which input to get composite's picture from."""
if user_input is not None:
entity_id = user_input.get(CONF_ENTITY)
for cfg in self.options[CONF_ENTITY_ID]:
cfg[CONF_USE_PICTURE] = cfg[CONF_ENTITY] == entity_id
return await self.async_step_all_states()

data_schema = vol.Schema(
{
vol.Optional(CONF_ENTITY): EntitySelector(
EntitySelectorConfig(include_entities=self._entity_ids)
)
}
)
picture_entity_id = None
for cfg in self.options[CONF_ENTITY_ID]:
if cfg[CONF_USE_PICTURE]:
picture_entity_id = cfg[CONF_ENTITY]
break
if picture_entity_id:
data_schema = self.add_suggested_values_to_schema(
data_schema, {CONF_ENTITY: picture_entity_id}
)
return self.async_show_form(step_id="use_picture", data_schema=data_schema)

async def async_step_all_states(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Specify if all states should be used for appropriate entities."""
if user_input is not None:
for cfg in self.options[CONF_ENTITY_ID]:
cfg[CONF_ALL_STATES] = cfg[CONF_ENTITY] in user_input[CONF_ENTITY]
return await self.async_step_done()

data_schema = vol.Schema(
{
vol.Required(CONF_ENTITY): EntitySelector(
EntitySelectorConfig(
include_entities=self._entity_ids, multiple=True
)
)
}
)
all_state_entities = [
cfg[CONF_ENTITY]
for cfg in self.options[CONF_ENTITY_ID]
if cfg[CONF_ALL_STATES]
]
if all_state_entities:
data_schema = self.add_suggested_values_to_schema(
data_schema, {CONF_ENTITY: all_state_entities}
)
return self.async_show_form(step_id="all_states", data_schema=data_schema)

@abstractmethod
async def async_step_done(self, _: dict[str, Any] | None = None) -> FlowResult:
"""Finish the flow."""


class CompositeConfigFlow(ConfigFlow, CompositeFlow, domain=DOMAIN):
"""Composite config flow."""

VERSION = 1

_name = ""
_id: str

def __init__(self) -> None:
"""Initialize config flow."""
self._options: dict[str, Any] = {}

@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry) -> CompositeOptionsFlow:
"""Get the options flow for this handler."""
flow = CompositeOptionsFlow(config_entry)
flow.init_step = "options"
return flow

@classmethod
@callback
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
"""Return options flow support for this handler."""
if config_entry.source == SOURCE_IMPORT:
return False
return True

@property
def options(self) -> dict[str, Any]:
"""Return mutable copy of options."""
return self._options

async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
"""Import config entry from configuration."""
if existing_entry := await self.async_set_unique_id(data[CONF_ID]):
Expand All @@ -38,3 +225,65 @@ async def async_step_import(self, data: dict[str, Any]) -> FlowResult:
title=f"{data[CONF_NAME]} (from configuration)",
**split_conf(data), # type: ignore[arg-type]
)

async def async_step_user(self, _: dict[str, Any] | None = None) -> FlowResult:
"""Start user config flow."""
return await self.async_step_name()

async def async_step_name(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Get name."""
errors = {}

if user_input is not None:
self._name = user_input[CONF_NAME]
if not any(self._name == entry.data[CONF_NAME] for entry in self._entries):
self._id = f"composite_{slugify(self._name)}"
return await self.async_step_id()
errors[CONF_NAME] = "name_used"

data_schema = vol.Schema({vol.Required(CONF_NAME): TextSelector()})
data_schema = self.add_suggested_values_to_schema(
data_schema, {CONF_NAME: self._name}
)
return self.async_show_form(
step_id="name", data_schema=data_schema, errors=errors, last_step=False
)

async def async_step_id(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Get object ID."""
errors = {}

if user_input is not None:
self._id = user_input[CONF_ID]
registry = er.async_get(self.hass)
if not registry.async_is_registered(f"{DT_DOMAIN}.{self._id}"):
return await self.async_step_options()
errors[CONF_ID] = "id_used"

data_schema = vol.Schema({vol.Required(CONF_ID): TextSelector()})
data_schema = self.add_suggested_values_to_schema(
data_schema, {CONF_ID: self._id}
)
return self.async_show_form(
step_id="id", data_schema=data_schema, errors=errors, last_step=False
)

async def async_step_done(self, _: dict[str, Any] | None = None) -> FlowResult:
"""Finish the flow."""
return self.async_create_entry(
title=self._name,
data={CONF_NAME: self._name, CONF_ID: self._id},
options=self.options,
)


class CompositeOptionsFlow(OptionsFlowWithConfigEntry, CompositeFlow):
"""Composite integration options flow."""

async def async_step_done(self, _: dict[str, Any] | None = None) -> FlowResult:
"""Finish the flow."""
return self.async_create_entry(title="", data=self.options or {})
5 changes: 5 additions & 0 deletions custom_components/composite/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ def __init__(self, name: str, obj_id: str) -> None:
self._attr_extra_state_attributes = {}
self._entities: dict[str, EntityData] = {}

@property
def force_update(self) -> bool:
"""Return True if state updates should be forced."""
return False

@property
def battery_level(self) -> int | None:
"""Return the battery level of the device."""
Expand Down
1 change: 1 addition & 0 deletions custom_components/composite/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"domain": "composite",
"name": "Composite",
"config_flow": true,
"codeowners": ["@pnbruckner"],
"dependencies": [],
"documentation": "https://github.com/pnbruckner/ha-composite-tracker/blob/master/README.md",
Expand Down
70 changes: 70 additions & 0 deletions custom_components/composite/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
{
"title": "Composite",
"config": {
"step": {
"all_states": {
"title": "Use All States",
"description": "Select entities for which all states should be used.\nNote that this only applies to entities whose source type is not GPS.",
"data": {
"entity": "Entity"
}
},
"id": {
"title": "Object Identifier",
"data": {
"id": "Object ID"
}
},
"name": {
"title": "Name",
"data": {
"name": "Name"
}
},
"options": {
"title": "Composite Options",
"data": {
"entity_id": "Input entities",
"require_movement": "Require movement"
}
},
"use_picture": {
"title": "Picture Entity",
"description": "Choose entity whose picture will be used for the composite.\nIt may be none.",
"data": {
"entity": "Entity"
}
}
},
"error": {
"at_least_one_entity": "Must select at least one input entity.",
"id_used": "Object identifier has already been used.",
"name_used": "Name has already been used."
}
},
"entity": {
"device_tracker": {
"tracker": {
Expand All @@ -19,6 +61,34 @@
}
}
},
"options": {
"step": {
"all_states": {
"title": "Use All States",
"description": "Select entities for which all states should be used.\nNote that this only applies to entities whose source type is not GPS.",
"data": {
"entity": "Entity"
}
},
"options": {
"title": "Composite Options",
"data": {
"entity_id": "Input entities",
"require_movement": "Require movement"
}
},
"use_picture": {
"title": "Picture Entity",
"description": "Choose entity whose picture will be used for the composite.\nIt may be none.",
"data": {
"entity": "Entity"
}
}
},
"error": {
"at_least_one_entity": "Must select at least one input entity."
}
},
"services": {
"reload": {
"name": "Reload",
Expand Down
Loading

0 comments on commit 4247e93

Please sign in to comment.