Skip to content

Commit

Permalink
cflow: add new option for experimental feautres
Browse files Browse the repository at this point in the history
entitybase: add services for approve/deny requests
update translations
  • Loading branch information
pantherale0 authored Nov 9, 2024
1 parent ee0158a commit d01019b
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 48 deletions.
12 changes: 8 additions & 4 deletions custom_components/family_safety/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@
HomeAssistantError
)

from .const import DOMAIN, AGG_ERROR
from .const import DOMAIN, AGG_ERROR, CONF_EXPR_DEFAULT, CONF_KEY_EXPR
from .coordinator import FamilySafetyCoordinator

_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Create ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
_LOGGER.debug("Got request to setup entry.")
try:
familysafety = await FamilySafety.create(
token=entry.options.get("refresh_token", entry.data["refresh_token"]),
use_refresh_token=True
token=entry.options.get(
"refresh_token", entry.options.get("refresh_token", entry.data.get("refresh_token"))),
use_refresh_token=True,
experimental=entry.options.get(CONF_KEY_EXPR, CONF_EXPR_DEFAULT)
)
_LOGGER.debug("Login successful, setting up coordinator.")
hass.data[DOMAIN][entry.entry_id] = FamilySafetyCoordinator(
Expand All @@ -53,9 +56,9 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
entry.async_on_unload(entry.add_update_listener(update_listener))

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."""
_LOGGER.debug("Unloading config entry %s", entry.entry_id)
Expand All @@ -65,5 +68,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

return unload_ok


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
72 changes: 47 additions & 25 deletions custom_components/family_safety/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector

from .const import DOMAIN
from .const import DOMAIN, CONF_EXPR_DEFAULT, CONF_KEY_EXPR

_LOGGER = logging.getLogger(__name__)

Expand All @@ -28,22 +28,27 @@
}
)


def _get_application_id(name: str, applications: list[Application]):
"""Return the single application ID."""
return [a for a in applications if a.name == name][0].app_id


def _convert_applications(applications: list[Application]):
"""Convert a list of applications to an array for options."""
return [a.name for a in applications]


def _convert_accounts(accounts: list[Account]):
"""Convert a list of accounts to an array for options."""
return [f"{a.first_name} {a.surname}" for a in accounts]


def _get_account_id(name: str, accounts: list[Account]):
"""Return the account ID."""
return [a for a in accounts if (f"{a.first_name} {a.surname}" == name)][0].user_id


async def validate_input(data: dict[str, Any]) -> dict[str, Any]:
"""Validate the input."""
auth: Authenticator = None
Expand All @@ -60,12 +65,14 @@ async def validate_input(data: dict[str, Any]) -> dict[str, Any]:
_LOGGER.error(err)
raise CannotConnect from err

_LOGGER.debug("Authentication success, expiry time %s, returning refresh_token.", auth.expires)
_LOGGER.debug(
"Authentication success, expiry time %s, returning refresh_token.", auth.expires)
return {
"title": "Microsoft Family Safety",
"refresh_token": auth.refresh_token
}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

Expand Down Expand Up @@ -102,13 +109,11 @@ async def async_step_user(
errors=errors
)

class OptionsFlow(config_entries.OptionsFlow):

class OptionsFlow(config_entries.OptionsFlowWithConfigEntry):
"""An options flow for HASS."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self.family_safety: FamilySafety = None
family_safety: FamilySafety = None

def _get_config_entry(self, key):
"""Return the specific config entry."""
Expand All @@ -119,7 +124,7 @@ def _get_config_entry(self, key):
config = self.config_entry.options.get(key)
return config

async def _async_create_entry(self, **kwargs) -> config_entries.FlowResult:
async def async_create_entry(self, **kwargs) -> config_entries.FlowResult:
"""Create an entry using optional overrides."""
update_interval = self._get_config_entry("update_interval")
if kwargs.get("update_interval", None) is not None:
Expand All @@ -143,15 +148,23 @@ async def _async_create_entry(self, **kwargs) -> config_entries.FlowResult:
if accounts is None:
accounts = []

expr = self._get_config_entry(CONF_KEY_EXPR)
if kwargs.get(CONF_KEY_EXPR, None) is not None:
expr = kwargs[CONF_KEY_EXPR]
if expr is None:
expr = CONF_EXPR_DEFAULT

await self.family_safety.api.end_session()
return self.async_create_entry(
self.options.update({
"refresh_token": refresh_token,
"update_interval": update_interval,
"tracked_applications": tracked_applications,
"accounts": accounts,
CONF_KEY_EXPR: expr
})
return super().async_create_entry(
title=self.config_entry.title,
data={
"refresh_token": refresh_token,
"update_interval": update_interval,
"tracked_applications": tracked_applications,
"accounts": accounts
}
data=self.options
)

async def async_step_auth(
Expand All @@ -166,11 +179,13 @@ async def async_step_auth(

refresh_token = self.config_entry.data["refresh_token"]
if self.config_entry.options:
refresh_token = self.config_entry.options.get("refresh_token", refresh_token)
refresh_token = self.config_entry.options.get(
"refresh_token", refresh_token)

update_interval = self.config_entry.data["update_interval"]
if self.config_entry.options:
update_interval = self.config_entry.options.get("update_interval", update_interval)
update_interval = self.config_entry.options.get(
"update_interval", update_interval)

return self.async_show_form(
step_id="auth",
Expand All @@ -191,8 +206,9 @@ async def async_step_applications(
tracked_applications = []
applications = self.family_safety.accounts[0].applications
for app in user_input.get("tracked_applications", []):
tracked_applications.append(_get_application_id(app, applications))
return await self._async_create_entry(
tracked_applications.append(
_get_application_id(app, applications))
return await self.async_create_entry(
tracked_applications=tracked_applications
)

Expand All @@ -212,7 +228,8 @@ async def async_step_applications(
vol.Optional("tracked_applications",
default=default_tracked_applications): selector.SelectSelector(
selector.SelectSelectorConfig(
options=_convert_applications(self.family_safety.accounts[0].applications),
options=_convert_applications(
self.family_safety.accounts[0].applications),
custom_value=False,
multiple=True)
)
Expand All @@ -231,8 +248,9 @@ async def async_step_accounts(
tracked_user_ids.append(
_get_account_id(user, self.family_safety.accounts)
)
return await self._async_create_entry(
accounts=tracked_user_ids
return await self.async_create_entry(
accounts=tracked_user_ids,
experimental=user_input.get(CONF_KEY_EXPR, CONF_EXPR_DEFAULT)
)

default_tracked_accounts = []
Expand All @@ -242,7 +260,8 @@ async def async_step_accounts(
for account in tracked_accounts:
with contextlib.suppress(IndexError):
acc = self.family_safety.get_account(account)
default_tracked_accounts.append(f"{acc.first_name} {acc.surname}")
default_tracked_accounts.append(
f"{acc.first_name} {acc.surname}")

return self.async_show_form(
step_id="accounts",
Expand All @@ -251,11 +270,13 @@ async def async_step_accounts(
vol.Optional("accounts",
default=default_tracked_accounts): selector.SelectSelector(
selector.SelectSelectorConfig(
options=_convert_accounts(self.family_safety.accounts),
options=_convert_accounts(
self.family_safety.accounts),
custom_value=False,
multiple=True
)
)
),
vol.Optional(CONF_KEY_EXPR, default=CONF_EXPR_DEFAULT): selector.BooleanSelector()
}
)
)
Expand All @@ -273,6 +294,7 @@ async def async_step_init(
menu_options=["auth", "applications", "accounts"]
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

Expand Down
3 changes: 3 additions & 0 deletions custom_components/family_safety/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
NAME = "Microsoft Family Safety"
DOMAIN = "family_safety"

CONF_KEY_EXPR = "experimental"
CONF_EXPR_DEFAULT = False

DEFAULT_OVERRIDE_ENTITIES = [
OverrideTarget.MOBILE,
OverrideTarget.WINDOWS,
Expand Down
24 changes: 24 additions & 0 deletions custom_components/family_safety/entity_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.exceptions import ServiceValidationError

from .const import DOMAIN
from .coordinator import FamilySafetyCoordinator
Expand Down Expand Up @@ -60,6 +61,29 @@ async def async_unblock_application(self, name: str):
"""Blocks a application with a given app name."""
await [a for a in self._account.applications if a.name == name][0].unblock_app()

async def async_approve_request(self, request_id: str, extension_time: int):
"""Approve a pending request."""
try:
await self.coordinator.api.approve_pending_request(
request_id=request_id,
extension_time=extension_time
)
except ValueError:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_request_id"
)

async def async_deny_request(self, request_id: str):
"""Deny a pending request."""
try:
await self.coordinator.api.deny_pending_request(request_id=request_id)
except ValueError:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_request_id"
)


class ApplicationEntity(ManagedAccountEntity):
"""Define a application entity."""
Expand Down
55 changes: 46 additions & 9 deletions custom_components/family_safety/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from dataclasses import dataclass
from datetime import datetime
import logging
from typing import Generic, Any
from typing import Any

import voluptuous as vol

Expand All @@ -21,7 +21,7 @@

from .coordinator import FamilySafetyCoordinator

from .const import DOMAIN
from .const import DOMAIN, CONF_KEY_EXPR, CONF_EXPR_DEFAULT

from .entity_base import ManagedAccountEntity

Expand All @@ -31,6 +31,7 @@
@dataclass(frozen=True, kw_only=True)
class FamilySafetySensorEntityDescription(SensorEntityDescription):
"""Describes family_safety sensor entity."""

value_fn: Callable[[ManagedAccountEntity], str | int | datetime]
name_fn: Callable[[ManagedAccountEntity], str]
native_unit_of_measurement_fn: Callable[[ManagedAccountEntity], str]
Expand All @@ -43,7 +44,10 @@ class FamilySafetySensorEntityDescription(SensorEntityDescription):
device_class=SensorDeviceClass.MONETARY,
name_fn=lambda data: f"{data._account.first_name} Available Balance",
native_unit_of_measurement_fn=lambda data: data._account.account_currency,
),
)
}

EXPR_SENSORS: dict = {
"pending_requests": FamilySafetySensorEntityDescription(
key="pending_requests",
value_fn=lambda data: len(
Expand Down Expand Up @@ -91,13 +95,30 @@ async def async_setup_entry(
account_id=account.user_id
))
entities.extend(
[ScreentimeSensor(coordinator=hass.data[DOMAIN]
[config_entry.entry_id], idx=None, account_id=account.user_id, description=desc) for desc in TIME_SENSORS.values()]
[ScreentimeSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id,
description=desc
) for desc in TIME_SENSORS.values()]
)
entities.extend(
[GenericSensor(coordinator=hass.data[DOMAIN]
[config_entry.entry_id], idx=None, account_id=account.user_id, description=desc) for desc in GEN_SENSORS.values()]
[GenericSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id,
description=desc
) for desc in GEN_SENSORS.values()]
)
if config_entry.options.get(CONF_KEY_EXPR, CONF_EXPR_DEFAULT):
entities.extend(
[GenericSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id,
description=desc
) for desc in EXPR_SENSORS.values()]
)

async_add_entities(entities, True)
# register services
Expand All @@ -112,13 +133,29 @@ async def async_setup_entry(
schema={vol.Required("name"): str},
func="async_unblock_application",
)
if config_entry.options.get(CONF_KEY_EXPR, CONF_EXPR_DEFAULT):
platform.async_register_entity_service(
name="approve_request",
schema={
vol.Required("request_id"): str,
vol.Required("extension_time"): int
},
func="async_approve_request"
)
platform.async_register_entity_service(
name="deny_request",
schema={
vol.Required("request_id"): str
},
func="async_deny_request"
)


class GenericSensor(ManagedAccountEntity, SensorEntity):
"""Generic Sensor."""
"""Use a Basic Sensor."""

def __init__(self, coordinator: FamilySafetyCoordinator, description: FamilySafetySensorEntityDescription, idx, account_id) -> None:
"""Generic Sensor."""
"""Use a Basic Sensor."""
super().__init__(coordinator, idx, account_id, description.key)
self.entity_description = description

Expand Down
Loading

0 comments on commit d01019b

Please sign in to comment.