1 """The System Bridge integration."""
3 from __future__
import annotations
6 from dataclasses
import asdict
10 from systembridgeconnector.exceptions
import (
11 AuthenticationException,
12 ConnectionClosedException,
13 ConnectionErrorException,
15 from systembridgeconnector.version
import Version
16 from systembridgemodels.keyboard_key
import KeyboardKey
17 from systembridgemodels.keyboard_text
import KeyboardText
18 from systembridgemodels.modules.processes
import Process
19 from systembridgemodels.open_path
import OpenPath
20 from systembridgemodels.open_url
import OpenUrl
21 import voluptuous
as vol
44 ConfigEntryAuthFailed,
47 ServiceValidationError,
50 config_validation
as cv,
51 device_registry
as dr,
57 from .config_flow
import SystemBridgeConfigFlow
58 from .const
import DATA_WAIT_TIMEOUT, DOMAIN, MODULES
59 from .coordinator
import SystemBridgeDataUpdateCoordinator
61 _LOGGER = logging.getLogger(__name__)
64 Platform.BINARY_SENSOR,
65 Platform.MEDIA_PLAYER,
71 CONF_BRIDGE =
"bridge"
75 SERVICE_GET_PROCESS_BY_ID =
"get_process_by_id"
76 SERVICE_GET_PROCESSES_BY_NAME =
"get_processes_by_name"
77 SERVICE_OPEN_PATH =
"open_path"
78 SERVICE_POWER_COMMAND =
"power_command"
79 SERVICE_OPEN_URL =
"open_url"
80 SERVICE_SEND_KEYPRESS =
"send_keypress"
81 SERVICE_SEND_TEXT =
"send_text"
84 "hibernate":
"power_hibernate",
86 "logout":
"power_logout",
87 "restart":
"power_restart",
88 "shutdown":
"power_shutdown",
89 "sleep":
"power_sleep",
97 """Set up System Bridge from a config entry."""
101 entry.data[CONF_HOST],
102 entry.data[CONF_PORT],
103 entry.data[CONF_TOKEN],
108 async
with asyncio.timeout(DATA_WAIT_TIMEOUT):
109 supported = await version.check_supported()
110 except AuthenticationException
as exception:
111 _LOGGER.error(
"Authentication failed for %s: %s", entry.title, exception)
113 translation_domain=DOMAIN,
114 translation_key=
"authentication_failed",
115 translation_placeholders={
116 "title": entry.title,
117 "host": entry.data[CONF_HOST],
120 except (ConnectionClosedException, ConnectionErrorException)
as exception:
122 translation_domain=DOMAIN,
123 translation_key=
"connection_failed",
124 translation_placeholders={
125 "title": entry.title,
126 "host": entry.data[CONF_HOST],
129 except TimeoutError
as exception:
131 translation_domain=DOMAIN,
132 translation_key=
"timeout",
133 translation_placeholders={
134 "title": entry.title,
135 "host": entry.data[CONF_HOST],
144 issue_id=f
"system_bridge_{entry.entry_id}_unsupported_version",
145 translation_key=
"unsupported_version",
146 translation_placeholders={
"host": entry.data[CONF_HOST]},
147 severity=IssueSeverity.ERROR,
151 translation_domain=DOMAIN,
152 translation_key=
"unsupported_version",
153 translation_placeholders={
154 "title": entry.title,
155 "host": entry.data[CONF_HOST],
166 async
with asyncio.timeout(DATA_WAIT_TIMEOUT):
167 await coordinator.async_get_data(MODULES)
168 except AuthenticationException
as exception:
169 _LOGGER.error(
"Authentication failed for %s: %s", entry.title, exception)
171 translation_domain=DOMAIN,
172 translation_key=
"authentication_failed",
173 translation_placeholders={
174 "title": entry.title,
175 "host": entry.data[CONF_HOST],
178 except (ConnectionClosedException, ConnectionErrorException)
as exception:
180 translation_domain=DOMAIN,
181 translation_key=
"connection_failed",
182 translation_placeholders={
183 "title": entry.title,
184 "host": entry.data[CONF_HOST],
187 except TimeoutError
as exception:
189 translation_domain=DOMAIN,
190 translation_key=
"timeout",
191 translation_placeholders={
192 "title": entry.title,
193 "host": entry.data[CONF_HOST],
198 await coordinator.async_config_entry_first_refresh()
200 hass.data.setdefault(DOMAIN, {})
201 hass.data[DOMAIN][entry.entry_id] = coordinator
204 await hass.config_entries.async_forward_entry_setups(
205 entry, [platform
for platform
in PLATFORMS
if platform != Platform.NOTIFY]
209 hass.async_create_task(
210 discovery.async_load_platform(
215 CONF_NAME: f
"{DOMAIN}_{coordinator.data.system.hostname}",
216 CONF_ENTITY_ID: entry.entry_id,
218 hass.data[DOMAIN][entry.entry_id],
222 if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL):
225 def valid_device(device: str) -> str:
226 """Check device is valid."""
227 device_registry = dr.async_get(hass)
228 device_entry = device_registry.async_get(device)
229 if device_entry
is not None:
233 for entry
in hass.config_entries.async_entries(DOMAIN)
234 if entry.entry_id
in device_entry.config_entries
236 except StopIteration
as exception:
238 translation_domain=DOMAIN,
239 translation_key=
"device_not_found",
240 translation_placeholders={
"device": device},
243 translation_domain=DOMAIN,
244 translation_key=
"device_not_found",
245 translation_placeholders={
"device": device},
248 async
def handle_get_process_by_id(service_call: ServiceCall) -> ServiceResponse:
249 """Handle the get process by id service call."""
250 _LOGGER.debug(
"Get process by id: %s", service_call.data)
251 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
252 service_call.data[CONF_BRIDGE]
254 processes: list[Process] = coordinator.data.processes
261 for process
in processes
262 if process.id == service_call.data[CONF_ID]
265 except StopIteration
as exception:
267 translation_domain=DOMAIN,
268 translation_key=
"process_not_found",
269 translation_placeholders={
"id": service_call.data[CONF_ID]},
272 async
def handle_get_processes_by_name(
273 service_call: ServiceCall,
274 ) -> ServiceResponse:
275 """Handle the get process by name service call."""
276 _LOGGER.debug(
"Get process by name: %s", service_call.data)
277 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
278 service_call.data[CONF_BRIDGE]
282 items: list[dict[str, Any]] = [
284 for process
in coordinator.data.processes
285 if process.name
is not None
286 and service_call.data[CONF_NAME].lower()
in process.name.lower()
291 "processes":
list(items),
294 async
def handle_open_path(service_call: ServiceCall) -> ServiceResponse:
295 """Handle the open path service call."""
296 _LOGGER.debug(
"Open path: %s", service_call.data)
297 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
298 service_call.data[CONF_BRIDGE]
300 response = await coordinator.websocket_client.open_path(
301 OpenPath(path=service_call.data[CONF_PATH])
303 return asdict(response)
305 async
def handle_power_command(service_call: ServiceCall) -> ServiceResponse:
306 """Handle the power command service call."""
307 _LOGGER.debug(
"Power command: %s", service_call.data)
308 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
309 service_call.data[CONF_BRIDGE]
311 response = await getattr(
312 coordinator.websocket_client,
313 POWER_COMMAND_MAP[service_call.data[CONF_COMMAND]],
315 return asdict(response)
317 async
def handle_open_url(service_call: ServiceCall) -> ServiceResponse:
318 """Handle the open url service call."""
319 _LOGGER.debug(
"Open URL: %s", service_call.data)
320 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
321 service_call.data[CONF_BRIDGE]
323 response = await coordinator.websocket_client.open_url(
324 OpenUrl(url=service_call.data[CONF_URL])
326 return asdict(response)
328 async
def handle_send_keypress(service_call: ServiceCall) -> ServiceResponse:
329 """Handle the send_keypress service call."""
330 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
331 service_call.data[CONF_BRIDGE]
333 response = await coordinator.websocket_client.keyboard_keypress(
334 KeyboardKey(key=service_call.data[CONF_KEY])
336 return asdict(response)
338 async
def handle_send_text(service_call: ServiceCall) -> ServiceResponse:
339 """Handle the send_keypress service call."""
340 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
341 service_call.data[CONF_BRIDGE]
343 response = await coordinator.websocket_client.keyboard_text(
344 KeyboardText(text=service_call.data[CONF_TEXT])
346 return asdict(response)
348 hass.services.async_register(
350 SERVICE_GET_PROCESS_BY_ID,
351 handle_get_process_by_id,
354 vol.Required(CONF_BRIDGE): valid_device,
355 vol.Required(CONF_ID): cv.positive_int,
358 supports_response=SupportsResponse.ONLY,
361 hass.services.async_register(
363 SERVICE_GET_PROCESSES_BY_NAME,
364 handle_get_processes_by_name,
367 vol.Required(CONF_BRIDGE): valid_device,
368 vol.Required(CONF_NAME): cv.string,
371 supports_response=SupportsResponse.ONLY,
374 hass.services.async_register(
380 vol.Required(CONF_BRIDGE): valid_device,
381 vol.Required(CONF_PATH): cv.string,
384 supports_response=SupportsResponse.ONLY,
387 hass.services.async_register(
389 SERVICE_POWER_COMMAND,
390 handle_power_command,
393 vol.Required(CONF_BRIDGE): valid_device,
394 vol.Required(CONF_COMMAND): vol.In(POWER_COMMAND_MAP),
397 supports_response=SupportsResponse.ONLY,
400 hass.services.async_register(
406 vol.Required(CONF_BRIDGE): valid_device,
407 vol.Required(CONF_URL): cv.string,
410 supports_response=SupportsResponse.ONLY,
413 hass.services.async_register(
415 SERVICE_SEND_KEYPRESS,
416 handle_send_keypress,
419 vol.Required(CONF_BRIDGE): valid_device,
420 vol.Required(CONF_KEY): cv.string,
423 supports_response=SupportsResponse.ONLY,
426 hass.services.async_register(
432 vol.Required(CONF_BRIDGE): valid_device,
433 vol.Required(CONF_TEXT): cv.string,
436 supports_response=SupportsResponse.ONLY,
440 entry.async_on_unload(entry.add_update_listener(async_reload_entry))
446 """Unload a config entry."""
447 unload_ok = await hass.config_entries.async_unload_platforms(
448 entry, [platform
for platform
in PLATFORMS
if platform != Platform.NOTIFY]
451 coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][
456 await coordinator.websocket_client.close()
457 if coordinator.unsub:
460 del hass.data[DOMAIN][entry.entry_id]
462 if not hass.data[DOMAIN]:
463 hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH)
464 hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL)
465 hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS)
466 hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT)
472 """Reload the config entry when it changed."""
473 await hass.config_entries.async_reload(entry.entry_id)
477 """Migrate old entry."""
479 "Migrating from version %s.%s",
480 config_entry.version,
481 config_entry.minor_version,
484 if config_entry.version > SystemBridgeConfigFlow.VERSION:
487 if config_entry.minor_version < 2:
489 new_data =
dict(config_entry.data)
490 new_data.setdefault(CONF_TOKEN, config_entry.data.get(CONF_API_KEY))
492 hass.config_entries.async_update_entry(
499 "Migration to version %s.%s successful",
500 config_entry.version,
501 config_entry.minor_version,
None async_create_issue(HomeAssistant hass, str entry_id)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None async_reload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)