1 """Native Home Assistant iOS app component."""
4 from http
import HTTPStatus
7 from aiohttp
import web
8 import voluptuous
as vol
10 from homeassistant
import config_entries
28 ATTR_DEVICE_PERMANENT_ID,
29 ATTR_DEVICE_SYSTEM_VERSION,
32 CONF_ACTION_BACKGROUND_COLOR,
34 CONF_ACTION_ICON_COLOR,
35 CONF_ACTION_ICON_ICON,
37 CONF_ACTION_LABEL_COLOR,
38 CONF_ACTION_LABEL_TEXT,
40 CONF_ACTION_SHOW_IN_CARPLAY,
41 CONF_ACTION_SHOW_IN_WATCH,
42 CONF_ACTION_USE_CUSTOM_COLORS,
48 CONF_PUSH_CATEGORIES =
"categories"
49 CONF_PUSH_CATEGORIES_NAME =
"name"
50 CONF_PUSH_CATEGORIES_IDENTIFIER =
"identifier"
51 CONF_PUSH_CATEGORIES_ACTIONS =
"actions"
53 CONF_PUSH_ACTIONS_IDENTIFIER =
"identifier"
54 CONF_PUSH_ACTIONS_TITLE =
"title"
55 CONF_PUSH_ACTIONS_ACTIVATION_MODE =
"activationMode"
56 CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED =
"authenticationRequired"
57 CONF_PUSH_ACTIONS_DESTRUCTIVE =
"destructive"
58 CONF_PUSH_ACTIONS_BEHAVIOR =
"behavior"
59 CONF_PUSH_ACTIONS_CONTEXT =
"context"
60 CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE =
"textInputButtonTitle"
61 CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER =
"textInputPlaceholder"
65 ATTR_FOREGROUND =
"foreground"
66 ATTR_BACKGROUND =
"background"
68 ACTIVATION_MODES = [ATTR_FOREGROUND, ATTR_BACKGROUND]
70 ATTR_DEFAULT_BEHAVIOR =
"default"
71 ATTR_TEXT_INPUT_BEHAVIOR =
"textInput"
73 BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
75 ATTR_LAST_SEEN_AT =
"lastSeenAt"
77 ATTR_PUSH_TOKEN =
"pushToken"
79 ATTR_PERMISSIONS =
"permissions"
80 ATTR_PUSH_ID =
"pushId"
81 ATTR_PUSH_SOUNDS =
"pushSounds"
83 ATTR_DEVICE_LOCALIZED_MODEL =
"localizedModel"
84 ATTR_DEVICE_MODEL =
"model"
85 ATTR_DEVICE_SYSTEM_NAME =
"systemName"
87 ATTR_APP_BUNDLE_IDENTIFIER =
"bundleIdentifier"
88 ATTR_APP_BUILD_NUMBER =
"buildNumber"
89 ATTR_APP_VERSION_NUMBER =
"versionNumber"
91 ATTR_LOCATION_PERMISSION =
"location"
92 ATTR_NOTIFICATIONS_PERMISSION =
"notifications"
94 PERMISSIONS = [ATTR_LOCATION_PERMISSION, ATTR_NOTIFICATIONS_PERMISSION]
97 ATTR_DEVICES =
"devices"
99 PUSH_ACTION_SCHEMA = vol.Schema(
101 vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper,
102 vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string,
104 CONF_PUSH_ACTIONS_ACTIVATION_MODE, default=ATTR_BACKGROUND
105 ): vol.In(ACTIVATION_MODES),
107 CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED, default=
False
109 vol.Optional(CONF_PUSH_ACTIONS_DESTRUCTIVE, default=
False): cv.boolean,
110 vol.Optional(CONF_PUSH_ACTIONS_BEHAVIOR, default=ATTR_DEFAULT_BEHAVIOR): vol.In(
113 vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE): cv.string,
114 vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER): cv.string,
116 extra=vol.ALLOW_EXTRA,
119 PUSH_ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_ACTION_SCHEMA])
121 PUSH_CATEGORY_SCHEMA = vol.Schema(
123 vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string,
124 vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Lower,
125 vol.Required(CONF_PUSH_CATEGORIES_ACTIONS): PUSH_ACTION_LIST_SCHEMA,
129 PUSH_CATEGORY_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_CATEGORY_SCHEMA])
131 ACTION_SCHEMA = vol.Schema(
133 vol.Required(CONF_ACTION_NAME): cv.string,
134 vol.Optional(CONF_ACTION_BACKGROUND_COLOR): cv.string,
135 vol.Optional(CONF_ACTION_LABEL): {
136 vol.Optional(CONF_ACTION_LABEL_TEXT): cv.string,
137 vol.Optional(CONF_ACTION_LABEL_COLOR): cv.string,
139 vol.Optional(CONF_ACTION_ICON): {
140 vol.Optional(CONF_ACTION_ICON_ICON): cv.string,
141 vol.Optional(CONF_ACTION_ICON_COLOR): cv.string,
143 vol.Optional(CONF_ACTION_SHOW_IN_CARPLAY): cv.boolean,
144 vol.Optional(CONF_ACTION_SHOW_IN_WATCH): cv.boolean,
145 vol.Optional(CONF_ACTION_USE_CUSTOM_COLORS): cv.boolean,
149 ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [ACTION_SCHEMA])
151 CONFIG_SCHEMA = vol.Schema(
154 cv.deprecated(CONF_PUSH),
156 CONF_PUSH: {CONF_PUSH_CATEGORIES: PUSH_CATEGORY_LIST_SCHEMA},
157 CONF_ACTIONS: ACTION_LIST_SCHEMA,
161 extra=vol.ALLOW_EXTRA,
164 IDENTIFY_DEVICE_SCHEMA = vol.Schema(
166 vol.Required(ATTR_DEVICE_NAME): cv.string,
167 vol.Required(ATTR_DEVICE_LOCALIZED_MODEL): cv.string,
168 vol.Required(ATTR_DEVICE_MODEL): cv.string,
169 vol.Required(ATTR_DEVICE_PERMANENT_ID): cv.string,
170 vol.Required(ATTR_DEVICE_SYSTEM_VERSION): cv.string,
171 vol.Required(ATTR_DEVICE_TYPE): cv.string,
172 vol.Required(ATTR_DEVICE_SYSTEM_NAME): cv.string,
174 extra=vol.ALLOW_EXTRA,
177 IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
179 IDENTIFY_APP_SCHEMA = vol.Schema(
181 vol.Required(ATTR_APP_BUNDLE_IDENTIFIER): cv.string,
182 vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
183 vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string,
185 extra=vol.ALLOW_EXTRA,
188 IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
190 IDENTIFY_BATTERY_SCHEMA = vol.Schema(
192 vol.Required(ATTR_BATTERY_LEVEL): cv.positive_int,
193 vol.Required(ATTR_BATTERY_STATE): vol.In(BATTERY_STATES),
195 extra=vol.ALLOW_EXTRA,
198 IDENTIFY_BATTERY_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_BATTERY_SCHEMA)
200 IDENTIFY_SCHEMA = vol.Schema(
202 vol.Required(ATTR_DEVICE): IDENTIFY_DEVICE_SCHEMA_CONTAINER,
203 vol.Required(ATTR_BATTERY): IDENTIFY_BATTERY_SCHEMA_CONTAINER,
204 vol.Required(ATTR_PUSH_TOKEN): cv.string,
205 vol.Required(ATTR_APP): IDENTIFY_APP_SCHEMA_CONTAINER,
206 vol.Required(ATTR_PERMISSIONS): vol.All(cv.ensure_list, [vol.In(PERMISSIONS)]),
207 vol.Required(ATTR_PUSH_ID): cv.string,
208 vol.Required(ATTR_DEVICE_ID): cv.string,
209 vol.Optional(ATTR_PUSH_SOUNDS): list,
211 extra=vol.ALLOW_EXTRA,
214 CONFIGURATION_FILE =
".ios.conf"
216 PLATFORMS = [Platform.SENSOR]
220 """Return a dictionary of push enabled targets."""
222 device_name: device.get(ATTR_PUSH_ID)
223 for device_name, device
in hass.data[DOMAIN][ATTR_DEVICES].items()
224 if device.get(ATTR_PUSH_ID)
is not None
229 """Return a list of push enabled target push IDs."""
231 device.get(ATTR_PUSH_ID)
232 for device
in hass.data[DOMAIN][ATTR_DEVICES].values()
233 if device.get(ATTR_PUSH_ID)
is not None
237 def devices(hass: HomeAssistant) -> dict[str, dict[str, Any]]:
238 """Return a dictionary of all identified devices."""
239 return hass.data[DOMAIN][ATTR_DEVICES]
243 """Return the device name for the push ID."""
244 for device_name, device
in hass.data[DOMAIN][ATTR_DEVICES].items():
245 if device.get(ATTR_PUSH_ID)
is push_id:
250 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
251 """Set up the iOS component."""
252 conf: ConfigType |
None = config.get(DOMAIN)
254 ios_config = await hass.async_add_executor_job(
255 load_json_object, hass.config.path(CONFIGURATION_FILE)
259 ios_config[ATTR_DEVICES] = {}
261 if CONF_PUSH
not in (conf_user := conf
or {}):
262 conf_user[CONF_PUSH] = {}
264 ios_config[CONF_USER] = conf_user
266 hass.data[DOMAIN] = ios_config
269 discovery.load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config)
272 hass.async_create_task(
273 hass.config_entries.flow.async_init(
274 DOMAIN, context={
"source": config_entries.SOURCE_IMPORT}
284 """Set up an iOS entry."""
285 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
288 hass.http.register_view(
iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH]))
289 hass.http.register_view(
iOSConfigView(hass.data[DOMAIN][CONF_USER]))
295 """A view that provides the push categories configuration."""
297 url =
"/api/ios/push"
298 name =
"api:ios:push"
300 def __init__(self, push_config: dict[str, Any]) ->
None:
305 def get(self, request: web.Request) -> web.Response:
306 """Handle the GET request for the push configuration."""
311 """A view that provides the whole user-defined configuration."""
313 url =
"/api/ios/config"
314 name =
"api:ios:config"
316 def __init__(self, config: dict[str, Any]) ->
None:
321 def get(self, request: web.Request) -> web.Response:
322 """Handle the GET request for the user-defined configuration."""
323 return self.json(self.
configconfig)
327 """A view that accepts device identification requests."""
329 url =
"/api/ios/identify"
330 name =
"api:ios:identify"
333 """Initialize the view."""
336 async
def post(self, request: web.Request) -> web.Response:
337 """Handle the POST request for device identification."""
339 data = await request.json()
341 return self.json_message(
"Invalid JSON", HTTPStatus.BAD_REQUEST)
343 hass = request.app[KEY_HASS]
345 data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
347 device_id = data[ATTR_DEVICE_ID]
349 hass.data[DOMAIN][ATTR_DEVICES][device_id] = data
355 except HomeAssistantError:
356 return self.json_message(
357 "Error saving device.", HTTPStatus.INTERNAL_SERVER_ERROR
360 return self.json({
"status":
"registered"})
web.Response get(self, web.Request request)
None __init__(self, dict[str, Any] config)
web.Response post(self, web.Request request)
None __init__(self, str config_path)
None __init__(self, dict[str, Any] push_config)
web.Response get(self, web.Request request)
str|None device_name_for_push_id(HomeAssistant hass, str push_id)
list[str] enabled_push_ids(HomeAssistant hass)
dict[str, str] devices_with_push(HomeAssistant hass)
bool async_setup(HomeAssistant hass, ConfigType config)
dict[str, dict[str, Any]] devices(HomeAssistant hass)
bool async_setup_entry(HomeAssistant hass, config_entries.ConfigEntry entry)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
None save_json(str filename, list|dict data, bool private=False, *type[json.JSONEncoder]|None encoder=None, bool atomic_writes=False)