Skip to content

Commit

Permalink
1.1.0b1
Browse files Browse the repository at this point in the history
Add options to limit accounts that entities are registered for
Add application specific screentime sensors
Add options to create application specific screentime sensors
Bump pyfamilysafety to 0.1.1
  • Loading branch information
pantherale0 committed Sep 19, 2023
1 parent 85eed7a commit dba8bf5
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 38 deletions.
11 changes: 9 additions & 2 deletions custom_components/family_safety/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Got request to setup entry.")
try:
familysafety = await FamilySafety.create(
token=entry.data["refresh_token"],
token=entry.options.get("refresh_token", entry.data["refresh_token"]),
use_refresh_token=True
)
_LOGGER.debug("Login successful, setting up coordinator.")
hass.data[DOMAIN][entry.entry_id] = FamilySafetyCoordinator(
hass,
familysafety,
entry.data["update_interval"])
entry.options.get("update_interval", entry.data["update_interval"]))
# no need to fetch initial data as this is already handled on creation
except HttpException as err:
_LOGGER.error(err)
Expand All @@ -41,13 +41,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error(err)
raise CannotConnect from err

async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update listener"""
await hass.config_entries.async_reload(entry.entry_id)

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)
hass.data[DOMAIN][entry.entry_id].api.end_session()
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

Expand Down
187 changes: 170 additions & 17 deletions custom_components/family_safety/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
import logging
from typing import Any

from pyfamilysafety import FamilySafety
from pyfamilysafety.authenticator import Authenticator
from pyfamilysafety.exceptions import HttpException
from pyfamilysafety.application import Application
from pyfamilysafety.account import Account
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector

from .const import DOMAIN

Expand All @@ -23,7 +27,23 @@
}
)

async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
def _get_application_id(name: str, applications: list[Application]):
"""Returns the single application ID."""
return [a for a in applications if a.name == name][0].app_id

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

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

def _get_account_id(name: str, accounts: list[Account]):
"""Returns 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
try:
Expand Down Expand Up @@ -56,7 +76,7 @@ def async_get_options_flow(
config_entry: config_entries.ConfigEntry
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlow(config_entry)
return OptionsFlow(config_entry=config_entry)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
Expand All @@ -65,7 +85,7 @@ async def async_step_user(
errors: dict[str, str] = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
info = await validate_input(user_input)
user_input["refresh_token"] = info["refresh_token"]
return self.async_create_entry(title=info["title"], data=user_input)
except InvalidAuth as err:
Expand All @@ -74,9 +94,6 @@ async def async_step_user(
except CannotConnect as err:
_LOGGER.warning("Cannot connect: %s", err)
errors["base"] = "cannot_connect"
except Exception as err:
_LOGGER.error(err)
errors["base"] = "unknown"

return self.async_show_form(
step_id="user",
Expand All @@ -89,35 +106,171 @@ class OptionsFlow(config_entries.OptionsFlow):
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self.family_safety: FamilySafety = None

async def async_step_init(
def _get_config_entry(self, key):
"""Returns the specific config entry."""
config = self.config_entry.data.get(key, None)
if (self.config_entry.options) and (
self.config_entry.options.get(key, None) is not None
):
config = self.config_entry.options.get(key)
return config

async def _async_create_entry(self, **kwargs) -> config_entries.FlowResult:
"""Creates an entry using optional overrides"""
update_interval = self._get_config_entry("update_interval")
if kwargs.get("update_interval", None) is not None:
update_interval = kwargs.get("update_interval")

refresh_token = self._get_config_entry("refresh_token")
if kwargs.get("refresh_token", None) is not None:
update_interval = kwargs.get("refresh_token")

tracked_applications = self._get_config_entry("tracked_applications")
if kwargs.get("tracked_applications", None) is not None:
tracked_applications = kwargs.get("tracked_applications")

accounts = self._get_config_entry("accounts")
if kwargs.get("accounts", None) is not None:
accounts = kwargs.get("accounts")

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

async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
"""Initial step."""
"""Auth step."""
if user_input is not None:
return self.async_create_entry(
title=self.config_entry.title,
data={
"refresh_token": user_input["refresh_token"],
"update_interval": user_input["update_interval"]
}
return await self._async_create_entry(
refresh_token=user_input["refresh_token"],
update_interval=user_input["update_interval"]
)

refresh_token = self.config_entry.data["refresh_token"]
if self.config_entry.options:
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")

return self.async_show_form(
step_id="init",
step_id="auth",
data_schema=vol.Schema(
{
vol.Required("update_interval", default=update_interval): int,
vol.Required("refresh_token",
default=self.config_entry.data["refresh_token"]): str
default=refresh_token): str
}
)
)

async def async_step_applications(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
"""Application configuration step."""
if user_input is not None:
tracked_application_ids = []
applications = self.family_safety.accounts[0].applications
for app in user_input.get("tracked_applications", []):
tracked_application_ids.append(_get_application_id(app, applications))
return await self._async_create_entry(
tracked_application_ids=tracked_application_ids
)

default_tracked_applications = []
tracked_applications = self.config_entry.data.get("tracked_applications", [])
if self.config_entry.options:
tracked_applications = self.config_entry.options.get("tracked_applications", [])
for app in tracked_applications:
try:
default_tracked_applications.append(
self.family_safety.accounts[0].get_application(app).name
)
except IndexError:
pass

return self.async_show_form(
step_id="applications",
data_schema=vol.Schema({
vol.Optional("tracked_applications",
default=default_tracked_applications): selector.SelectSelector(
selector.SelectSelectorConfig(
options=_convert_applications(self.family_safety.accounts[0].applications),
custom_value=False,
multiple=True)
)
})
)

async def async_step_accounts(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
"""Accounts step."""

if user_input is not None:
tracked_user_ids = []
try:
for user in user_input.get("accounts", []):
tracked_user_ids.append(
_get_account_id(user, self.family_safety.accounts)
)
except IndexError:
pass
return await self._async_create_entry(
accounts=tracked_user_ids
)

default_tracked_accounts = []
tracked_accounts = self.config_entry.data.get("accounts", [])
if self.config_entry.options:
tracked_accounts = self.config_entry.options.get("accounts", [])
for account in tracked_accounts:
try:
acc = self.family_safety.get_account(account)
default_tracked_accounts.append(f"{acc.first_name} {acc.surname}")
except IndexError:
pass

return self.async_show_form(
step_id="accounts",
data_schema=vol.Schema(
{
vol.Optional("accounts",
default=default_tracked_accounts): selector.SelectSelector(
selector.SelectSelectorConfig(
options=_convert_accounts(self.family_safety.accounts),
custom_value=False,
multiple=True
)
)
}
)
)

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> config_entries.FlowResult:
"""Initial step."""
self.family_safety = await FamilySafety.create(
token=self.config_entry.data["refresh_token"],
use_refresh_token=True
)
return self.async_show_menu(
step_id="init",
menu_options=["auth", "applications", "accounts"]
)

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

Expand Down
4 changes: 2 additions & 2 deletions custom_components/family_safety/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"documentation": "https://github.com/pantherale0/ha-familysafety",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/pantherale0/ha-familysafety/issues",
"requirements": ["pyfamilysafety==0.1.0"],
"requirements": ["pyfamilysafety==0.1.1"],
"ssdp": [],
"zeroconf": [],
"version": "1.1.0b0",
"version": "1.1.0b1",
"integration_type": "service"
}
67 changes: 61 additions & 6 deletions custom_components/family_safety/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,25 @@ async def async_setup_entry(
accounts: list[Account] = hass.data[DOMAIN][config_entry.entry_id].api.accounts
entities = []
for account in accounts:
entities.append(
AccountScreentimeSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id
if (account.user_id in config_entry.options.get("accounts", [])) or (
len(config_entry.options.get("accounts", []))==0
):
entities.append(
AccountScreentimeSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id
)
)
)
for app in config_entry.options.get("tracked_applications", []):
entities.append(
ApplicationScreentimeSensor(
coordinator=hass.data[DOMAIN][config_entry.entry_id],
idx=None,
account_id=account.user_id,
app_id=app
)
)

async_add_entities(entities, True)

Expand Down Expand Up @@ -78,3 +90,46 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
"application_usage": applications,
"device_usage": devices
}

class ApplicationScreentimeSensor(ManagedAccountEntity, SensorEntity):
"""Application specific screentime sensor"""

def __init__(self,
coordinator: FamilySafetyCoordinator,
idx,
account_id,
app_id) -> None:
super().__init__(coordinator, idx, account_id, f"{app_id}_screentime")
self._app_id = app_id

@property
def name(self) -> str:
return f"{self._application.name} Used Screen Time"

@property
def _application(self):
"""Return the application."""
return self._account.get_application(self._app_id)

@property
def native_value(self) -> float:
"""Return duration (minutes)"""
return self._application.usage

@property
def native_unit_of_measurement(self) -> str | None:
return "min"

@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.DURATION

@property
def icon(self) -> str | None:
return self._application.icon

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return {
"blocked": self._application.blocked
}
Loading

0 comments on commit dba8bf5

Please # to comment.