diff --git a/custom_components/family_safety/__init__.py b/custom_components/family_safety/__init__.py index 0e7bfaf..5a9faf3 100644 --- a/custom_components/family_safety/__init__.py +++ b/custom_components/family_safety/__init__.py @@ -3,7 +3,7 @@ import logging from pyfamilysafety import FamilySafety -from pyfamilysafety.exceptions import HttpException +from pyfamilysafety.exceptions import HttpException, Unauthorized, AggregatorException from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -13,7 +13,7 @@ HomeAssistantError ) -from .const import DOMAIN +from .const import DOMAIN, AGG_ERROR from .coordinator import FamilySafetyCoordinator _LOGGER = logging.getLogger(__name__) @@ -34,9 +34,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: familysafety, entry.options.get("update_interval", entry.data["update_interval"])) # no need to fetch initial data as this is already handled on creation + except AggregatorException as err: + _LOGGER.error(AGG_ERROR) + raise CannotConnect from err + except Unauthorized as err: + raise ConfigEntryAuthFailed from err except HttpException as err: _LOGGER.error(err) - raise ConfigEntryAuthFailed from err + raise CannotConnect from err except Exception as err: _LOGGER.error(err) raise CannotConnect from err diff --git a/custom_components/family_safety/const.py b/custom_components/family_safety/const.py index 3ac0613..25e5b25 100644 --- a/custom_components/family_safety/const.py +++ b/custom_components/family_safety/const.py @@ -6,9 +6,13 @@ NAME = "Microsoft Family Safety" DOMAIN = "family_safety" -VERSION = "1.1.0b4" +VERSION = "1.1.0b5" DEFAULT_OVERRIDE_ENTITIES = [OverrideTarget.MOBILE, OverrideTarget.WINDOWS, OverrideTarget.XBOX, OverrideTarget.ALL_DEVICES] +AGG_TITLE = "Aggregator error occured. " +AGG_HELP = "This is an upstream issue with Microsoft and is usually temporary. " +AGG_FUTURE = "Try reloading the integration in 15 minutes." +AGG_ERROR = f"{AGG_TITLE}{AGG_HELP}{AGG_FUTURE}" diff --git a/custom_components/family_safety/entity_base.py b/custom_components/family_safety/entity_base.py index 8762bc8..693a6fb 100644 --- a/custom_components/family_safety/entity_base.py +++ b/custom_components/family_safety/entity_base.py @@ -5,7 +5,7 @@ from datetime import datetime, time, timedelta from pyfamilysafety import Account -from pyfamilysafety.account import Device +from pyfamilysafety.application import Application from pyfamilysafety.enum import OverrideTarget, OverrideType import homeassistant.helpers.device_registry as dr @@ -50,6 +50,35 @@ def device_info(self): entry_type=dr.DeviceEntryType.SERVICE ) + async def async_block_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].block_app() + + 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() + +class ApplicationEntity(ManagedAccountEntity): + """Defines a application entity.""" + def __init__(self, + coordinator: FamilySafetyCoordinator, + idx, + account_id, + app_id: str) -> None: + """init entity.""" + super().__init__(coordinator, idx, account_id, f"override_{str(app_id).lower()}") + self._app_id = app_id + + @property + def _application(self) -> Application: + """Gets the application.""" + return self._account.get_application(self._app_id) + + @property + def icon(self) -> str | None: + return self._application.icon + + class PlatformOverrideEntity(ManagedAccountEntity): """Defines a managed device.""" diff --git a/custom_components/family_safety/manifest.json b/custom_components/family_safety/manifest.json index b6049c9..351bdf3 100644 --- a/custom_components/family_safety/manifest.json +++ b/custom_components/family_safety/manifest.json @@ -11,6 +11,6 @@ "requirements": ["pyfamilysafety==0.2.0"], "ssdp": [], "zeroconf": [], - "version": "1.1.0b4", + "version": "1.1.0b5", "integration_type": "service" } \ No newline at end of file diff --git a/custom_components/family_safety/sensor.py b/custom_components/family_safety/sensor.py index a783a14..9e4a5f8 100644 --- a/custom_components/family_safety/sensor.py +++ b/custom_components/family_safety/sensor.py @@ -4,12 +4,14 @@ import logging from typing import Any +import voluptuous as vol + from pyfamilysafety import Account from homeassistant.components.sensor import SensorEntity, SensorDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddEntitiesCallback, async_get_current_platform from .coordinator import FamilySafetyCoordinator @@ -17,7 +19,7 @@ DOMAIN ) -from .entity_base import ManagedAccountEntity +from .entity_base import ManagedAccountEntity, ApplicationEntity _LOGGER = logging.getLogger(__name__) @@ -40,6 +42,13 @@ async def async_setup_entry( account_id=account.user_id ) ) + entities.append( + AccountBalanceSensor( + 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( @@ -51,6 +60,44 @@ async def async_setup_entry( ) async_add_entities(entities, True) + # register services + platform = async_get_current_platform() + platform.async_register_entity_service( + name="block_app", + schema={ + vol.Required("name"): str + }, + func="async_block_application" + ) + platform.async_register_entity_service( + name="unblock_app", + schema={ + vol.Required("name"): str + }, + func="async_unblock_application" + ) + +class AccountBalanceSensor(ManagedAccountEntity, SensorEntity): + """A balance sensor for the account.""" + def __init__(self, coordinator: FamilySafetyCoordinator, idx, account_id) -> None: + super().__init__(coordinator, idx, account_id, "balance") + + @property + def name(self) -> str: + return "Available Balance" + + @property + def native_value(self) -> float: + """Return balance""" + return self._account.account_balance + + @property + def native_unit_of_measurement(self) -> str | None: + return self._account.account_currency + + @property + def device_class(self) -> SensorDeviceClass | None: + return SensorDeviceClass.MONETARY class AccountScreentimeSensor(ManagedAccountEntity, SensorEntity): """Aggregate screentime sensor.""" @@ -91,26 +138,12 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: "device_usage": devices } -class ApplicationScreentimeSensor(ManagedAccountEntity, SensorEntity): +class ApplicationScreentimeSensor(ApplicationEntity, 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)""" @@ -124,10 +157,6 @@ def native_unit_of_measurement(self) -> str | None: 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 { diff --git a/custom_components/family_safety/services.yaml b/custom_components/family_safety/services.yaml new file mode 100644 index 0000000..9382ab6 --- /dev/null +++ b/custom_components/family_safety/services.yaml @@ -0,0 +1,20 @@ +block_app: + target: + entity: + integration: family_safety + fields: + name: + required: True + selector: + text: + multiline: False +unblock_app: + target: + entity: + integration: family_safety + fields: + name: + required: True + selector: + text: + multiline: False diff --git a/custom_components/family_safety/switch.py b/custom_components/family_safety/switch.py index 0bbfe94..11f5b5a 100644 --- a/custom_components/family_safety/switch.py +++ b/custom_components/family_safety/switch.py @@ -17,7 +17,7 @@ DEFAULT_OVERRIDE_ENTITIES ) -from .entity_base import PlatformOverrideEntity +from .entity_base import PlatformOverrideEntity, ApplicationEntity _LOGGER = logging.getLogger(__name__) @@ -43,9 +43,40 @@ async def async_setup_entry( platform=platform ) ) + for app in config_entry.options.get("tracked_applications", []): + entities.append( + ApplicationBlockSwitch( + coordinator=hass.data[DOMAIN][config_entry.entry_id], + idx=None, + account_id=account.user_id, + app_id=app + ) + ) async_add_entities(entities, True) +class ApplicationBlockSwitch(ApplicationEntity, SwitchEntity): + """Defines a application switch.""" + @property + def name(self) -> str: + return f"Block {self._application.name}" + + @property + def is_on(self) -> bool: + return self._application.blocked + + @property + def device_class(self) -> SwitchDeviceClass | None: + return SwitchDeviceClass.SWITCH + + async def async_turn_off(self, **kwargs: Any) -> None: + await self._application.unblock_app() + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + await self._application.block_app() + await self.coordinator.async_request_refresh() + class PlatformOverrideSwitch(PlatformOverrideEntity, SwitchEntity): """Platform override switch."""