Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Native Home Assistant iOS app component."""
2 
3 import datetime
4 from http import HTTPStatus
5 from typing import Any
6 
7 from aiohttp import web
8 import voluptuous as vol
9 
10 from homeassistant import config_entries
11 from homeassistant.components.http import KEY_HASS, HomeAssistantView
12 from homeassistant.const import Platform
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.exceptions import HomeAssistantError
15 from homeassistant.helpers import config_validation as cv, discovery
16 from homeassistant.helpers.dispatcher import async_dispatcher_send
17 from homeassistant.helpers.json import save_json
18 from homeassistant.helpers.typing import ConfigType
19 from homeassistant.util.json import load_json_object
20 
21 from .const import (
22  ATTR_BATTERY,
23  ATTR_BATTERY_LEVEL,
24  ATTR_BATTERY_STATE,
25  ATTR_DEVICE,
26  ATTR_DEVICE_ID,
27  ATTR_DEVICE_NAME,
28  ATTR_DEVICE_PERMANENT_ID,
29  ATTR_DEVICE_SYSTEM_VERSION,
30  ATTR_DEVICE_TYPE,
31  BATTERY_STATES,
32  CONF_ACTION_BACKGROUND_COLOR,
33  CONF_ACTION_ICON,
34  CONF_ACTION_ICON_COLOR,
35  CONF_ACTION_ICON_ICON,
36  CONF_ACTION_LABEL,
37  CONF_ACTION_LABEL_COLOR,
38  CONF_ACTION_LABEL_TEXT,
39  CONF_ACTION_NAME,
40  CONF_ACTION_SHOW_IN_CARPLAY,
41  CONF_ACTION_SHOW_IN_WATCH,
42  CONF_ACTION_USE_CUSTOM_COLORS,
43  CONF_ACTIONS,
44  DOMAIN,
45 )
46 
47 CONF_PUSH = "push"
48 CONF_PUSH_CATEGORIES = "categories"
49 CONF_PUSH_CATEGORIES_NAME = "name"
50 CONF_PUSH_CATEGORIES_IDENTIFIER = "identifier"
51 CONF_PUSH_CATEGORIES_ACTIONS = "actions"
52 
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"
62 
63 CONF_USER = "user"
64 
65 ATTR_FOREGROUND = "foreground"
66 ATTR_BACKGROUND = "background"
67 
68 ACTIVATION_MODES = [ATTR_FOREGROUND, ATTR_BACKGROUND]
69 
70 ATTR_DEFAULT_BEHAVIOR = "default"
71 ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
72 
73 BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
74 
75 ATTR_LAST_SEEN_AT = "lastSeenAt"
76 
77 ATTR_PUSH_TOKEN = "pushToken"
78 ATTR_APP = "app"
79 ATTR_PERMISSIONS = "permissions"
80 ATTR_PUSH_ID = "pushId"
81 ATTR_PUSH_SOUNDS = "pushSounds"
82 
83 ATTR_DEVICE_LOCALIZED_MODEL = "localizedModel"
84 ATTR_DEVICE_MODEL = "model"
85 ATTR_DEVICE_SYSTEM_NAME = "systemName"
86 
87 ATTR_APP_BUNDLE_IDENTIFIER = "bundleIdentifier"
88 ATTR_APP_BUILD_NUMBER = "buildNumber"
89 ATTR_APP_VERSION_NUMBER = "versionNumber"
90 
91 ATTR_LOCATION_PERMISSION = "location"
92 ATTR_NOTIFICATIONS_PERMISSION = "notifications"
93 
94 PERMISSIONS = [ATTR_LOCATION_PERMISSION, ATTR_NOTIFICATIONS_PERMISSION]
95 
96 
97 ATTR_DEVICES = "devices"
98 
99 PUSH_ACTION_SCHEMA = vol.Schema(
100  {
101  vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper,
102  vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string,
103  vol.Optional(
104  CONF_PUSH_ACTIONS_ACTIVATION_MODE, default=ATTR_BACKGROUND
105  ): vol.In(ACTIVATION_MODES),
106  vol.Optional(
107  CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED, default=False
108  ): cv.boolean,
109  vol.Optional(CONF_PUSH_ACTIONS_DESTRUCTIVE, default=False): cv.boolean,
110  vol.Optional(CONF_PUSH_ACTIONS_BEHAVIOR, default=ATTR_DEFAULT_BEHAVIOR): vol.In(
111  BEHAVIORS
112  ),
113  vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE): cv.string,
114  vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER): cv.string,
115  },
116  extra=vol.ALLOW_EXTRA,
117 )
118 
119 PUSH_ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_ACTION_SCHEMA])
120 
121 PUSH_CATEGORY_SCHEMA = vol.Schema(
122  {
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,
126  }
127 )
128 
129 PUSH_CATEGORY_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_CATEGORY_SCHEMA])
130 
131 ACTION_SCHEMA = vol.Schema(
132  {
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,
138  },
139  vol.Optional(CONF_ACTION_ICON): {
140  vol.Optional(CONF_ACTION_ICON_ICON): cv.string,
141  vol.Optional(CONF_ACTION_ICON_COLOR): cv.string,
142  },
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,
146  },
147 )
148 
149 ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [ACTION_SCHEMA])
150 
151 CONFIG_SCHEMA = vol.Schema(
152  {
153  DOMAIN: vol.All(
154  cv.deprecated(CONF_PUSH),
155  {
156  CONF_PUSH: {CONF_PUSH_CATEGORIES: PUSH_CATEGORY_LIST_SCHEMA},
157  CONF_ACTIONS: ACTION_LIST_SCHEMA,
158  },
159  )
160  },
161  extra=vol.ALLOW_EXTRA,
162 )
163 
164 IDENTIFY_DEVICE_SCHEMA = vol.Schema(
165  {
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,
173  },
174  extra=vol.ALLOW_EXTRA,
175 )
176 
177 IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
178 
179 IDENTIFY_APP_SCHEMA = vol.Schema(
180  {
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,
184  },
185  extra=vol.ALLOW_EXTRA,
186 )
187 
188 IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
189 
190 IDENTIFY_BATTERY_SCHEMA = vol.Schema(
191  {
192  vol.Required(ATTR_BATTERY_LEVEL): cv.positive_int,
193  vol.Required(ATTR_BATTERY_STATE): vol.In(BATTERY_STATES),
194  },
195  extra=vol.ALLOW_EXTRA,
196 )
197 
198 IDENTIFY_BATTERY_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_BATTERY_SCHEMA)
199 
200 IDENTIFY_SCHEMA = vol.Schema(
201  {
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,
210  },
211  extra=vol.ALLOW_EXTRA,
212 )
213 
214 CONFIGURATION_FILE = ".ios.conf"
215 
216 PLATFORMS = [Platform.SENSOR]
217 
218 
219 def devices_with_push(hass: HomeAssistant) -> dict[str, str]:
220  """Return a dictionary of push enabled targets."""
221  return {
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
225  }
226 
227 
228 def enabled_push_ids(hass: HomeAssistant) -> list[str]:
229  """Return a list of push enabled target push IDs."""
230  return [
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
234  ]
235 
236 
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] # type: ignore[no-any-return]
240 
241 
242 def device_name_for_push_id(hass: HomeAssistant, push_id: str) -> str | None:
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:
246  return device_name # type: ignore[no-any-return]
247  return None
248 
249 
250 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
251  """Set up the iOS component."""
252  conf: ConfigType | None = config.get(DOMAIN)
253 
254  ios_config = await hass.async_add_executor_job(
255  load_json_object, hass.config.path(CONFIGURATION_FILE)
256  )
257 
258  if ios_config == {}:
259  ios_config[ATTR_DEVICES] = {}
260 
261  if CONF_PUSH not in (conf_user := conf or {}):
262  conf_user[CONF_PUSH] = {}
263 
264  ios_config[CONF_USER] = conf_user
265 
266  hass.data[DOMAIN] = ios_config
267 
268  # No entry support for notify component yet
269  discovery.load_platform(hass, Platform.NOTIFY, DOMAIN, {}, config)
270 
271  if conf is not None:
272  hass.async_create_task(
273  hass.config_entries.flow.async_init(
274  DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
275  )
276  )
277 
278  return True
279 
280 
282  hass: HomeAssistant, entry: config_entries.ConfigEntry
283 ) -> bool:
284  """Set up an iOS entry."""
285  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
286 
287  hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE)))
288  hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH]))
289  hass.http.register_view(iOSConfigView(hass.data[DOMAIN][CONF_USER]))
290 
291  return True
292 
293 
294 class iOSPushConfigView(HomeAssistantView):
295  """A view that provides the push categories configuration."""
296 
297  url = "/api/ios/push"
298  name = "api:ios:push"
299 
300  def __init__(self, push_config: dict[str, Any]) -> None:
301  """Init the view."""
302  self.push_configpush_config = push_config
303 
304  @callback
305  def get(self, request: web.Request) -> web.Response:
306  """Handle the GET request for the push configuration."""
307  return self.json(self.push_configpush_config)
308 
309 
310 class iOSConfigView(HomeAssistantView):
311  """A view that provides the whole user-defined configuration."""
312 
313  url = "/api/ios/config"
314  name = "api:ios:config"
315 
316  def __init__(self, config: dict[str, Any]) -> None:
317  """Init the view."""
318  self.configconfig = config
319 
320  @callback
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)
324 
325 
326 class iOSIdentifyDeviceView(HomeAssistantView):
327  """A view that accepts device identification requests."""
328 
329  url = "/api/ios/identify"
330  name = "api:ios:identify"
331 
332  def __init__(self, config_path: str) -> None:
333  """Initialize the view."""
334  self._config_path_config_path = config_path
335 
336  async def post(self, request: web.Request) -> web.Response:
337  """Handle the POST request for device identification."""
338  try:
339  data = await request.json()
340  except ValueError:
341  return self.json_message("Invalid JSON", HTTPStatus.BAD_REQUEST)
342 
343  hass = request.app[KEY_HASS]
344 
345  data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
346 
347  device_id = data[ATTR_DEVICE_ID]
348 
349  hass.data[DOMAIN][ATTR_DEVICES][device_id] = data
350 
351  async_dispatcher_send(hass, f"{DOMAIN}.{device_id}", data)
352 
353  try:
354  save_json(self._config_path_config_path, hass.data[DOMAIN])
355  except HomeAssistantError:
356  return self.json_message(
357  "Error saving device.", HTTPStatus.INTERNAL_SERVER_ERROR
358  )
359 
360  return self.json({"status": "registered"})
web.Response get(self, web.Request request)
Definition: __init__.py:321
None __init__(self, dict[str, Any] config)
Definition: __init__.py:316
web.Response post(self, web.Request request)
Definition: __init__.py:336
None __init__(self, dict[str, Any] push_config)
Definition: __init__.py:300
web.Response get(self, web.Request request)
Definition: __init__.py:305
str|None device_name_for_push_id(HomeAssistant hass, str push_id)
Definition: __init__.py:242
list[str] enabled_push_ids(HomeAssistant hass)
Definition: __init__.py:228
dict[str, str] devices_with_push(HomeAssistant hass)
Definition: __init__.py:219
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:250
dict[str, dict[str, Any]] devices(HomeAssistant hass)
Definition: __init__.py:237
bool async_setup_entry(HomeAssistant hass, config_entries.ConfigEntry entry)
Definition: __init__.py:283
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
None save_json(str filename, list|dict data, bool private=False, *type[json.JSONEncoder]|None encoder=None, bool atomic_writes=False)
Definition: json.py:202