├── README.md ├── custom_components └── dlna_dmr_xiaodu │ ├── __init__.py │ ├── config_flow.py │ ├── const.py │ ├── data.py │ ├── manifest.json │ ├── media_player.py │ ├── strings.json │ └── translations │ ├── bg.json │ ├── ca.json │ ├── cs.json │ ├── de.json │ ├── el.json │ ├── en.json │ ├── es-419.json │ ├── es.json │ ├── et.json │ ├── fr.json │ ├── he.json │ ├── hu.json │ ├── id.json │ ├── is.json │ ├── it.json │ ├── ja.json │ ├── ko.json │ ├── nl.json │ ├── no.json │ ├── pl.json │ ├── pt-BR.json │ ├── ru.json │ ├── sl.json │ ├── sv.json │ ├── tr.json │ ├── zh-Hans.json │ └── zh-Hant.json └── hacs.json /README.md: -------------------------------------------------------------------------------- 1 | # DLNA_DMR_XIAODU 2 | dlna_dmr for xiaodu 3 | 4 | 5 | 此版本请使用homeassistant 2024.7以后的版本\ 6 | 官方的DLNA_DMR修改后适配小度音箱,由于小度音箱每次启动UUID会变动,这里以xml地址为准。\ 7 | 解决小度不能自动停止需要每次点两次TTS才能发声的问题。\ 8 | 支持小讯R1音箱安装app常开dlna的TTS。(官方原版只能支持开启蓝牙的内置dlna)。\ 9 | 其它设备按官方原版DLNA_DMR不变。 10 | 11 | 同官方集成一样支持跨网段使用,配置时不选择直接提交,手动输入xml网址。 12 | 13 | 小度音箱: http://小度音箱IP:49494/description.xml 14 | 15 | R1音箱安装R1-dlna.apk后 : http://R1音箱IP:38520/description.xml 16 | 17 | 18 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/__init__.py: -------------------------------------------------------------------------------- 1 | """The dlna_dmr component.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant import config_entries 5 | from homeassistant.const import Platform 6 | from homeassistant.core import HomeAssistant 7 | 8 | from .const import LOGGER 9 | 10 | PLATFORMS = [Platform.MEDIA_PLAYER] 11 | 12 | 13 | async def async_setup_entry( 14 | hass: HomeAssistant, entry: config_entries.ConfigEntry 15 | ) -> bool: 16 | """Set up a DLNA DMR device from a config entry.""" 17 | LOGGER.debug("Setting up config entry: %s", entry.unique_id) 18 | 19 | # Forward setup to the appropriate platform 20 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 21 | 22 | return True 23 | 24 | 25 | async def async_unload_entry( 26 | hass: HomeAssistant, config_entry: config_entries.ConfigEntry 27 | ) -> bool: 28 | """Unload a config entry.""" 29 | # Forward to the same platform as async_setup_entry did 30 | return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) 31 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for DLNA DMR.""" 2 | from __future__ import annotations 3 | 4 | from collections.abc import Callable, Mapping 5 | import logging 6 | from pprint import pformat 7 | from typing import Any, Optional, cast 8 | from urllib.parse import urlparse 9 | 10 | from async_upnp_client.client import UpnpError 11 | from async_upnp_client.profiles.dlna import DmrDevice 12 | from async_upnp_client.profiles.profile import find_device_of_type 13 | import voluptuous as vol 14 | 15 | from homeassistant import config_entries 16 | from homeassistant.components import ssdp 17 | from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_TYPE, CONF_URL 18 | from homeassistant.core import callback 19 | from homeassistant.data_entry_flow import FlowResult 20 | from homeassistant.exceptions import IntegrationError 21 | import homeassistant.helpers.config_validation as cv 22 | 23 | from .const import ( 24 | CONF_BROWSE_UNFILTERED, 25 | CONF_CALLBACK_URL_OVERRIDE, 26 | CONF_LISTEN_PORT, 27 | CONF_POLL_AVAILABILITY, 28 | DEFAULT_NAME, 29 | DOMAIN, 30 | ) 31 | from .data import get_domain_data 32 | 33 | LOGGER = logging.getLogger(__name__) 34 | 35 | FlowInput = Optional[Mapping[str, Any]] 36 | 37 | 38 | class ConnectError(IntegrationError): 39 | """Error occurred when trying to connect to a device.""" 40 | 41 | 42 | class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 43 | """Handle a DLNA DMR config flow. 44 | 45 | The Unique Device Name (UDN) of the DMR device is used as the unique_id for 46 | config entries and for entities. This UDN may differ from the root UDN if 47 | the DMR is an embedded device. 48 | """ 49 | 50 | VERSION = 1 51 | 52 | def __init__(self) -> None: 53 | """Initialize flow.""" 54 | self._discoveries: dict[str, ssdp.SsdpServiceInfo] = {} 55 | self._location: str | None = None 56 | #self._udn: str | None = None 57 | self._device_type: str | None = None 58 | self._name: str | None = None 59 | self._options: dict[str, Any] = {} 60 | 61 | @staticmethod 62 | @callback 63 | def async_get_options_flow( 64 | config_entry: config_entries.ConfigEntry, 65 | ) -> config_entries.OptionsFlow: 66 | """Define the config flow to handle options.""" 67 | return DlnaDmrOptionsFlowHandler(config_entry) 68 | 69 | async def async_step_user(self, user_input: FlowInput = None) -> FlowResult: 70 | """Handle a flow initialized by the user. 71 | 72 | Let user choose from a list of found and unconfigured devices or to 73 | enter an URL manually. 74 | """ 75 | LOGGER.debug("async_step_user: user_input: %s", user_input) 76 | 77 | if user_input is not None: 78 | if not (host := user_input.get(CONF_HOST)): 79 | # No device chosen, user might want to directly enter an URL 80 | return await self.async_step_manual() 81 | # User has chosen a device, ask for confirmation 82 | discovery = self._discoveries[host] 83 | await self._async_set_info_from_discovery(discovery) 84 | return self._create_entry() 85 | 86 | if not (discoveries := await self._async_get_discoveries()): 87 | # Nothing found, maybe the user knows an URL to try 88 | return await self.async_step_manual() 89 | 90 | self._discoveries = { 91 | discovery.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) 92 | or cast(str, urlparse(discovery.ssdp_location).hostname): discovery 93 | for discovery in discoveries 94 | } 95 | 96 | data_schema = vol.Schema( 97 | {vol.Optional(CONF_HOST): vol.In(self._discoveries.keys())} 98 | ) 99 | return self.async_show_form(step_id="user", data_schema=data_schema) 100 | 101 | async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: 102 | """Manual URL entry by the user.""" 103 | LOGGER.debug("async_step_manual: user_input: %s", user_input) 104 | 105 | # Device setup manually, assume we don't get SSDP broadcast notifications 106 | self._options[CONF_POLL_AVAILABILITY] = True 107 | 108 | errors = {} 109 | if user_input is not None: 110 | self._location = user_input[CONF_URL] 111 | try: 112 | await self._async_connect() 113 | except ConnectError as err: 114 | errors["base"] = err.args[0] 115 | else: 116 | return self._create_entry() 117 | 118 | data_schema = vol.Schema({CONF_URL: str}) 119 | return self.async_show_form( 120 | step_id="manual", data_schema=data_schema, errors=errors 121 | ) 122 | 123 | async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: 124 | """Handle a flow initialized by SSDP discovery.""" 125 | LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) 126 | 127 | await self._async_set_info_from_discovery(discovery_info) 128 | 129 | if _is_ignored_device(discovery_info): 130 | return self.async_abort(reason="alternative_integration") 131 | 132 | # Abort if the device doesn't support all services required for a DmrDevice. 133 | # Use the discovery_info instead of DmrDevice.is_profile_device to avoid 134 | # contacting the device again. 135 | discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST) 136 | if not discovery_service_list: 137 | return self.async_abort(reason="not_dmr") 138 | 139 | services = discovery_service_list.get("service") 140 | if not services: 141 | discovery_service_ids: set[str] = set() 142 | elif isinstance(services, list): 143 | discovery_service_ids = {service.get("serviceId") for service in services} 144 | else: 145 | # Only one service defined (etree_to_dict failed to make a list) 146 | discovery_service_ids = {services.get("serviceId")} 147 | 148 | if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids): 149 | return self.async_abort(reason="not_dmr") 150 | 151 | # Abort if another config entry has the same location, in case the 152 | # device doesn't have a static and unique UDN (breaking the UPnP spec). 153 | self._async_abort_entries_match({CONF_URL: self._location}) 154 | 155 | self.context["title_placeholders"] = {"name": self._name} 156 | 157 | return await self.async_step_confirm() 158 | 159 | async def async_step_unignore(self, user_input: Mapping[str, Any]) -> FlowResult: 160 | """Rediscover previously ignored devices by their unique_id.""" 161 | LOGGER.debug("async_step_unignore: user_input: %s", user_input) 162 | self._location = user_input["location"] 163 | assert self._location 164 | await self.async_set_unique_id(self._location.replace(":","_").replace("/","_")) 165 | 166 | # Find a discovery matching the unignored unique_id for a DMR device 167 | for dev_type in DmrDevice.DEVICE_TYPES: 168 | discovery = await ssdp.async_get_discovery_info_by_udn_st( 169 | self.hass, self._location, dev_type 170 | ) 171 | if discovery: 172 | break 173 | else: 174 | return self.async_abort(reason="discovery_error") 175 | 176 | await self._async_set_info_from_discovery(discovery, abort_if_configured=False) 177 | 178 | self.context["title_placeholders"] = {"name": self._name} 179 | 180 | return await self.async_step_confirm() 181 | 182 | async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: 183 | """Allow the user to confirm adding the device.""" 184 | LOGGER.debug("async_step_confirm: %s", user_input) 185 | 186 | if user_input is not None: 187 | return self._create_entry() 188 | 189 | self._set_confirm_only() 190 | return self.async_show_form(step_id="confirm") 191 | 192 | async def _async_connect(self) -> None: 193 | """Connect to a device to confirm it works and gather extra information. 194 | 195 | Updates this flow's unique ID to the device UDN if not already done. 196 | Raises ConnectError if something goes wrong. 197 | """ 198 | LOGGER.debug("_async_connect: location: %s", self._location) 199 | assert self._location, "self._location has not been set before connect" 200 | 201 | domain_data = get_domain_data(self.hass) 202 | try: 203 | device = await domain_data.upnp_factory.async_create_device(self._location) 204 | except UpnpError as err: 205 | raise ConnectError("cannot_connect") from err 206 | 207 | if not DmrDevice.is_profile_device(device): 208 | raise ConnectError("not_dmr") 209 | 210 | device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) 211 | 212 | if not self._location: 213 | self._location = device.location 214 | await self.async_set_unique_id(self._location.replace(":","_").replace("/","_")) 215 | 216 | # Abort if already configured, but update the last-known location 217 | self._abort_if_unique_id_configured( 218 | updates={CONF_URL: self._location}, reload_on_update=False 219 | ) 220 | 221 | if not self._device_type: 222 | self._device_type = device.device_type 223 | 224 | if not self._name: 225 | self._name = device.name 226 | 227 | def _create_entry(self) -> FlowResult: 228 | """Create a config entry, assuming all required information is now known.""" 229 | LOGGER.debug( 230 | "_async_create_entry: location: %s", self._location 231 | ) 232 | assert self._location 233 | assert self._device_type 234 | 235 | title = self._name or urlparse(self._location).hostname or DEFAULT_NAME 236 | data = { 237 | CONF_URL: self._location, 238 | CONF_DEVICE_ID: self._location, 239 | CONF_TYPE: self._device_type, 240 | } 241 | return self.async_create_entry(title=title, data=data, options=self._options) 242 | 243 | async def _async_set_info_from_discovery( 244 | self, discovery_info: ssdp.SsdpServiceInfo, abort_if_configured: bool = True 245 | ) -> None: 246 | """Set information required for a config entry from the SSDP discovery.""" 247 | LOGGER.debug( 248 | "_async_set_info_from_discovery: location: %s", 249 | discovery_info.ssdp_location, 250 | ) 251 | 252 | if not self._location: 253 | self._location = discovery_info.ssdp_location 254 | assert isinstance(self._location, str) 255 | 256 | await self.async_set_unique_id(self._location) 257 | 258 | if abort_if_configured: 259 | # Abort if already configured, but update the last-known location 260 | self._abort_if_unique_id_configured( 261 | updates={CONF_URL: self._location}, reload_on_update=False 262 | ) 263 | 264 | self._device_type = discovery_info.ssdp_nt or discovery_info.ssdp_st 265 | self._name = ( 266 | discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) 267 | or urlparse(self._location).hostname 268 | or DEFAULT_NAME 269 | ) 270 | 271 | async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]: 272 | """Get list of unconfigured DLNA devices discovered by SSDP.""" 273 | LOGGER.debug("_get_discoveries") 274 | 275 | # Get all compatible devices from ssdp's cache 276 | discoveries: list[ssdp.SsdpServiceInfo] = [] 277 | for udn_st in DmrDevice.DEVICE_TYPES: 278 | st_discoveries = await ssdp.async_get_discovery_info_by_st( 279 | self.hass, udn_st 280 | ) 281 | discoveries.extend(st_discoveries) 282 | 283 | # Filter out devices already configured 284 | current_unique_ids = { 285 | entry.unique_id 286 | for entry in self._async_current_entries(include_ignore=False) 287 | } 288 | discoveries = [ 289 | disc for disc in discoveries if disc.ssdp_location not in current_unique_ids 290 | ] 291 | 292 | return discoveries 293 | 294 | 295 | class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): 296 | """Handle a DLNA DMR options flow. 297 | 298 | Configures the single instance and updates the existing config entry. 299 | """ 300 | 301 | def __init__(self, config_entry: config_entries.ConfigEntry) -> None: 302 | """Initialize.""" 303 | self.config_entry = config_entry 304 | 305 | async def async_step_init( 306 | self, user_input: dict[str, Any] | None = None 307 | ) -> FlowResult: 308 | """Manage the options.""" 309 | errors: dict[str, str] = {} 310 | # Don't modify existing (read-only) options -- copy and update instead 311 | options = dict(self.config_entry.options) 312 | 313 | if user_input is not None: 314 | LOGGER.debug("user_input: %s", user_input) 315 | listen_port = user_input.get(CONF_LISTEN_PORT) or None 316 | callback_url_override = user_input.get(CONF_CALLBACK_URL_OVERRIDE) or None 317 | 318 | try: 319 | # Cannot use cv.url validation in the schema itself so apply 320 | # extra validation here 321 | if callback_url_override: 322 | cv.url(callback_url_override) 323 | except vol.Invalid: 324 | errors["base"] = "invalid_url" 325 | 326 | options[CONF_LISTEN_PORT] = listen_port 327 | options[CONF_CALLBACK_URL_OVERRIDE] = callback_url_override 328 | options[CONF_POLL_AVAILABILITY] = user_input[CONF_POLL_AVAILABILITY] 329 | options[CONF_BROWSE_UNFILTERED] = user_input[CONF_BROWSE_UNFILTERED] 330 | 331 | # Save if there's no errors, else fall through and show the form again 332 | if not errors: 333 | return self.async_create_entry(title="", data=options) 334 | 335 | fields = {} 336 | 337 | def _add_with_suggestion(key: str, validator: Callable | type[bool]) -> None: 338 | """Add a field to with a suggested value. 339 | 340 | For bools, use the existing value as default, or fallback to False. 341 | """ 342 | if validator is bool: 343 | fields[vol.Required(key, default=options.get(key, False))] = validator 344 | elif (suggested_value := options.get(key)) is None: 345 | fields[vol.Optional(key)] = validator 346 | else: 347 | fields[ 348 | vol.Optional(key, description={"suggested_value": suggested_value}) 349 | ] = validator 350 | 351 | # listen_port can be blank or 0 for "bind any free port" 352 | _add_with_suggestion(CONF_LISTEN_PORT, cv.port) 353 | _add_with_suggestion(CONF_CALLBACK_URL_OVERRIDE, str) 354 | _add_with_suggestion(CONF_POLL_AVAILABILITY, bool) 355 | _add_with_suggestion(CONF_BROWSE_UNFILTERED, bool) 356 | 357 | return self.async_show_form( 358 | step_id="init", 359 | data_schema=vol.Schema(fields), 360 | errors=errors, 361 | ) 362 | 363 | 364 | def _is_ignored_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: 365 | """Return True if this device should be ignored for discovery. 366 | 367 | These devices are supported better by other integrations, so don't bug 368 | the user about them. The user can add them if desired by via the user config 369 | flow, which will list all discovered but unconfigured devices. 370 | """ 371 | # Did the discovery trigger more than just this flow? 372 | if len(discovery_info.x_homeassistant_matching_domains) > 1: 373 | LOGGER.debug( 374 | "Ignoring device supported by multiple integrations: %s", 375 | discovery_info.x_homeassistant_matching_domains, 376 | ) 377 | return True 378 | 379 | # Is the root device not a DMR? 380 | if ( 381 | discovery_info.upnp.get(ssdp.ATTR_UPNP_DEVICE_TYPE) 382 | not in DmrDevice.DEVICE_TYPES 383 | ): 384 | return True 385 | 386 | # Special cases for devices with other discovery methods (e.g. mDNS), or 387 | # that advertise multiple unrelated (sent in separate discovery packets) 388 | # UPnP devices. 389 | manufacturer = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER) or "").lower() 390 | model = (discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "").lower() 391 | 392 | if manufacturer.startswith("xbmc") or model == "kodi": 393 | # kodi 394 | return True 395 | if "philips" in manufacturer and "tv" in model: 396 | # philips_js 397 | # These TVs don't have a stable UDN, so also get discovered as a new 398 | # device every time they are turned on. 399 | return True 400 | if manufacturer.startswith("samsung") and "tv" in model: 401 | # samsungtv 402 | return True 403 | if manufacturer.startswith("lg") and "tv" in model: 404 | # webostv 405 | return True 406 | if manufacturer.startswith("DuerOS"): 407 | # DuerOS 408 | return True 409 | 410 | return False 411 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the DLNA DMR component.""" 2 | from __future__ import annotations 3 | 4 | from collections.abc import Mapping 5 | import logging 6 | from typing import Final 7 | 8 | from async_upnp_client.profiles.dlna import PlayMode as _PlayMode 9 | 10 | from homeassistant.components.media_player import const as _mp_const 11 | 12 | LOGGER = logging.getLogger(__package__) 13 | 14 | DOMAIN: Final = "dlna_dmr_xiaodu" 15 | 16 | CONF_LISTEN_PORT: Final = "listen_port" 17 | CONF_CALLBACK_URL_OVERRIDE: Final = "callback_url_override" 18 | CONF_POLL_AVAILABILITY: Final = "poll_availability" 19 | CONF_BROWSE_UNFILTERED: Final = "browse_unfiltered" 20 | 21 | DEFAULT_NAME: Final = "DLNA Digital Media Renderer" 22 | 23 | CONNECT_TIMEOUT: Final = 10 24 | 25 | PROTOCOL_HTTP: Final = "http-get" 26 | PROTOCOL_RTSP: Final = "rtsp-rtp-udp" 27 | PROTOCOL_ANY: Final = "*" 28 | STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] 29 | 30 | # Map UPnP class to media_player media_content_type 31 | MEDIA_TYPE_MAP: Mapping[str, str] = { 32 | "object": _mp_const.MEDIA_TYPE_URL, 33 | "object.item": _mp_const.MEDIA_TYPE_URL, 34 | "object.item.imageItem": _mp_const.MEDIA_TYPE_IMAGE, 35 | "object.item.imageItem.photo": _mp_const.MEDIA_TYPE_IMAGE, 36 | "object.item.audioItem": _mp_const.MEDIA_TYPE_MUSIC, 37 | "object.item.audioItem.musicTrack": _mp_const.MEDIA_TYPE_MUSIC, 38 | "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_TYPE_MUSIC, 39 | "object.item.audioItem.audioBook": _mp_const.MEDIA_TYPE_PODCAST, 40 | "object.item.videoItem": _mp_const.MEDIA_TYPE_VIDEO, 41 | "object.item.videoItem.movie": _mp_const.MEDIA_TYPE_MOVIE, 42 | "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_TYPE_TVSHOW, 43 | "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_TYPE_VIDEO, 44 | "object.item.playlistItem": _mp_const.MEDIA_TYPE_PLAYLIST, 45 | "object.item.textItem": _mp_const.MEDIA_TYPE_URL, 46 | "object.item.bookmarkItem": _mp_const.MEDIA_TYPE_URL, 47 | "object.item.epgItem": _mp_const.MEDIA_TYPE_EPISODE, 48 | "object.item.epgItem.audioProgram": _mp_const.MEDIA_TYPE_EPISODE, 49 | "object.item.epgItem.videoProgram": _mp_const.MEDIA_TYPE_EPISODE, 50 | "object.container": _mp_const.MEDIA_TYPE_PLAYLIST, 51 | "object.container.person": _mp_const.MEDIA_TYPE_ARTIST, 52 | "object.container.person.musicArtist": _mp_const.MEDIA_TYPE_ARTIST, 53 | "object.container.playlistContainer": _mp_const.MEDIA_TYPE_PLAYLIST, 54 | "object.container.album": _mp_const.MEDIA_TYPE_ALBUM, 55 | "object.container.album.musicAlbum": _mp_const.MEDIA_TYPE_ALBUM, 56 | "object.container.album.photoAlbum": _mp_const.MEDIA_TYPE_ALBUM, 57 | "object.container.genre": _mp_const.MEDIA_TYPE_GENRE, 58 | "object.container.genre.musicGenre": _mp_const.MEDIA_TYPE_GENRE, 59 | "object.container.genre.movieGenre": _mp_const.MEDIA_TYPE_GENRE, 60 | "object.container.channelGroup": _mp_const.MEDIA_TYPE_CHANNELS, 61 | "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, 62 | "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, 63 | "object.container.epgContainer": _mp_const.MEDIA_TYPE_TVSHOW, 64 | "object.container.storageSystem": _mp_const.MEDIA_TYPE_PLAYLIST, 65 | "object.container.storageVolume": _mp_const.MEDIA_TYPE_PLAYLIST, 66 | "object.container.storageFolder": _mp_const.MEDIA_TYPE_PLAYLIST, 67 | "object.container.bookmarkFolder": _mp_const.MEDIA_TYPE_PLAYLIST, 68 | } 69 | 70 | # Map media_player media_content_type to UPnP class. Not everything will map 71 | # directly, in which case it's not specified and other defaults will be used. 72 | MEDIA_UPNP_CLASS_MAP: Mapping[str, str] = { 73 | _mp_const.MEDIA_TYPE_ALBUM: "object.container.album.musicAlbum", 74 | _mp_const.MEDIA_TYPE_ARTIST: "object.container.person.musicArtist", 75 | _mp_const.MEDIA_TYPE_CHANNEL: "object.item.videoItem.videoBroadcast", 76 | _mp_const.MEDIA_TYPE_CHANNELS: "object.container.channelGroup", 77 | _mp_const.MEDIA_TYPE_COMPOSER: "object.container.person.musicArtist", 78 | _mp_const.MEDIA_TYPE_CONTRIBUTING_ARTIST: "object.container.person.musicArtist", 79 | _mp_const.MEDIA_TYPE_EPISODE: "object.item.epgItem.videoProgram", 80 | _mp_const.MEDIA_TYPE_GENRE: "object.container.genre", 81 | _mp_const.MEDIA_TYPE_IMAGE: "object.item.imageItem", 82 | _mp_const.MEDIA_TYPE_MOVIE: "object.item.videoItem.movie", 83 | _mp_const.MEDIA_TYPE_MUSIC: "object.item.audioItem.musicTrack", 84 | _mp_const.MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", 85 | _mp_const.MEDIA_TYPE_PODCAST: "object.item.audioItem.audioBook", 86 | _mp_const.MEDIA_TYPE_SEASON: "object.item.epgItem.videoProgram", 87 | _mp_const.MEDIA_TYPE_TRACK: "object.item.audioItem.musicTrack", 88 | _mp_const.MEDIA_TYPE_TVSHOW: "object.item.videoItem.videoBroadcast", 89 | _mp_const.MEDIA_TYPE_URL: "object.item.bookmarkItem", 90 | _mp_const.MEDIA_TYPE_VIDEO: "object.item.videoItem", 91 | } 92 | 93 | # Translation of MediaMetadata keys to DIDL-Lite keys. 94 | # See https://developers.google.com/cast/docs/reference/messages#MediaData via 95 | # https://www.home-assistant.io/integrations/media_player/ for HA keys. 96 | # See http://www.upnp.org/specs/av/UPnP-av-ContentDirectory-v4-Service.pdf for 97 | # DIDL-Lite keys. 98 | MEDIA_METADATA_DIDL: Mapping[str, str] = { 99 | "subtitle": "longDescription", 100 | "releaseDate": "date", 101 | "studio": "publisher", 102 | "season": "episodeSeason", 103 | "episode": "episodeNumber", 104 | "albumName": "album", 105 | "trackNumber": "originalTrackNumber", 106 | } 107 | 108 | # For (un)setting repeat mode, map a combination of shuffle & repeat to a list 109 | # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any 110 | # case. NOTE: This list is slightly different to that in SHUFFLE_PLAY_MODES, 111 | # due to fallback behaviour when turning on repeat modes. 112 | REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { 113 | (False, _mp_const.REPEAT_MODE_OFF): [ 114 | _PlayMode.NORMAL, 115 | ], 116 | (False, _mp_const.REPEAT_MODE_ONE): [ 117 | _PlayMode.REPEAT_ONE, 118 | _PlayMode.REPEAT_ALL, 119 | _PlayMode.NORMAL, 120 | ], 121 | (False, _mp_const.REPEAT_MODE_ALL): [ 122 | _PlayMode.REPEAT_ALL, 123 | _PlayMode.REPEAT_ONE, 124 | _PlayMode.NORMAL, 125 | ], 126 | (True, _mp_const.REPEAT_MODE_OFF): [ 127 | _PlayMode.SHUFFLE, 128 | _PlayMode.RANDOM, 129 | _PlayMode.NORMAL, 130 | ], 131 | (True, _mp_const.REPEAT_MODE_ONE): [ 132 | _PlayMode.REPEAT_ONE, 133 | _PlayMode.RANDOM, 134 | _PlayMode.SHUFFLE, 135 | _PlayMode.NORMAL, 136 | ], 137 | (True, _mp_const.REPEAT_MODE_ALL): [ 138 | _PlayMode.RANDOM, 139 | _PlayMode.REPEAT_ALL, 140 | _PlayMode.SHUFFLE, 141 | _PlayMode.NORMAL, 142 | ], 143 | } 144 | 145 | # For (un)setting shuffle mode, map a combination of shuffle & repeat to a list 146 | # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any 147 | # case. 148 | SHUFFLE_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { 149 | (False, _mp_const.REPEAT_MODE_OFF): [ 150 | _PlayMode.NORMAL, 151 | ], 152 | (False, _mp_const.REPEAT_MODE_ONE): [ 153 | _PlayMode.REPEAT_ONE, 154 | _PlayMode.REPEAT_ALL, 155 | _PlayMode.NORMAL, 156 | ], 157 | (False, _mp_const.REPEAT_MODE_ALL): [ 158 | _PlayMode.REPEAT_ALL, 159 | _PlayMode.REPEAT_ONE, 160 | _PlayMode.NORMAL, 161 | ], 162 | (True, _mp_const.REPEAT_MODE_OFF): [ 163 | _PlayMode.SHUFFLE, 164 | _PlayMode.RANDOM, 165 | _PlayMode.NORMAL, 166 | ], 167 | (True, _mp_const.REPEAT_MODE_ONE): [ 168 | _PlayMode.RANDOM, 169 | _PlayMode.SHUFFLE, 170 | _PlayMode.REPEAT_ONE, 171 | _PlayMode.NORMAL, 172 | ], 173 | (True, _mp_const.REPEAT_MODE_ALL): [ 174 | _PlayMode.RANDOM, 175 | _PlayMode.SHUFFLE, 176 | _PlayMode.REPEAT_ALL, 177 | _PlayMode.NORMAL, 178 | ], 179 | } 180 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/data.py: -------------------------------------------------------------------------------- 1 | """Data used by this integration.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from collections import defaultdict 6 | from typing import NamedTuple, cast 7 | 8 | from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester 9 | from async_upnp_client.client import UpnpRequester 10 | from async_upnp_client.client_factory import UpnpFactory 11 | from async_upnp_client.event_handler import UpnpEventHandler 12 | 13 | from homeassistant.const import EVENT_HOMEASSISTANT_STOP 14 | from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant 15 | from homeassistant.helpers import aiohttp_client 16 | 17 | from .const import DOMAIN, LOGGER 18 | 19 | 20 | class EventListenAddr(NamedTuple): 21 | """Unique identifier for an event listener.""" 22 | 23 | host: str | None # Specific local IP(v6) address for listening on 24 | port: int # Listening port, 0 means use an ephemeral port 25 | callback_url: str | None 26 | 27 | 28 | class DlnaDmrData: 29 | """Storage class for domain global data.""" 30 | 31 | lock: asyncio.Lock 32 | requester: UpnpRequester 33 | upnp_factory: UpnpFactory 34 | event_notifiers: dict[EventListenAddr, AiohttpNotifyServer] 35 | event_notifier_refs: defaultdict[EventListenAddr, int] 36 | stop_listener_remove: CALLBACK_TYPE | None = None 37 | 38 | def __init__(self, hass: HomeAssistant) -> None: 39 | """Initialize global data.""" 40 | self.lock = asyncio.Lock() 41 | session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) 42 | self.requester = AiohttpSessionRequester(session, with_sleep=True) 43 | self.upnp_factory = UpnpFactory(self.requester, non_strict=True) 44 | self.event_notifiers = {} 45 | self.event_notifier_refs = defaultdict(int) 46 | 47 | async def async_cleanup_event_notifiers(self, event: Event) -> None: 48 | """Clean up resources when Home Assistant is stopped.""" 49 | LOGGER.debug("Cleaning resources in DlnaDmrData") 50 | async with self.lock: 51 | tasks = ( 52 | server.async_stop_server() for server in self.event_notifiers.values() 53 | ) 54 | asyncio.gather(*tasks) 55 | self.event_notifiers = {} 56 | self.event_notifier_refs = defaultdict(int) 57 | 58 | async def async_get_event_notifier( 59 | self, listen_addr: EventListenAddr, hass: HomeAssistant 60 | ) -> UpnpEventHandler: 61 | """Return existing event notifier for the listen_addr, or create one. 62 | 63 | Only one event notify server is kept for each listen_addr. Must call 64 | async_release_event_notifier when done to cleanup resources. 65 | """ 66 | LOGGER.debug("Getting event handler for %s", listen_addr) 67 | 68 | async with self.lock: 69 | # Stop all servers when HA shuts down, to release resources on devices 70 | if not self.stop_listener_remove: 71 | self.stop_listener_remove = hass.bus.async_listen_once( 72 | EVENT_HOMEASSISTANT_STOP, self.async_cleanup_event_notifiers 73 | ) 74 | 75 | # Always increment the reference counter, for existing or new event handlers 76 | self.event_notifier_refs[listen_addr] += 1 77 | 78 | # Return an existing event handler if we can 79 | if listen_addr in self.event_notifiers: 80 | return self.event_notifiers[listen_addr].event_handler 81 | 82 | # Start event handler 83 | source = (listen_addr.host or "0.0.0.0", listen_addr.port) 84 | server = AiohttpNotifyServer( 85 | requester=self.requester, 86 | source=source, 87 | callback_url=listen_addr.callback_url, 88 | loop=hass.loop, 89 | ) 90 | await server.async_start_server() 91 | LOGGER.debug("Started event handler at %s", server.callback_url) 92 | 93 | self.event_notifiers[listen_addr] = server 94 | 95 | return server.event_handler 96 | 97 | async def async_release_event_notifier(self, listen_addr: EventListenAddr) -> None: 98 | """Indicate that the event notifier for listen_addr is not used anymore. 99 | 100 | This is called once by each caller of async_get_event_notifier, and will 101 | stop the listening server when all users are done. 102 | """ 103 | async with self.lock: 104 | assert self.event_notifier_refs[listen_addr] > 0 105 | self.event_notifier_refs[listen_addr] -= 1 106 | 107 | # Shutdown the server when it has no more users 108 | if self.event_notifier_refs[listen_addr] == 0: 109 | server = self.event_notifiers.pop(listen_addr) 110 | await server.async_stop_server() 111 | 112 | # Remove the cleanup listener when there's nothing left to cleanup 113 | if not self.event_notifiers: 114 | assert self.stop_listener_remove is not None 115 | self.stop_listener_remove() 116 | self.stop_listener_remove = None 117 | 118 | 119 | def get_domain_data(hass: HomeAssistant) -> DlnaDmrData: 120 | """Obtain this integration's domain data, creating it if needed.""" 121 | if DOMAIN in hass.data: 122 | return cast(DlnaDmrData, hass.data[DOMAIN]) 123 | 124 | data = DlnaDmrData(hass) 125 | hass.data[DOMAIN] = data 126 | return data 127 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "dlna_dmr_xiaodu", 3 | "name": "DLNA For Xiaodu", 4 | "after_dependencies": ["media_source"], 5 | "codeowners": ["@StevenLooman", "@chishm","@dscao"], 6 | "config_flow": true, 7 | "dependencies": ["ssdp"], 8 | "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", 9 | "loggers": ["async_upnp_client"], 10 | "iot_class": "local_push", 11 | "requirements": ["async-upnp-client>=0.28.0"], 12 | "ssdp": [ 13 | { 14 | "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", 15 | "st": "urn:schemas-upnp-org:device:MediaRenderer:1" 16 | }, 17 | { 18 | "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", 19 | "st": "urn:schemas-upnp-org:device:MediaRenderer:2" 20 | }, 21 | { 22 | "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", 23 | "st": "urn:schemas-upnp-org:device:MediaRenderer:3" 24 | } 25 | ], 26 | "version": "2024.7.19" 27 | } 28 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/media_player.py: -------------------------------------------------------------------------------- 1 | """Support for DLNA DMR (Device Media Renderer).""" 2 | from __future__ import annotations 3 | 4 | # import requests 5 | # import xml.etree.ElementTree as etree 6 | # import re 7 | import os 8 | from urllib.parse import unquote 9 | 10 | 11 | 12 | import asyncio 13 | from collections.abc import Awaitable, Callable, Coroutine, Sequence 14 | import contextlib 15 | from datetime import datetime, timedelta 16 | import functools 17 | from typing import Any, TypeVar 18 | 19 | from async_upnp_client.client import UpnpService, UpnpStateVariable 20 | from async_upnp_client.const import NotificationSubType 21 | from async_upnp_client.exceptions import UpnpError, UpnpResponseError 22 | from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState 23 | from async_upnp_client.utils import async_get_local_ip 24 | from didl_lite import didl_lite 25 | from typing_extensions import Concatenate, ParamSpec 26 | 27 | from homeassistant import config_entries 28 | from homeassistant.components import media_source, ssdp 29 | from homeassistant.components.media_player import ( 30 | BrowseMedia, 31 | MediaPlayerEntity, 32 | MediaPlayerEntityFeature, 33 | async_process_play_media_url, 34 | ) 35 | from homeassistant.components.media_player.const import ( 36 | ATTR_MEDIA_EXTRA, 37 | REPEAT_MODE_ALL, 38 | REPEAT_MODE_OFF, 39 | REPEAT_MODE_ONE, 40 | ) 41 | from homeassistant.const import ( 42 | CONF_DEVICE_ID, 43 | CONF_TYPE, 44 | CONF_URL, 45 | STATE_IDLE, 46 | STATE_OFF, 47 | STATE_ON, 48 | STATE_PAUSED, 49 | STATE_PLAYING, 50 | ) 51 | from homeassistant.core import HomeAssistant 52 | from homeassistant.helpers import device_registry, entity_registry 53 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 54 | 55 | from .const import ( 56 | CONF_BROWSE_UNFILTERED, 57 | CONF_CALLBACK_URL_OVERRIDE, 58 | CONF_LISTEN_PORT, 59 | CONF_POLL_AVAILABILITY, 60 | DOMAIN, 61 | LOGGER as _LOGGER, 62 | MEDIA_METADATA_DIDL, 63 | MEDIA_TYPE_MAP, 64 | MEDIA_UPNP_CLASS_MAP, 65 | REPEAT_PLAY_MODES, 66 | SHUFFLE_PLAY_MODES, 67 | STREAMABLE_PROTOCOLS, 68 | ) 69 | from .data import EventListenAddr, get_domain_data 70 | 71 | PARALLEL_UPDATES = 0 72 | 73 | _DlnaDmrEntityT = TypeVar("_DlnaDmrEntityT", bound="DlnaDmrEntity") 74 | _R = TypeVar("_R") 75 | _P = ParamSpec("_P") 76 | 77 | 78 | def catch_request_errors( 79 | func: Callable[Concatenate[_DlnaDmrEntityT, _P], Awaitable[_R]] 80 | ) -> Callable[Concatenate[_DlnaDmrEntityT, _P], Coroutine[Any, Any, _R | None]]: 81 | """Catch UpnpError errors.""" 82 | 83 | @functools.wraps(func) 84 | async def wrapper( 85 | self: _DlnaDmrEntityT, *args: _P.args, **kwargs: _P.kwargs 86 | ) -> _R | None: 87 | """Catch UpnpError errors and check availability before and after request.""" 88 | if not self.available: 89 | _LOGGER.warning( 90 | "Device disappeared when trying to call service %s", func.__name__ 91 | ) 92 | return None 93 | try: 94 | return await func(self, *args, **kwargs) 95 | #except UpnpError as err: 96 | except: 97 | self.check_available = True 98 | #_LOGGER.error("Error during call %s: %r", func.__name__, err) 99 | return None 100 | 101 | return wrapper 102 | 103 | 104 | async def async_setup_entry( 105 | hass: HomeAssistant, 106 | entry: config_entries.ConfigEntry, 107 | async_add_entities: AddEntitiesCallback, 108 | ) -> None: 109 | """Set up the DlnaDmrEntity from a config entry.""" 110 | _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) 111 | 112 | # Create our own device-wrapping entity 113 | entity = DlnaDmrEntity( 114 | #udn=entry.data[CONF_DEVICE_ID], 115 | device_type=entry.data[CONF_TYPE], 116 | name=entry.title, 117 | event_port=entry.options.get(CONF_LISTEN_PORT) or 0, 118 | event_callback_url=entry.options.get(CONF_CALLBACK_URL_OVERRIDE), 119 | poll_availability=entry.options.get(CONF_POLL_AVAILABILITY, False), 120 | location=entry.data[CONF_URL], 121 | browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False), 122 | ) 123 | 124 | async_add_entities([entity]) 125 | 126 | 127 | class DlnaDmrEntity(MediaPlayerEntity): 128 | """Representation of a DLNA DMR device as a HA entity.""" 129 | 130 | #udn: str 131 | device_type: str 132 | 133 | _event_addr: EventListenAddr 134 | poll_availability: bool 135 | # Last known URL for the device, used when adding this entity to hass to try 136 | # to connect before SSDP has rediscovered it, or when SSDP discovery fails. 137 | location: str 138 | # Should the async_browse_media function *not* filter out incompatible media? 139 | browse_unfiltered: bool 140 | 141 | _device_lock: asyncio.Lock # Held when connecting or disconnecting the device 142 | _device: DmrDevice | None = None 143 | check_available: bool = False 144 | _ssdp_connect_failed: bool = False 145 | 146 | # Track BOOTID in SSDP advertisements for device changes 147 | _bootid: int | None = None 148 | 149 | # DMR devices need polling for track position information. async_update will 150 | # determine whether further device polling is required. 151 | _attr_should_poll = True 152 | 153 | def __init__( 154 | self, 155 | #udn: str, 156 | device_type: str, 157 | name: str, 158 | event_port: int, 159 | event_callback_url: str | None, 160 | poll_availability: bool, 161 | location: str, 162 | browse_unfiltered: bool, 163 | ) -> None: 164 | """Initialize DLNA DMR entity.""" 165 | #self.udn = udn 166 | self.device_type = device_type 167 | self._attr_name = name 168 | self._event_addr = EventListenAddr(None, event_port, event_callback_url) 169 | self.poll_availability = poll_availability 170 | self.location = location 171 | self.browse_unfiltered = browse_unfiltered 172 | self._device_lock = asyncio.Lock() 173 | 174 | async def async_added_to_hass(self) -> None: 175 | """Handle addition.""" 176 | # Update this entity when the associated config entry is modified 177 | if self.registry_entry and self.registry_entry.config_entry_id: 178 | config_entry = self.hass.config_entries.async_get_entry( 179 | self.registry_entry.config_entry_id 180 | ) 181 | assert config_entry is not None 182 | self.async_on_remove( 183 | config_entry.add_update_listener(self.async_config_update_listener) 184 | ) 185 | 186 | # Try to connect to the last known location, but don't worry if not available 187 | if not self._device: 188 | try: 189 | await self._device_connect(self.location) 190 | except UpnpError as err: 191 | _LOGGER.debug("Couldn't connect immediately: %r", err) 192 | 193 | # Get SSDP notifications for only this device 194 | self.async_on_remove( 195 | await ssdp.async_register_callback( 196 | self.hass, self.async_ssdp_callback, {"USN": self.usn} 197 | ) 198 | ) 199 | 200 | # async_upnp_client.SsdpListener only reports byebye once for each *UDN* 201 | # (device name) which often is not the USN (service within the device) 202 | # that we're interested in. So also listen for byebye advertisements for 203 | # the UDN, which is reported in the _udn field of the combined_headers. 204 | self.async_on_remove( 205 | await ssdp.async_register_callback( 206 | self.hass, 207 | self.async_ssdp_callback, 208 | {"_udn": self.location, "NTS": NotificationSubType.SSDP_BYEBYE}, 209 | ) 210 | ) 211 | 212 | async def async_will_remove_from_hass(self) -> None: 213 | """Handle removal.""" 214 | await self._device_disconnect() 215 | 216 | async def async_ssdp_callback( 217 | self, info: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange 218 | ) -> None: 219 | """Handle notification from SSDP of device state change.""" 220 | _LOGGER.debug( 221 | "SSDP %s notification of device %s at %s", 222 | change, 223 | info.ssdp_usn, 224 | info.ssdp_location, 225 | ) 226 | 227 | try: 228 | bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_BOOTID] 229 | bootid: int | None = int(bootid_str, 10) 230 | except (KeyError, ValueError): 231 | bootid = None 232 | 233 | if change == ssdp.SsdpChange.UPDATE: 234 | # This is an announcement that bootid is about to change 235 | if self._bootid is not None and self._bootid == bootid: 236 | # Store the new value (because our old value matches) so that we 237 | # can ignore subsequent ssdp:alive messages 238 | with contextlib.suppress(KeyError, ValueError): 239 | next_bootid_str = info.ssdp_headers[ssdp.ATTR_SSDP_NEXTBOOTID] 240 | self._bootid = int(next_bootid_str, 10) 241 | # Nothing left to do until ssdp:alive comes through 242 | return 243 | 244 | if self._bootid is not None and self._bootid != bootid: 245 | # Device has rebooted 246 | # Maybe connection will succeed now 247 | self._ssdp_connect_failed = False 248 | if self._device: 249 | # Drop existing connection and maybe reconnect 250 | await self._device_disconnect() 251 | self._bootid = bootid 252 | 253 | if change == ssdp.SsdpChange.BYEBYE: 254 | # Device is going away 255 | if self._device: 256 | # Disconnect from gone device 257 | await self._device_disconnect() 258 | # Maybe the next alive message will result in a successful connection 259 | self._ssdp_connect_failed = False 260 | 261 | if ( 262 | change == ssdp.SsdpChange.ALIVE 263 | and not self._device 264 | and not self._ssdp_connect_failed 265 | ): 266 | assert info.ssdp_location 267 | location = info.ssdp_location 268 | try: 269 | await self._device_connect(location) 270 | except UpnpError as err: 271 | self._ssdp_connect_failed = True 272 | _LOGGER.warning( 273 | "Failed connecting to recently alive device at %s: %r", 274 | location, 275 | err, 276 | ) 277 | 278 | # Device could have been de/re-connected, state probably changed 279 | self.async_write_ha_state() 280 | 281 | async def async_config_update_listener( 282 | self, hass: HomeAssistant, entry: config_entries.ConfigEntry 283 | ) -> None: 284 | """Handle options update by modifying self in-place.""" 285 | _LOGGER.debug( 286 | "Updating: %s with data=%s and options=%s", 287 | self.name, 288 | entry.data, 289 | entry.options, 290 | ) 291 | self.location = entry.data[CONF_URL] 292 | self.poll_availability = entry.options.get(CONF_POLL_AVAILABILITY, False) 293 | self.browse_unfiltered = entry.options.get(CONF_BROWSE_UNFILTERED, False) 294 | 295 | new_port = entry.options.get(CONF_LISTEN_PORT) or 0 296 | new_callback_url = entry.options.get(CONF_CALLBACK_URL_OVERRIDE) 297 | 298 | if ( 299 | new_port == self._event_addr.port 300 | and new_callback_url == self._event_addr.callback_url 301 | ): 302 | return 303 | 304 | # Changes to eventing requires a device reconnect for it to update correctly 305 | await self._device_disconnect() 306 | # Update _event_addr after disconnecting, to stop the right event listener 307 | self._event_addr = self._event_addr._replace( 308 | port=new_port, callback_url=new_callback_url 309 | ) 310 | try: 311 | await self._device_connect(self.location) 312 | except UpnpError as err: 313 | _LOGGER.warning("Couldn't (re)connect after config change: %r", err) 314 | 315 | # Device was de/re-connected, state might have changed 316 | self.async_write_ha_state() 317 | 318 | async def _device_connect(self, location: str) -> None: 319 | """Connect to the device now that it's available.""" 320 | _LOGGER.debug("Connecting to device at %s", location) 321 | 322 | async with self._device_lock: 323 | if self._device: 324 | _LOGGER.debug("Trying to connect when device already connected") 325 | return 326 | 327 | domain_data = get_domain_data(self.hass) 328 | 329 | # Connect to the base UPNP device 330 | upnp_device = await domain_data.upnp_factory.async_create_device(location) 331 | 332 | # Create/get event handler that is reachable by the device, using 333 | # the connection's local IP to listen only on the relevant interface 334 | _, event_ip = await async_get_local_ip(location, self.hass.loop) 335 | self._event_addr = self._event_addr._replace(host=event_ip) 336 | event_handler = await domain_data.async_get_event_notifier( 337 | self._event_addr, self.hass 338 | ) 339 | _LOGGER.debug("Device self._event_addr: %r", self._event_addr) 340 | 341 | # Create profile wrapper 342 | self._device = DmrDevice(upnp_device, event_handler) 343 | 344 | self.location = location 345 | 346 | # Subscribe to event notifications 347 | if self._device.manufacturer != "Amlogic Corporation": #小讯R1音箱app常开dlna不支持 348 | try: 349 | self._device.on_event = self._on_event 350 | await self._device.async_subscribe_services(auto_resubscribe=True) 351 | except UpnpResponseError as err: 352 | # Device rejected subscription request. This is OK, variables 353 | # will be polled instead. 354 | _LOGGER.debug("Device rejected subscription: %r", err) 355 | except UpnpError as err: 356 | # Don't leave the device half-constructed 357 | self._device.on_event = None 358 | self._device = None 359 | await domain_data.async_release_event_notifier(self._event_addr) 360 | _LOGGER.debug("Error while subscribing during device connect: %r", err) 361 | raise 362 | 363 | if ( 364 | not self.registry_entry 365 | or not self.registry_entry.config_entry_id 366 | or self.registry_entry.device_id 367 | ): 368 | return 369 | 370 | # Create linked HA DeviceEntry now the information is known. 371 | dev_reg = device_registry.async_get(self.hass) 372 | device_entry = dev_reg.async_get_or_create( 373 | config_entry_id=self.registry_entry.config_entry_id, 374 | # Connections are based on the root device's UDN, and the DMR 375 | # embedded device's UDN. They may be the same, if the DMR is the 376 | # root device. 377 | connections={ 378 | ( 379 | device_registry.CONNECTION_UPNP, 380 | self.location.replace(":","_").replace("/","_"), 381 | ), 382 | (device_registry.CONNECTION_UPNP, self.location.replace(":","_").replace("/","_")), 383 | }, 384 | #identifiers={(DOMAIN, self.unique_id)}, # 2023.8 只有且必须有connections或identifiers中的一项 385 | default_manufacturer=self._device.manufacturer, 386 | default_model=self._device.model_name, 387 | default_name=self._device.name.replace("\n",""), 388 | ) 389 | 390 | # Update entity registry to link to the device 391 | ent_reg = entity_registry.async_get(self.hass) 392 | ent_reg.async_get_or_create( 393 | self.registry_entry.domain, 394 | self.registry_entry.platform, 395 | self.unique_id, 396 | device_id=device_entry.id, 397 | ) 398 | 399 | async def _device_disconnect(self) -> None: 400 | """Destroy connections to the device now that it's not available. 401 | 402 | Also call when removing this entity from hass to clean up connections. 403 | """ 404 | if self._device.manufacturer != "Amlogic Corporation": #小讯R1音箱app常开dlna不支持 405 | async with self._device_lock: 406 | if not self._device: 407 | _LOGGER.debug("Disconnecting from device that's not connected") 408 | return 409 | 410 | _LOGGER.debug("Disconnecting from %s", self._device.name) 411 | 412 | self._device.on_event = None 413 | old_device = self._device 414 | self._device = None 415 | await old_device.async_unsubscribe_services() 416 | 417 | domain_data = get_domain_data(self.hass) 418 | await domain_data.async_release_event_notifier(self._event_addr) 419 | 420 | async def async_update(self) -> None: 421 | """Retrieve the latest data.""" 422 | if not self._device: 423 | if self.poll_availability: #这里改为默认轮询设备,小度一定要轮询,否则实体会变成不可用。 424 | return 425 | try: 426 | await self._device_connect(self.location) 427 | except UpnpError: 428 | return 429 | 430 | assert self._device is not None 431 | 432 | 433 | 434 | try: 435 | #if self._device.manufacturer == "Amlogic Corporation": #小讯R1音箱app常开dlna不支持 不注释会报错,但可以使用。 436 | # return 437 | do_ping = self.poll_availability or self.check_available 438 | await self._device.async_update(do_ping=do_ping) 439 | #except UpnpError as err: 440 | except: 441 | #_LOGGER.debug("Device unavailable: %r", err) 442 | await self._device_disconnect() 443 | return 444 | finally: 445 | self.check_available = False 446 | 447 | def _on_event( 448 | self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] 449 | ) -> None: 450 | """State variable(s) changed, let home-assistant know.""" 451 | if not state_variables: 452 | # Indicates a failure to resubscribe, check if device is still available 453 | self.check_available = True 454 | 455 | force_refresh = False 456 | 457 | if service.service_id == "urn:upnp-org:serviceId:AVTransport": 458 | for state_variable in state_variables: 459 | # Force a state refresh when player begins or pauses playback 460 | # to update the position info. 461 | if ( 462 | state_variable.name == "TransportState" 463 | and state_variable.value 464 | in (TransportState.PLAYING, TransportState.PAUSED_PLAYBACK) 465 | ): 466 | force_refresh = True 467 | 468 | self.async_schedule_update_ha_state(force_refresh) 469 | 470 | @property 471 | def available(self) -> bool: 472 | """Device is available when we have a connection to it.""" 473 | return self._device is not None and self._device.profile_device.available 474 | 475 | @property 476 | def unique_id(self) -> str: 477 | """Report the UDN (Unique Device Name) as this entity's unique ID.""" 478 | return self.location.replace(":","_").replace("/","_") 479 | 480 | @property 481 | def usn(self) -> str: 482 | """Get the USN based on the UDN (Unique Device Name) and device type.""" 483 | return f"{self.location}::{self.device_type}" 484 | 485 | @property 486 | def state(self) -> str | None: 487 | """State of the player.""" 488 | if not self._device or not self.available: 489 | return STATE_OFF 490 | if self._device.transport_state is None: 491 | return STATE_ON 492 | if self._device.transport_state in ( 493 | TransportState.PLAYING, 494 | TransportState.TRANSITIONING, 495 | ): 496 | # Fix xiaodu can not stop automatically 但因小度不支持停止操作,并不能将状态变更为待机,,放弃修改。 497 | # _LOGGER.debug("manufacturer: %s, media_duration:%s, media_position:%s", self._device.manufacturer, self.media_duration, self.media_position) 498 | # if self._device.manufacturer == "DuerOS" and self.media_duration != None and self.media_duration == self.media_position: 499 | # self.async_media_stop() 500 | return STATE_PLAYING 501 | if self._device.transport_state in ( 502 | TransportState.PAUSED_PLAYBACK, 503 | TransportState.PAUSED_RECORDING, 504 | ): 505 | return STATE_PAUSED 506 | 507 | 508 | 509 | if self._device.transport_state == TransportState.VENDOR_DEFINED: 510 | # Unable to map this state to anything reasonable, so it's "Unknown" 511 | if self._device.manufacturer == "Amlogic Corporation": #小讯R1音箱app常开dlna设置在无状态时为idle 512 | return STATE_IDLE 513 | return None 514 | 515 | return STATE_IDLE 516 | 517 | @property 518 | def supported_features(self) -> int: 519 | """Flag media player features that are supported at this moment. 520 | 521 | Supported features may change as the device enters different states. 522 | """ 523 | if not self._device: 524 | return MediaPlayerEntityFeature(0) 525 | 526 | supported_features = MediaPlayerEntityFeature(0) 527 | 528 | if self._device.has_volume_level: 529 | supported_features |= MediaPlayerEntityFeature.VOLUME_SET 530 | supported_features |= MediaPlayerEntityFeature.VOLUME_STEP 531 | if self._device.has_volume_mute: 532 | supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE 533 | if self._device.can_play: 534 | supported_features |= MediaPlayerEntityFeature.PLAY 535 | if self._device.can_pause: 536 | supported_features |= MediaPlayerEntityFeature.PAUSE 537 | if self._device.can_stop: 538 | supported_features |= MediaPlayerEntityFeature.STOP 539 | if self._device.can_previous: 540 | supported_features |= MediaPlayerEntityFeature.PREVIOUS_TRACK 541 | if self._device.can_next: 542 | supported_features |= MediaPlayerEntityFeature.NEXT_TRACK 543 | if self._device.has_play_media: 544 | supported_features |= ( 545 | MediaPlayerEntityFeature.PLAY_MEDIA 546 | | MediaPlayerEntityFeature.BROWSE_MEDIA 547 | ) 548 | if self._device.can_seek_rel_time: 549 | supported_features |= MediaPlayerEntityFeature.SEEK 550 | 551 | play_modes = self._device.valid_play_modes 552 | if play_modes & {PlayMode.RANDOM, PlayMode.SHUFFLE}: 553 | supported_features |= MediaPlayerEntityFeature.SHUFFLE_SET 554 | if play_modes & {PlayMode.REPEAT_ONE, PlayMode.REPEAT_ALL}: 555 | supported_features |= MediaPlayerEntityFeature.REPEAT_SET 556 | 557 | if self._device.has_presets: 558 | supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE 559 | 560 | return supported_features 561 | 562 | @property 563 | def volume_level(self) -> float | None: 564 | """Volume level of the media player (0..1).""" 565 | if not self._device or not self._device.has_volume_level: 566 | return None 567 | return self._device.volume_level 568 | 569 | @catch_request_errors 570 | async def async_set_volume_level(self, volume: float) -> None: 571 | """Set volume level, range 0..1.""" 572 | assert self._device is not None 573 | await self._device.async_set_volume_level(volume) 574 | 575 | @property 576 | def is_volume_muted(self) -> bool | None: 577 | """Boolean if volume is currently muted.""" 578 | if not self._device: 579 | return None 580 | return self._device.is_volume_muted 581 | 582 | @catch_request_errors 583 | async def async_mute_volume(self, mute: bool) -> None: 584 | """Mute the volume.""" 585 | assert self._device is not None 586 | desired_mute = bool(mute) 587 | await self._device.async_mute_volume(desired_mute) 588 | 589 | @catch_request_errors 590 | async def async_media_pause(self) -> None: 591 | """Send pause command.""" 592 | assert self._device is not None 593 | await self._device.async_pause() 594 | 595 | @catch_request_errors 596 | async def async_media_play(self) -> None: 597 | """Send play command.""" 598 | assert self._device is not None 599 | await self._device.async_play() 600 | 601 | @catch_request_errors 602 | async def async_media_stop(self) -> None: 603 | """Send stop command.""" 604 | assert self._device is not None 605 | await self._device.async_stop() 606 | 607 | @catch_request_errors 608 | async def async_media_seek(self, position: int | float) -> None: 609 | """Send seek command.""" 610 | assert self._device is not None 611 | time = timedelta(seconds=position) 612 | await self._device.async_seek_rel_time(time) 613 | 614 | @catch_request_errors 615 | async def async_play_media( 616 | self, media_type: str, media_id: str, **kwargs: Any 617 | ) -> None: 618 | """Play a piece of media.""" 619 | _LOGGER.debug("Playing media: %s, %s, %s", media_type, media_id, kwargs) 620 | assert self._device is not None 621 | 622 | didl_metadata: str | None = None 623 | title: str = "" 624 | 625 | # If media is media_source, resolve it to url and MIME type, and maybe metadata 626 | if media_source.is_media_source_id(media_id): 627 | sourced_media = await media_source.async_resolve_media(self.hass, media_id) 628 | media_type = sourced_media.mime_type 629 | media_id = sourced_media.url 630 | _LOGGER.debug("sourced_media is %s", sourced_media) 631 | if sourced_metadata := getattr(sourced_media, "didl_metadata", None): 632 | didl_metadata = didl_lite.to_xml_string(sourced_metadata).decode( 633 | "utf-8" 634 | ) 635 | title = sourced_metadata.title 636 | 637 | # If media ID is a relative URL, we serve it from HA. 638 | media_id = async_process_play_media_url(self.hass, media_id) 639 | 640 | extra: dict[str, Any] = kwargs.get(ATTR_MEDIA_EXTRA) or {} 641 | metadata: dict[str, Any] = extra.get("metadata") or {} 642 | 643 | 644 | mediatitle = os.path.basename(media_id) 645 | self._mediatitle = unquote(mediatitle, encoding="UTF-8") 646 | 647 | if not title: 648 | title = extra.get("title") or metadata.get("title") or self._mediatitle or "Home Assistant" 649 | if thumb := extra.get("thumb"): 650 | metadata["album_art_uri"] = thumb 651 | 652 | # Translate metadata keys from HA names to DIDL-Lite names 653 | for hass_key, didl_key in MEDIA_METADATA_DIDL.items(): 654 | if hass_key in metadata: 655 | metadata[didl_key] = metadata.pop(hass_key) 656 | 657 | if not didl_metadata: 658 | # Create metadata specific to the given media type; different fields are 659 | # available depending on what the upnp_class is. 660 | upnp_class = MEDIA_UPNP_CLASS_MAP.get(media_type) 661 | didl_metadata = await self._device.construct_play_media_metadata( 662 | media_url=media_id, 663 | media_title=title, 664 | override_upnp_class=upnp_class, 665 | meta_data=metadata, 666 | ) 667 | 668 | # Stop current playing media 669 | if self._device.can_stop and self._device.manufacturer != "Amlogic Corporation": #小讯R1音箱app常开dlna使用stop命令报错 670 | await self.async_media_stop() 671 | 672 | # Queue media 673 | await self._device.async_set_transport_uri(media_id, title, didl_metadata) 674 | 675 | # If already playing, or don't want to autoplay, no need to call Play 676 | autoplay = extra.get("autoplay", True) 677 | if (self._device.manufacturer != "DuerOS" and self._device.transport_state == TransportState.PLAYING) or not autoplay: 678 | return 679 | 680 | # Play it 681 | await self._device.async_wait_for_can_play() 682 | await self.async_media_play() 683 | 684 | @catch_request_errors 685 | async def async_media_previous_track(self) -> None: 686 | """Send previous track command.""" 687 | assert self._device is not None 688 | await self._device.async_previous() 689 | 690 | @catch_request_errors 691 | async def async_media_next_track(self) -> None: 692 | """Send next track command.""" 693 | assert self._device is not None 694 | await self._device.async_next() 695 | 696 | @property 697 | def shuffle(self) -> bool | None: 698 | """Boolean if shuffle is enabled.""" 699 | if not self._device: 700 | return None 701 | 702 | if not (play_mode := self._device.play_mode): 703 | return None 704 | 705 | if play_mode == PlayMode.VENDOR_DEFINED: 706 | return None 707 | 708 | return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM) 709 | 710 | @catch_request_errors 711 | async def async_set_shuffle(self, shuffle: bool) -> None: 712 | """Enable/disable shuffle mode.""" 713 | assert self._device is not None 714 | 715 | repeat = self.repeat or REPEAT_MODE_OFF 716 | potential_play_modes = SHUFFLE_PLAY_MODES[(shuffle, repeat)] 717 | 718 | valid_play_modes = self._device.valid_play_modes 719 | 720 | for mode in potential_play_modes: 721 | if mode in valid_play_modes: 722 | await self._device.async_set_play_mode(mode) 723 | return 724 | 725 | _LOGGER.debug( 726 | "Couldn't find a suitable mode for shuffle=%s, repeat=%s", shuffle, repeat 727 | ) 728 | 729 | @property 730 | def repeat(self) -> str | None: 731 | """Return current repeat mode.""" 732 | if not self._device: 733 | return None 734 | 735 | if not (play_mode := self._device.play_mode): 736 | return None 737 | 738 | if play_mode == PlayMode.VENDOR_DEFINED: 739 | return None 740 | 741 | if play_mode == PlayMode.REPEAT_ONE: 742 | return REPEAT_MODE_ONE 743 | 744 | if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM): 745 | return REPEAT_MODE_ALL 746 | 747 | return REPEAT_MODE_OFF 748 | 749 | @catch_request_errors 750 | async def async_set_repeat(self, repeat: str) -> None: 751 | """Set repeat mode.""" 752 | assert self._device is not None 753 | 754 | shuffle = self.shuffle or False 755 | potential_play_modes = REPEAT_PLAY_MODES[(shuffle, repeat)] 756 | 757 | valid_play_modes = self._device.valid_play_modes 758 | 759 | for mode in potential_play_modes: 760 | if mode in valid_play_modes: 761 | await self._device.async_set_play_mode(mode) 762 | return 763 | 764 | _LOGGER.debug( 765 | "Couldn't find a suitable mode for shuffle=%s, repeat=%s", shuffle, repeat 766 | ) 767 | 768 | @property 769 | def sound_mode(self) -> str | None: 770 | """Name of the current sound mode, not supported by DLNA.""" 771 | return None 772 | 773 | @property 774 | def sound_mode_list(self) -> list[str] | None: 775 | """List of available sound modes.""" 776 | if not self._device: 777 | return None 778 | return self._device.preset_names 779 | 780 | @catch_request_errors 781 | async def async_select_sound_mode(self, sound_mode: str) -> None: 782 | """Select sound mode.""" 783 | assert self._device is not None 784 | await self._device.async_select_preset(sound_mode) 785 | 786 | async def async_browse_media( 787 | self, 788 | media_content_type: str | None = None, 789 | media_content_id: str | None = None, 790 | ) -> BrowseMedia: 791 | """Implement the websocket media browsing helper. 792 | 793 | Browses all available media_sources by default. Filters content_type 794 | based on the DMR's sink_protocol_info. 795 | """ 796 | _LOGGER.debug( 797 | "async_browse_media(%s, %s)", media_content_type, media_content_id 798 | ) 799 | 800 | # media_content_type is ignored; it's the content_type of the current 801 | # media_content_id, not the desired content_type of whomever is calling. 802 | 803 | if self.browse_unfiltered: 804 | content_filter = None 805 | else: 806 | content_filter = self._get_content_filter() 807 | 808 | return await media_source.async_browse_media( 809 | self.hass, media_content_id, content_filter=content_filter 810 | ) 811 | 812 | def _get_content_filter(self) -> Callable[[BrowseMedia], bool]: 813 | """Return a function that filters media based on what the renderer can play. 814 | 815 | The filtering is pretty loose; it's better to show something that can't 816 | be played than hide something that can. 817 | """ 818 | if not self._device or not self._device.sink_protocol_info: 819 | # Nothing is specified by the renderer, so show everything 820 | _LOGGER.debug("Get content filter with no device or sink protocol info") 821 | #return lambda _: True 822 | return lambda item: item.media_content_type.startswith("audio/") 823 | 824 | 825 | _LOGGER.debug("Get content filter for %s", self._device.sink_protocol_info) 826 | if self._device.sink_protocol_info[0] == "*": 827 | # Renderer claims it can handle everything, so show everything 828 | return lambda _: True 829 | 830 | # Convert list of things like "http-get:*:audio/mpeg;codecs=mp3:*" 831 | # to just "audio/mpeg" 832 | content_types = set[str]() 833 | for protocol_info in self._device.sink_protocol_info: 834 | protocol, _, content_format, _ = protocol_info.split(":", 3) 835 | # Transform content_format for better generic matching 836 | content_format = content_format.lower().replace("/x-", "/", 1) 837 | content_format = content_format.partition(";")[0] 838 | 839 | if protocol in STREAMABLE_PROTOCOLS: 840 | content_types.add(content_format) 841 | 842 | def _content_filter(item: BrowseMedia) -> bool: 843 | """Filter media items by their media_content_type.""" 844 | content_type = item.media_content_type 845 | content_type = content_type.lower().replace("/x-", "/", 1).partition(";")[0] 846 | return content_type in content_types 847 | 848 | return _content_filter 849 | 850 | @property 851 | def media_title(self) -> str | None: 852 | """Title of current playing media.""" 853 | if not self._device: 854 | return None 855 | # Use the best available title 856 | return self._device.media_program_title or self._device.media_title 857 | 858 | @property 859 | def media_image_url(self) -> str | None: 860 | """Image url of current playing media.""" 861 | if not self._device or self._device.manufacturer == "Amlogic Corporation" or self._device.manufacturer == "DuerOS": 862 | return None 863 | return self._device.media_image_url 864 | 865 | @property 866 | def media_content_id(self) -> str | None: 867 | """Content ID of current playing media.""" 868 | if not self._device: 869 | return None 870 | return self._device.current_track_uri 871 | 872 | @property 873 | def media_content_type(self) -> str | None: 874 | """Content type of current playing media.""" 875 | if not self._device or not self._device.media_class: 876 | return None 877 | return MEDIA_TYPE_MAP.get(self._device.media_class) 878 | 879 | @property 880 | def media_duration(self) -> int | None: 881 | """Duration of current playing media in seconds.""" 882 | if not self._device: 883 | return None 884 | return self._device.media_duration 885 | 886 | @property 887 | def media_position(self) -> int | None: 888 | """Position of current playing media in seconds.""" 889 | if not self._device: 890 | return None 891 | return self._device.media_position 892 | 893 | @property 894 | def media_position_updated_at(self) -> datetime | None: 895 | """When was the position of the current playing media valid. 896 | 897 | Returns value from homeassistant.util.dt.utcnow(). 898 | """ 899 | if not self._device: 900 | return None 901 | return self._device.media_position_updated_at 902 | 903 | @property 904 | def media_artist(self) -> str | None: 905 | """Artist of current playing media, music track only.""" 906 | if not self._device: 907 | return None 908 | return self._device.media_artist 909 | 910 | @property 911 | def media_album_name(self) -> str | None: 912 | """Album name of current playing media, music track only.""" 913 | if not self._device: 914 | return None 915 | return self._device.media_album_name 916 | 917 | @property 918 | def media_album_artist(self) -> str | None: 919 | """Album artist of current playing media, music track only.""" 920 | if not self._device: 921 | return None 922 | return self._device.media_album_artist 923 | 924 | @property 925 | def media_track(self) -> int | None: 926 | """Track number of current playing media, music track only.""" 927 | if not self._device: 928 | return None 929 | return self._device.media_track_number 930 | 931 | @property 932 | def media_series_title(self) -> str | None: 933 | """Title of series of current playing media, TV show only.""" 934 | if not self._device: 935 | return None 936 | return self._device.media_series_title 937 | 938 | @property 939 | def media_season(self) -> str | None: 940 | """Season number, starting at 1, of current playing media, TV show only.""" 941 | if not self._device: 942 | return None 943 | # Some DMRs, like Kodi, leave this as 0 and encode the season & episode 944 | # in the episode_number metadata, as {season:d}{episode:02d} 945 | if ( 946 | not self._device.media_season_number 947 | or self._device.media_season_number == "0" 948 | ) and self._device.media_episode_number: 949 | with contextlib.suppress(ValueError): 950 | episode = int(self._device.media_episode_number, 10) 951 | if episode > 100: 952 | return str(episode // 100) 953 | return self._device.media_season_number 954 | 955 | @property 956 | def media_episode(self) -> str | None: 957 | """Episode number of current playing media, TV show only.""" 958 | if not self._device: 959 | return None 960 | # Complement to media_season math above 961 | if ( 962 | not self._device.media_season_number 963 | or self._device.media_season_number == "0" 964 | ) and self._device.media_episode_number: 965 | with contextlib.suppress(ValueError): 966 | episode = int(self._device.media_episode_number, 10) 967 | if episode > 100: 968 | return str(episode % 100) 969 | return self._device.media_episode_number 970 | 971 | @property 972 | def media_channel(self) -> str | None: 973 | """Channel name currently playing.""" 974 | if not self._device: 975 | return None 976 | return self._device.media_channel_name 977 | 978 | @property 979 | def media_playlist(self) -> str | None: 980 | """Title of Playlist currently playing.""" 981 | if not self._device: 982 | return None 983 | return self._device.media_playlist_title 984 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "flow_title": "{name}", 4 | "step": { 5 | "user": { 6 | "title": "Discovered DLNA DMR devices", 7 | "description": "Choose a device to configure or leave blank to enter a URL", 8 | "data": { 9 | "host": "[%key:common::config_flow::data::host%]" 10 | } 11 | }, 12 | "manual": { 13 | "title": "Manual DLNA DMR device connection", 14 | "description": "URL to a device description XML file", 15 | "data": { 16 | "url": "[%key:common::config_flow::data::url%]" 17 | } 18 | }, 19 | "import_turn_on": { 20 | "description": "Please turn on the device and click submit to continue migration" 21 | }, 22 | "confirm": { 23 | "description": "[%key:common::config_flow::description::confirm_setup%]" 24 | } 25 | }, 26 | "abort": { 27 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", 28 | "alternative_integration": "Device is better supported by another integration", 29 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 30 | "discovery_error": "Failed to discover a matching DLNA device", 31 | "incomplete_config": "Configuration is missing a required variable", 32 | "non_unique_id": "Multiple devices found with the same unique ID", 33 | "not_dmr": "Device is not a supported Digital Media Renderer" 34 | }, 35 | "error": { 36 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 37 | "not_dmr": "Device is not a supported Digital Media Renderer" 38 | } 39 | }, 40 | "options": { 41 | "step": { 42 | "init": { 43 | "title": "DLNA Digital Media Renderer configuration", 44 | "data": { 45 | "listen_port": "Event listener port (random if not set)", 46 | "callback_url_override": "Event listener callback URL", 47 | "poll_availability": "Poll for device availability", 48 | "browse_unfiltered": "Show incompatible media when browsing" 49 | } 50 | } 51 | }, 52 | "error": { 53 | "invalid_url": "Invalid URL" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/bg.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", 5 | "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", 6 | "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043b\u0438\u043f\u0441\u0432\u0430 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0430", 7 | "non_unique_id": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u0443\u043d\u0438\u043a\u0430\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440" 8 | }, 9 | "error": { 10 | "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" 11 | }, 12 | "flow_title": "{name}", 13 | "step": { 14 | "confirm": { 15 | "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" 16 | }, 17 | "manual": { 18 | "data": { 19 | "url": "URL" 20 | } 21 | }, 22 | "user": { 23 | "data": { 24 | "host": "\u0425\u043e\u0441\u0442" 25 | }, 26 | "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u0438 DLNA DMR \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" 27 | } 28 | } 29 | }, 30 | "options": { 31 | "error": { 32 | "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "El dispositiu ja est\u00e0 configurat", 5 | "alternative_integration": "El dispositiu t\u00e9 millor compatibilitat amb una altra integraci\u00f3", 6 | "cannot_connect": "Ha fallat la connexi\u00f3", 7 | "discovery_error": "No s'ha pogut descobrir cap dispositiu DLNA coincident", 8 | "incomplete_config": "Falta una variable obligat\u00f2ria a la configuraci\u00f3", 9 | "non_unique_id": "S'han trobat diversos dispositius amb el mateix identificador \u00fanic", 10 | "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals compatible" 11 | }, 12 | "error": { 13 | "cannot_connect": "Ha fallat la connexi\u00f3", 14 | "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals compatible" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Vols comen\u00e7ar la configuraci\u00f3?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Engega el dispositiu i fes clic a Envia per continuar la migraci\u00f3" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL al fitxer XML de descripci\u00f3 del dispositiu", 29 | "title": "Connexi\u00f3 manual de dispositiu DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Amfitri\u00f3" 34 | }, 35 | "description": "Tria un dispositiu a configurar o deixeu-ho en blanc per introduir un URL", 36 | "title": "Dispositius descoberts DLNA DMR" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL inv\u00e0lid" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Mostra fitxers multim\u00e8dia incompatibles mentre navegui", 48 | "callback_url_override": "URL de crida de l'oient d'esdeveniments", 49 | "listen_port": "Port de l'oient d'esdeveniments (aleatori si no es defineix)", 50 | "poll_availability": "Sondeja per saber la disponibilitat del dispositiu" 51 | }, 52 | "title": "Configuraci\u00f3 del renderitzador de mitjans digitals DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" 5 | }, 6 | "flow_title": "{name}", 7 | "step": { 8 | "confirm": { 9 | "description": "Chcete za\u010d\u00edt nastavovat?" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Ger\u00e4t ist bereits konfiguriert", 5 | "alternative_integration": "Das Ger\u00e4t wird besser durch eine andere Integration unterst\u00fctzt", 6 | "cannot_connect": "Verbindung fehlgeschlagen", 7 | "discovery_error": "Ein passendes DLNA-Ger\u00e4t konnte nicht gefunden werden", 8 | "incomplete_config": "In der Konfiguration fehlt eine erforderliche Variable", 9 | "non_unique_id": "Mehrere Ger\u00e4te mit derselben eindeutigen ID gefunden", 10 | "not_dmr": "Ger\u00e4t ist kein unterst\u00fctzter Digital Media Renderer" 11 | }, 12 | "error": { 13 | "cannot_connect": "Verbindung fehlgeschlagen", 14 | "not_dmr": "Ger\u00e4t ist kein unterst\u00fctzter Digital Media Renderer" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Bitte schalte das Ger\u00e4t ein und klicke auf Senden, um die Migration fortzusetzen" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL zu einer XML-Datei mit Ger\u00e4tebeschreibung", 29 | "title": "Manuelle DLNA DMR-Ger\u00e4teverbindung" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "W\u00e4hle ein zu konfigurierendes Ger\u00e4t oder lasse es leer, um eine URL einzugeben.", 36 | "title": "Erkannte DLNA-DMR-Ger\u00e4te" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Ung\u00fcltige URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Inkompatible Medien beim Durchsuchen anzeigen", 48 | "callback_url_override": "R\u00fcckruf-URL des Ereignis-Listeners", 49 | "listen_port": "Port des Ereignis-Listeners (zuf\u00e4llig, wenn nicht festgelegt)", 50 | "poll_availability": "Abfrage der Ger\u00e4teverf\u00fcgbarkeit" 51 | }, 52 | "title": "DLNA Digital Media Renderer Konfiguration" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", 5 | "alternative_integration": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7", 6 | "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", 7 | "discovery_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03c3\u03c4\u03bf\u03b9\u03c7\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 DLNA", 8 | "incomplete_config": "\u0391\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae", 9 | "non_unique_id": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", 10 | "not_dmr": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c8\u03b7\u03c6\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", 14 | "not_dmr": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c8\u03b7\u03c6\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" 20 | }, 21 | "import_turn_on": { 22 | "description": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" 27 | }, 28 | "description": "URL \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf XML \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", 29 | "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" 34 | }, 35 | "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", 36 | "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 DLNA DMR" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03bc\u03b7 \u03c3\u03c5\u03bc\u03b2\u03b1\u03c4\u03ce\u03bd \u03bc\u03ad\u03c3\u03c9\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7", 48 | "callback_url_override": "URL \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2 \u03b1\u03ba\u03c1\u03bf\u03b1\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2", 49 | "listen_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd (\u03c4\u03c5\u03c7\u03b1\u03af\u03b1 \u03b1\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af)", 50 | "poll_availability": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" 51 | }, 52 | "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 DLNA Digital Media Renderer" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured", 5 | "alternative_integration": "Device is better supported by another integration", 6 | "cannot_connect": "Failed to connect", 7 | "discovery_error": "Failed to discover a matching DLNA device", 8 | "incomplete_config": "Configuration is missing a required variable", 9 | "non_unique_id": "Multiple devices found with the same unique ID", 10 | "not_dmr": "Device is not a supported Digital Media Renderer" 11 | }, 12 | "error": { 13 | "cannot_connect": "Failed to connect", 14 | "not_dmr": "Device is not a supported Digital Media Renderer" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Do you want to start set up?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Please turn on the device and click submit to continue migration" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL to a device description XML file", 29 | "title": "Manual DLNA DMR device connection" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Choose a device to configure or leave blank to enter a URL", 36 | "title": "Discovered DLNA DMR devices" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Invalid URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Show incompatible media when browsing", 48 | "callback_url_override": "Event listener callback URL", 49 | "listen_port": "Event listener port (random if not set)", 50 | "poll_availability": "Turn off polling for device availability (Do not select this option if the small speakers, etc. are not always turned on)" 51 | }, 52 | "title": "DLNA Digital Media Renderer configuration" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/es-419.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "alternative_integration": "El dispositivo es mejor compatible con otra integraci\u00f3n", 5 | "discovery_error": "Error al descubrir un dispositivo DLNA coincidente", 6 | "incomplete_config": "A la configuraci\u00f3n le falta una variable requerida", 7 | "non_unique_id": "Varios dispositivos encontrados con la misma ID \u00fanica", 8 | "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" 9 | }, 10 | "error": { 11 | "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" 12 | }, 13 | "flow_title": "{name}", 14 | "step": { 15 | "import_turn_on": { 16 | "description": "Encienda el dispositivo y haga clic en Enviar para continuar con la migraci\u00f3n." 17 | }, 18 | "manual": { 19 | "description": "URL a un archivo XML de descripci\u00f3n de dispositivo" 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "El dispositivo ya est\u00e1 configurado", 5 | "alternative_integration": "Dispositivo compatible con otra integraci\u00f3n", 6 | "cannot_connect": "No se pudo conectar", 7 | "discovery_error": "No se ha podido descubrir un dispositivo DLNA coincidente", 8 | "incomplete_config": "A la configuraci\u00f3n le falta una variable necesaria", 9 | "non_unique_id": "Se han encontrado varios dispositivos con el mismo ID \u00fanico", 10 | "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" 11 | }, 12 | "error": { 13 | "cannot_connect": "No se pudo conectar", 14 | "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Por favor, encienda el dispositivo y haga clic en enviar para continuar la migraci\u00f3n" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL de un archivo XML de descripci\u00f3n del dispositivo", 29 | "title": "Conexi\u00f3n manual del dispositivo DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Elija un dispositivo para configurar o d\u00e9jelo en blanco para introducir una URL", 36 | "title": "Dispositivos DLNA DMR descubiertos" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL no v\u00e1lida" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Muestra archivos multimedia incompatibles mientras navega", 48 | "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", 49 | "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", 50 | "poll_availability": "Sondeo para la disponibilidad del dispositivo" 51 | }, 52 | "title": "Configuraci\u00f3n de DLNA Digital Media Renderer" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/et.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Seade on juba h\u00e4\u00e4lestatud", 5 | "alternative_integration": "Seadet toetab paremini teine sidumine", 6 | "cannot_connect": "\u00dchendamine nurjus", 7 | "discovery_error": "Sobiva DLNA -seadme leidmine nurjus", 8 | "incomplete_config": "Seadetes puudub n\u00f5utav muutuja", 9 | "non_unique_id": "Leiti mitu sama unikaalse ID-ga seadet", 10 | "not_dmr": "Seade ei ole toetatud digitaalne meediumiedastusseade" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u00dchendamine nurjus", 14 | "not_dmr": "Seade ei ole toetatud digitaalne meediumiedastusseade" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Kas alustada seadistamist?" 20 | }, 21 | "import_turn_on": { 22 | "description": "L\u00fclita seade sisse ja kl\u00f5psa migreerimise j\u00e4tkamiseks nuppu Edasta" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "Seadme kirjelduse XML-faili URL", 29 | "title": "DLNA DMR seadme k\u00e4sitsi \u00fchendamine" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Vali h\u00e4\u00e4lestatav seade v\u00f5i j\u00e4ta URL -i sisestamiseks t\u00fchjaks", 36 | "title": "Avastatud DLNA DMR-seadmed" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Sobimatu URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Kuva sirvimisel \u00fchildumatu meedia", 48 | "callback_url_override": "S\u00fcndmuse kuulaja URL", 49 | "listen_port": "S\u00fcndmuste kuulaja port (juhuslik kui pole m\u00e4\u00e4ratud)", 50 | "poll_availability": "K\u00fcsitle seadme saadavuse kohta" 51 | }, 52 | "title": "DLNA digitaalse meediumi renderdaja s\u00e4tted" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", 5 | "alternative_integration": "L'appareil est mieux pris en charge par une autre int\u00e9gration", 6 | "cannot_connect": "\u00c9chec de connexion", 7 | "discovery_error": "\u00c9chec de la d\u00e9couverte d'un appareil DLNA correspondant", 8 | "incomplete_config": "Il manque une variable requise dans la configuration", 9 | "non_unique_id": "Plusieurs appareils trouv\u00e9s avec le m\u00eame identifiant unique", 10 | "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u00c9chec de connexion", 14 | "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Voulez-vous commencer la configuration\u00a0?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Veuillez allumer l'appareil et cliquer sur soumettre pour continuer la migration" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL vers un fichier XML de description d'appareil", 29 | "title": "Connexion manuelle de l'appareil DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "H\u00f4te" 34 | }, 35 | "description": "Choisissez un appareil \u00e0 configurer ou laissez vide pour saisir une URL", 36 | "title": "Appareils DLNA DMR d\u00e9couverts" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL non valide" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Afficher les fichiers multim\u00e9dias incompatibles lors de la navigation", 48 | "callback_url_override": "URL de rappel de l'\u00e9couteur d'\u00e9v\u00e9nement", 49 | "listen_port": "Port d'\u00e9coute d'\u00e9v\u00e9nement (al\u00e9atoire s'il n'est pas d\u00e9fini)", 50 | "poll_availability": "Sondage pour la disponibilit\u00e9 de l'appareil" 51 | }, 52 | "title": "Configuration du moteur de rendu multim\u00e9dia num\u00e9rique DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/he.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", 5 | "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" 6 | }, 7 | "error": { 8 | "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" 9 | }, 10 | "flow_title": "{name}", 11 | "step": { 12 | "confirm": { 13 | "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" 14 | }, 15 | "manual": { 16 | "data": { 17 | "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" 18 | } 19 | }, 20 | "user": { 21 | "data": { 22 | "host": "\u05de\u05d0\u05e8\u05d7" 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Az eszk\u00f6z m\u00e1r be van konfigur\u00e1lva", 5 | "alternative_integration": "Az eszk\u00f6zt jobban t\u00e1mogatja egy m\u00e1sik integr\u00e1ci\u00f3", 6 | "cannot_connect": "Sikertelen csatlakoz\u00e1s", 7 | "discovery_error": "Nem siker\u00fclt megfelel\u0151 DLNA-eszk\u00f6zt tal\u00e1lni", 8 | "incomplete_config": "A konfigur\u00e1ci\u00f3b\u00f3l hi\u00e1nyzik egy sz\u00fcks\u00e9ges \u00e9rt\u00e9k", 9 | "non_unique_id": "T\u00f6bb eszk\u00f6z tal\u00e1lhat\u00f3 ugyanazzal az egyedi azonos\u00edt\u00f3val", 10 | "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" 11 | }, 12 | "error": { 13 | "cannot_connect": "Sikertelen csatlakoz\u00e1s", 14 | "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Kezd\u0151dhet a be\u00e1ll\u00edt\u00e1s?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Kapcsolja be az eszk\u00f6zt, majd folytassa a migr\u00e1ci\u00f3t" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL egy eszk\u00f6zle\u00edr\u00f3 XML f\u00e1jlhoz", 29 | "title": "DLNA DMR eszk\u00f6z manu\u00e1lis csatlakoztat\u00e1sa" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "C\u00edm" 34 | }, 35 | "description": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt, vagy hagyja \u00fcresen az URL-c\u00edm k\u00e9zi megad\u00e1s\u00e1hoz", 36 | "title": "DLNA digit\u00e1lis m\u00e9dia renderel\u0151" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "\u00c9rv\u00e9nytelen URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Inkompatibilis m\u00e9dia megjelen\u00edt\u00e9se b\u00f6ng\u00e9sz\u00e9s k\u00f6zben", 48 | "callback_url_override": "Esem\u00e9nyfigyel\u0151 visszah\u00edv\u00e1si URL (callback)", 49 | "listen_port": "Esem\u00e9nyfigyel\u0151 port (v\u00e9letlenszer\u0171, ha nincs be\u00e1ll\u00edtva)", 50 | "poll_availability": "Eszk\u00f6z el\u00e9r\u00e9s\u00e9nek tesztel\u00e9se lek\u00e9rdez\u00e9ssel" 51 | }, 52 | "title": "DLNA konfigur\u00e1ci\u00f3" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Perangkat sudah dikonfigurasi", 5 | "alternative_integration": "Perangkat dapat didukung lebih baik lewat integrasi lainnya", 6 | "cannot_connect": "Gagal terhubung", 7 | "discovery_error": "Gagal menemukan perangkat DLNA yang cocok", 8 | "incomplete_config": "Konfigurasi tidak memiliki variabel yang diperlukan", 9 | "non_unique_id": "Beberapa perangkat ditemukan dengan ID unik yang sama", 10 | "not_dmr": "Perangkat bukan Digital Media Renderer yang didukung" 11 | }, 12 | "error": { 13 | "cannot_connect": "Gagal terhubung", 14 | "not_dmr": "Perangkat bukan Digital Media Renderer yang didukung" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Ingin memulai penyiapan?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Nyalakan perangkat dan klik kirim untuk melanjutkan migrasi" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL ke file XML deskripsi perangkat", 29 | "title": "Koneksi perangkat DLNA DMR manual" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Pilih perangkat untuk dikonfigurasi atau biarkan kosong untuk memasukkan URL", 36 | "title": "Perangkat DLNA DMR yang ditemukan" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL tidak valid" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Tampilkan media yang tidak kompatibel saat menjelajah", 48 | "callback_url_override": "URL panggilan balik pendengar peristiwa", 49 | "listen_port": "Port pendengar peristiwa (acak jika tidak diatur)", 50 | "poll_availability": "Polling untuk ketersediaan perangkat" 51 | }, 52 | "title": "Konfigurasi Digital Media Renderer DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/is.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "import_turn_on": { 5 | "description": "Kveiktu \u00e1 t\u00e6kinu og smelltu \u00e1 senda til a\u00f0 halda \u00e1fram flutningi" 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", 5 | "alternative_integration": "Il dispositivo \u00e8 meglio supportato da un'altra integrazione", 6 | "cannot_connect": "Impossibile connettersi", 7 | "discovery_error": "Impossibile individuare un dispositivo DLNA corrispondente", 8 | "incomplete_config": "Nella configurazione manca una variabile richiesta", 9 | "non_unique_id": "Pi\u00f9 dispositivi trovati con lo stesso ID univoco", 10 | "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer supportato" 11 | }, 12 | "error": { 13 | "cannot_connect": "Impossibile connettersi", 14 | "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer supportato" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Vuoi iniziare la configurazione?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Accendi il dispositivo e fai clic su Invia per continuare la migrazione" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL di un file XML di descrizione del dispositivo", 29 | "title": "Connessione manuale del dispositivo DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Scegli un dispositivo da configurare o lascia vuoto per inserire un URL", 36 | "title": "Rilevati dispositivi DLNA DMR" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL non valido" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Mostra file multimediali incompatibili durante la navigazione", 48 | "callback_url_override": "URL di richiamata dell'ascoltatore di eventi", 49 | "listen_port": "Porta dell'ascoltatore di eventi (casuale se non impostata)", 50 | "poll_availability": "Interrogazione per la disponibilit\u00e0 del dispositivo" 51 | }, 52 | "title": "Configurazione DLNA Digital Media Renderer" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", 5 | "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", 6 | "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", 7 | "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", 8 | "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", 9 | "non_unique_id": "\u540c\u4e00\u306eID\u3067\u8907\u6570\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", 10 | "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", 14 | "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" 20 | }, 21 | "import_turn_on": { 22 | "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u3092\u5165\u308c\u3001\u9001\u4fe1(submit)\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u79fb\u884c\u3092\u7d9a\u3051\u3066\u304f\u3060\u3055\u3044" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "\u30c7\u30d0\u30a4\u30b9\u8a18\u8ff0XML\u30d5\u30a1\u30a4\u30eb\u3078\u306eURL", 29 | "title": "\u624b\u52d5\u3067DLNA DMR\u6a5f\u5668\u306b\u63a5\u7d9a" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "\u30db\u30b9\u30c8" 34 | }, 35 | "description": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066URL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", 36 | "title": "\u767a\u898b\u3055\u308c\u305fDLNA DMR\u6a5f\u5668" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "\u7121\u52b9\u306aURL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "\u30d6\u30e9\u30a6\u30ba\u6642\u306b\u4e92\u63db\u6027\u306e\u306a\u3044\u30e1\u30c7\u30a3\u30a2\u3092\u8868\u793a\u3059\u308b", 48 | "callback_url_override": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u306e\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL", 49 | "listen_port": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u30dd\u30fc\u30c8(\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u30e9\u30f3\u30c0\u30e0)", 50 | "poll_availability": "\u30c7\u30d0\u30a4\u30b9\u306e\u53ef\u7528\u6027\u3092\u30dd\u30fc\u30ea\u30f3\u30b0" 51 | }, 52 | "title": "DLNA Digital Media Renderer\u306e\u8a2d\u5b9a" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "alternative_integration": "\uae30\uae30\uac00 \ub2e4\ub978 \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uc5d0\uc11c \ub354 \uc798 \uc9c0\uc6d0\ub429\ub2c8\ub2e4." 5 | }, 6 | "step": { 7 | "confirm": { 8 | "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" 9 | }, 10 | "manual": { 11 | "description": "\uc7a5\uce58 \uc124\uba85 XML \ud30c\uc77c\uc758 URL" 12 | }, 13 | "user": { 14 | "title": "DLNA DMR \uc7a5\uce58 \ubc1c\uacac" 15 | } 16 | } 17 | }, 18 | "options": { 19 | "step": { 20 | "init": { 21 | "data": { 22 | "browse_unfiltered": "\ud638\ud658\ub418\uc9c0 \uc54a\ub294 \ubbf8\ub514\uc5b4 \ud45c\uc2dc" 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Apparaat is al geconfigureerd", 5 | "alternative_integration": "Apparaat wordt beter ondersteund door een andere integratie", 6 | "cannot_connect": "Kan geen verbinding maken", 7 | "discovery_error": "Kan geen overeenkomend DLNA-apparaat vinden", 8 | "incomplete_config": "Configuratie mist een vereiste variabele", 9 | "non_unique_id": "Meerdere apparaten gevonden met hetzelfde unieke ID", 10 | "not_dmr": "Apparaat is een niet-ondersteund Digital Media Renderer" 11 | }, 12 | "error": { 13 | "cannot_connect": "Kan geen verbinding maken", 14 | "not_dmr": "Apparaat is een niet-ondersteund Digital Media Renderer" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Wil je beginnen met instellen?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Zet het apparaat aan en klik op verzenden om door te gaan met de migratie" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL naar een XML-bestand met apparaatbeschrijvingen", 29 | "title": "Handmatige DLNA DMR-apparaatverbinding" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Host" 34 | }, 35 | "description": "Kies een apparaat om te configureren of laat leeg om een URL in te voeren", 36 | "title": "Ontdekt DLNA Digital Media Renderer" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Ongeldige URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Incompatibele media weergeven tijdens browsen", 48 | "callback_url_override": "Event listener callback URL", 49 | "listen_port": "Poort om naar gebeurtenissen te luisteren (willekeurige poort indien niet ingesteld)", 50 | "poll_availability": "Pollen voor apparaat beschikbaarheid" 51 | }, 52 | "title": "DLNA Digital Media Renderer instellingen" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/no.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Enheten er allerede konfigurert", 5 | "alternative_integration": "Enheten st\u00f8ttes bedre av en annen integrasjon", 6 | "cannot_connect": "Tilkobling mislyktes", 7 | "discovery_error": "Kunne ikke finne en matchende DLNA -enhet", 8 | "incomplete_config": "Konfigurasjonen mangler en n\u00f8dvendig variabel", 9 | "non_unique_id": "Flere enheter ble funnet med samme unike ID", 10 | "not_dmr": "Enheten er ikke en st\u00f8ttet Digital Media Renderer" 11 | }, 12 | "error": { 13 | "cannot_connect": "Tilkobling mislyktes", 14 | "not_dmr": "Enheten er ikke en st\u00f8ttet Digital Media Renderer" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Vil du starte oppsettet?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Sl\u00e5 p\u00e5 enheten og klikk p\u00e5 send for \u00e5 fortsette overf\u00f8ringen" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL til en enhetsbeskrivelse XML -fil", 29 | "title": "Manuell DLNA DMR -enhetstilkobling" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Vert" 34 | }, 35 | "description": "Velg en enhet du vil konfigurere, eller la den st\u00e5 tom for \u00e5 angi en URL", 36 | "title": "Oppdaget DLNA DMR -enheter" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "ugyldig URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Vis inkompatible medier n\u00e5r du surfer", 48 | "callback_url_override": "URL for tilbakeringing av hendelseslytter", 49 | "listen_port": "Hendelseslytterport (tilfeldig hvis den ikke er angitt)", 50 | "poll_availability": "Avstemning for tilgjengelighet av enheter" 51 | }, 52 | "title": "DLNA Digital Media Renderer -konfigurasjon" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", 5 | "alternative_integration": "Urz\u0105dzenie jest lepiej obs\u0142ugiwane przez inn\u0105 integracj\u0119", 6 | "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", 7 | "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 pasuj\u0105cego urz\u0105dzenia DLNA", 8 | "incomplete_config": "W konfiguracji brakuje wymaganej zmiennej", 9 | "non_unique_id": "Znaleziono wiele urz\u0105dze\u0144 z tym samym unikalnym identyfikatorem", 10 | "not_dmr": "Urz\u0105dzenie nie jest obs\u0142ugiwanym rendererem multimedi\u00f3w cyfrowych (DMR)" 11 | }, 12 | "error": { 13 | "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", 14 | "not_dmr": "Urz\u0105dzenie nie jest obs\u0142ugiwanym rendererem multimedi\u00f3w cyfrowych (DMR)" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" 20 | }, 21 | "import_turn_on": { 22 | "description": "W\u0142\u0105cz urz\u0105dzenie i kliknij \"Zatwierd\u017a\", aby kontynuowa\u0107 migracj\u0119" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL do pliku XML z opisem urz\u0105dzenia", 29 | "title": "R\u0119czne pod\u0142\u0105czanie urz\u0105dzenia DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Nazwa hosta lub adres IP" 34 | }, 35 | "description": "Wybierz urz\u0105dzenie do skonfigurowania lub pozostaw puste, aby wprowadzi\u0107 adres URL", 36 | "title": "Wykryto urz\u0105dzenia DLNA DMR" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Nieprawid\u0142owy adres URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Poka\u017c niezgodne multimedia podczas przegl\u0105dania", 48 | "callback_url_override": "Adres Callback URL dla detektora zdarze\u0144", 49 | "listen_port": "Port detektora zdarze\u0144 (losowy, je\u015bli nie jest ustawiony)", 50 | "poll_availability": "Sondowanie na dost\u0119pno\u015b\u0107 urz\u0105dze\u0144" 51 | }, 52 | "title": "Konfiguracja rendera multimedi\u00f3w cyfrowych (DMR) dla DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", 5 | "alternative_integration": "O dispositivo \u00e9 melhor suportado por outra integra\u00e7\u00e3o", 6 | "cannot_connect": "Falha ao conectar", 7 | "discovery_error": "Falha ao descobrir um dispositivo DLNA correspondente", 8 | "incomplete_config": "A configura\u00e7\u00e3o n\u00e3o tem uma vari\u00e1vel obrigat\u00f3ria", 9 | "non_unique_id": "V\u00e1rios dispositivos encontrados com o mesmo ID exclusivo", 10 | "not_dmr": "O dispositivo n\u00e3o \u00e9 um renderizador de m\u00eddia digital compat\u00edvel" 11 | }, 12 | "error": { 13 | "cannot_connect": "Falha ao conectar", 14 | "not_dmr": "O dispositivo n\u00e3o \u00e9 um renderizador de m\u00eddia digital compat\u00edvel" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Deseja iniciar a configura\u00e7\u00e3o?" 20 | }, 21 | "import_turn_on": { 22 | "description": "Por favor, ligue o dispositivo e clique em enviar para continuar a migra\u00e7\u00e3o" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "URL para um arquivo XML de descri\u00e7\u00e3o do dispositivo", 29 | "title": "Conex\u00e3o manual do dispositivo DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Nome do host" 34 | }, 35 | "description": "Escolha um dispositivo para configurar ou deixe em branco para inserir um URL", 36 | "title": "Dispositivos DMR DLNA descobertos" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL inv\u00e1lida" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Mostrar m\u00eddia incompat\u00edvel ao navegar", 48 | "callback_url_override": "URL de retorno do ouvinte de eventos", 49 | "listen_port": "Porta do ouvinte de eventos (aleat\u00f3rio se n\u00e3o estiver definido)", 50 | "poll_availability": "Pesquisa de disponibilidade do dispositivo" 51 | }, 52 | "title": "Configura\u00e7\u00e3o do renderizador de m\u00eddia digital DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", 5 | "alternative_integration": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043b\u0443\u0447\u0448\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", 6 | "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", 7 | "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e DLNA.", 8 | "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f.", 9 | "non_unique_id": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u043c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u043c.", 10 | "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Digital Media Renderer." 11 | }, 12 | "error": { 13 | "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", 14 | "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Digital Media Renderer." 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" 20 | }, 21 | "import_turn_on": { 22 | "description": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL-\u0430\u0434\u0440\u0435\u0441" 27 | }, 28 | "description": "URL-\u0430\u0434\u0440\u0435\u0441 XML-\u0444\u0430\u0439\u043b\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", 29 | "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "\u0425\u043e\u0441\u0442" 34 | }, 35 | "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u0432\u0435\u0441\u0442\u0438 URL.", 36 | "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441." 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0435 \u043c\u0435\u0434\u0438\u0430", 48 | "callback_url_override": "Callback URL \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", 49 | "listen_port": "\u041f\u043e\u0440\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 (\u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d)", 50 | "poll_availability": "\u041e\u043f\u0440\u043e\u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" 51 | }, 52 | "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u0430 DLNA" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/sl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "alternative_integration": "Naprava je bolje podprta z drugo integracijo", 5 | "cannot_connect": "Povezava ni uspela" 6 | }, 7 | "error": { 8 | "cannot_connect": "Povezava ni uspela" 9 | }, 10 | "step": { 11 | "manual": { 12 | "data": { 13 | "url": "URL" 14 | } 15 | }, 16 | "user": { 17 | "data": { 18 | "host": "Gostitelj" 19 | } 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/sv.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": { 3 | "step": { 4 | "init": { 5 | "data": { 6 | "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar" 7 | } 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", 5 | "alternative_integration": "Cihaz ba\u015fka bir entegrasyon taraf\u0131ndan daha iyi destekleniyor", 6 | "cannot_connect": "Ba\u011flanma hatas\u0131", 7 | "discovery_error": "E\u015fle\u015fen bir DLNA cihaz\u0131 bulunamad\u0131", 8 | "incomplete_config": "Yap\u0131land\u0131rmada gerekli bir de\u011fi\u015fken eksik", 9 | "non_unique_id": "Ayn\u0131 benzersiz kimli\u011fe sahip birden fazla cihaz bulundu", 10 | "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" 11 | }, 12 | "error": { 13 | "cannot_connect": "Ba\u011flanma hatas\u0131", 14 | "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "Kuruluma ba\u015flamak ister misiniz?" 20 | }, 21 | "import_turn_on": { 22 | "description": "L\u00fctfen cihaz\u0131 a\u00e7\u0131n ve ta\u015f\u0131maya devam etmek i\u00e7in g\u00f6nder'i t\u0131klay\u0131n" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "URL" 27 | }, 28 | "description": "Ayg\u0131t a\u00e7\u0131klamas\u0131 XML dosyas\u0131n\u0131n URL'si", 29 | "title": "Manuel DLNA DMR ayg\u0131t ba\u011flant\u0131s\u0131" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "Sunucu" 34 | }, 35 | "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in veya bir URL girmek i\u00e7in bo\u015f b\u0131rak\u0131n", 36 | "title": "Ke\u015ffedilen DLNA DMR cihazlar\u0131" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "Ge\u00e7ersiz URL" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "Tarama s\u0131ras\u0131nda uyumsuz medyay\u0131 g\u00f6ster", 48 | "callback_url_override": "Olay dinleyici geri \u00e7a\u011f\u0131rma URL'si", 49 | "listen_port": "Olay dinleyici ba\u011flant\u0131 noktas\u0131 (ayarlanmam\u0131\u015fsa rastgele)", 50 | "poll_availability": "Cihaz kullan\u0131labilirli\u011fi i\u00e7in anket" 51 | }, 52 | "title": "DLNA Dijital Medya \u0130\u015fleyici yap\u0131land\u0131rmas\u0131" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", 5 | "alternative_integration": "\u8be5\u8bbe\u5907\u5728\u53e6\u4e00\u96c6\u6210\u80fd\u63d0\u4f9b\u66f4\u597d\u7684\u652f\u6301", 6 | "cannot_connect": "\u8fde\u63a5\u5931\u8d25", 7 | "discovery_error": "\u672a\u53d1\u73b0\u53ef\u7528\u7684 DLNA \u8bbe\u5907", 8 | "incomplete_config": "\u914d\u7f6e\u7f3a\u5c11\u5fc5\u8981\u7684\u53d8\u91cf\u4fe1\u606f", 9 | "non_unique_id": "\u53d1\u73b0\u591a\u53f0\u8bbe\u5907\u5177\u6709\u76f8\u540c\u7684 unique ID", 10 | "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\uff08DMR\uff09" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u8fde\u63a5\u5931\u8d25", 14 | "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\uff08DMR\uff09" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u60a8\u8981\u5f00\u59cb\u8bbe\u7f6e\u5417\uff1f" 20 | }, 21 | "import_turn_on": { 22 | "description": "\u8bf7\u6253\u5f00\u8bbe\u5907\uff0c\u7136\u540e\u70b9\u51fb\u201c\u63d0\u4ea4\u201d\u4ee5\u7ee7\u7eed\u8fc1\u79fb" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "\u7f51\u5740" 27 | }, 28 | "description": "\u8bbe\u5907\u63cf\u8ff0 XML \u6587\u4ef6\u7f51\u5740", 29 | "title": "\u624b\u52a8\u914d\u7f6e DLNA DMR \u8bbe\u5907\u8fde\u63a5" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "\u4e3b\u673a" 34 | }, 35 | "description": "选择要配置的设备或留空以输入 URL", 36 | "title": "\u53d1\u73b0 DLNA DMR \u8bbe\u5907" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "\u65e0\u6548\u7f51\u5740" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "浏览时显示不兼容的媒体", 48 | "callback_url_override": "\u4e8b\u4ef6\u76d1\u542c\u5668\u7684\u56de\u8c03 URL", 49 | "listen_port": "\u4e8b\u4ef6\u76d1\u542c\u5668\u7aef\u53e3\uff08\u5982\u4e0d\u6307\u5b9a\u5219\u968f\u673a\u7aef\u53e3\u53f7\uff09", 50 | "poll_availability": "关闭轮询设备可用性(小度音箱等不是一直开机的请不要选中此项)" 51 | }, 52 | "title": "DLNA \u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\u914d\u7f6e" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /custom_components/dlna_dmr_xiaodu/translations/zh-Hant.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", 5 | "alternative_integration": "\u4f7f\u7528\u5176\u4ed6\u6574\u5408\u4ee5\u53d6\u5f97\u66f4\u4f73\u7684\u88dd\u7f6e\u652f\u63f4", 6 | "cannot_connect": "\u9023\u7dda\u5931\u6557", 7 | "discovery_error": "DLNA \u88dd\u7f6e\u641c\u7d22\u5931\u6557", 8 | "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", 9 | "non_unique_id": "\u627e\u5230\u591a\u7d44\u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u552f\u4e00 ID", 10 | "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" 11 | }, 12 | "error": { 13 | "cannot_connect": "\u9023\u7dda\u5931\u6557", 14 | "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" 15 | }, 16 | "flow_title": "{name}", 17 | "step": { 18 | "confirm": { 19 | "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" 20 | }, 21 | "import_turn_on": { 22 | "description": "\u8acb\u958b\u555f\u88dd\u7f6e\u4e26\u9ede\u9078\u50b3\u9001\u4ee5\u7e7c\u7e8c\u9077\u79fb" 23 | }, 24 | "manual": { 25 | "data": { 26 | "url": "\u7db2\u5740" 27 | }, 28 | "description": "\u88dd\u7f6e\u8aaa\u660e XML \u6a94\u6848\u4e4b URL", 29 | "title": "\u624b\u52d5 DLNA DMR \u88dd\u7f6e\u9023\u7dda" 30 | }, 31 | "user": { 32 | "data": { 33 | "host": "\u4e3b\u6a5f\u7aef" 34 | }, 35 | "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", 36 | "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" 37 | } 38 | } 39 | }, 40 | "options": { 41 | "error": { 42 | "invalid_url": "URL \u7121\u6548" 43 | }, 44 | "step": { 45 | "init": { 46 | "data": { 47 | "browse_unfiltered": "\u7576\u700f\u89bd\u6642\u986f\u793a\u4e0d\u76f8\u5bb9\u5a92\u9ad4", 48 | "callback_url_override": "\u4e8b\u4ef6\u76e3\u807d\u56de\u547c URL", 49 | "listen_port": "\u4e8b\u4ef6\u76e3\u807d\u901a\u8a0a\u57e0\uff08\u672a\u8a2d\u7f6e\u5247\u70ba\u96a8\u6a5f\uff09", 50 | "poll_availability": "关闭轮询设备可用性(小度音箱等不是一直开机的请不要选中此项)" 51 | }, 52 | "title": "DLNA Digital Media Renderer \u8a2d\u5b9a" 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dlna_dmr_xiaodu", 3 | "render_readme": true 4 | } 5 | --------------------------------------------------------------------------------