mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Show device details in homekit accessory service info (#35100)
* Show device info in homekit accessory service info * fix conflict
This commit is contained in:
parent
6d9f561853
commit
0a9b373edb
@ -34,6 +34,7 @@ from homeassistant.helpers.entityfilter import (
|
||||
CONF_INCLUDE_ENTITIES,
|
||||
convert_filter,
|
||||
)
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.util import get_local_ip
|
||||
|
||||
from .accessories import get_accessory
|
||||
@ -41,6 +42,10 @@ from .aidmanager import AccessoryAidStorage
|
||||
from .const import (
|
||||
AID_STORAGE,
|
||||
ATTR_DISPLAY_NAME,
|
||||
ATTR_INTERGRATION,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
ATTR_VALUE,
|
||||
BRIDGE_NAME,
|
||||
CONF_ADVERTISE_IP,
|
||||
@ -200,7 +205,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
aid_storage = AccessoryAidStorage(hass, entry.entry_id)
|
||||
|
||||
await aid_storage.async_initialize()
|
||||
# These are yaml only
|
||||
# ip_address and advertise_ip are yaml only
|
||||
ip_address = conf.get(CONF_IP_ADDRESS)
|
||||
advertise_ip = conf.get(CONF_ADVERTISE_IP)
|
||||
|
||||
@ -494,6 +499,7 @@ class HomeKit:
|
||||
self.status = STATUS_WAIT
|
||||
|
||||
ent_reg = await entity_registry.async_get_registry(self.hass)
|
||||
dev_reg = await device_registry.async_get_registry(self.hass)
|
||||
|
||||
device_lookup = ent_reg.async_get_device_class_lookup(
|
||||
{
|
||||
@ -507,16 +513,24 @@ class HomeKit:
|
||||
if not self._filter(state.entity_id):
|
||||
continue
|
||||
|
||||
self._async_configure_linked_battery_sensors(ent_reg, device_lookup, state)
|
||||
ent_reg_ent = ent_reg.async_get(state.entity_id)
|
||||
if ent_reg_ent:
|
||||
await self._async_set_device_info_attributes(
|
||||
ent_reg_ent, dev_reg, state.entity_id
|
||||
)
|
||||
self._async_configure_linked_battery_sensors(
|
||||
ent_reg_ent, device_lookup, state
|
||||
)
|
||||
|
||||
bridged_states.append(state)
|
||||
|
||||
self._async_register_bridge(dev_reg)
|
||||
await self.hass.async_add_executor_job(self._start, bridged_states)
|
||||
await self._async_register_bridge()
|
||||
|
||||
async def _async_register_bridge(self):
|
||||
@callback
|
||||
def _async_register_bridge(self, dev_reg):
|
||||
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
|
||||
registry = await device_registry.async_get_registry(self.hass)
|
||||
registry.async_get_or_create(
|
||||
dev_reg.async_get_or_create(
|
||||
config_entry_id=self._entry_id,
|
||||
connections={
|
||||
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
|
||||
@ -567,21 +581,21 @@ class HomeKit:
|
||||
self.hass.add_job(self.driver.stop)
|
||||
|
||||
@callback
|
||||
def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state):
|
||||
entry = ent_reg.async_get(state.entity_id)
|
||||
|
||||
def _async_configure_linked_battery_sensors(
|
||||
self, ent_reg_ent, device_lookup, state
|
||||
):
|
||||
if (
|
||||
entry is None
|
||||
or entry.device_id is None
|
||||
or entry.device_id not in device_lookup
|
||||
or entry.device_class
|
||||
ent_reg_ent is None
|
||||
or ent_reg_ent.device_id is None
|
||||
or ent_reg_ent.device_id not in device_lookup
|
||||
or ent_reg_ent.device_class
|
||||
in (DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_BATTERY)
|
||||
):
|
||||
return
|
||||
|
||||
if ATTR_BATTERY_CHARGING not in state.attributes:
|
||||
battery_charging_binary_sensor_entity_id = device_lookup[
|
||||
entry.device_id
|
||||
ent_reg_ent.device_id
|
||||
].get(("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING))
|
||||
if battery_charging_binary_sensor_entity_id:
|
||||
self._config.setdefault(state.entity_id, {}).setdefault(
|
||||
@ -590,7 +604,7 @@ class HomeKit:
|
||||
)
|
||||
|
||||
if ATTR_BATTERY_LEVEL not in state.attributes:
|
||||
battery_sensor_entity_id = device_lookup[entry.device_id].get(
|
||||
battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get(
|
||||
("sensor", DEVICE_CLASS_BATTERY)
|
||||
)
|
||||
if battery_sensor_entity_id:
|
||||
@ -598,6 +612,21 @@ class HomeKit:
|
||||
CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id
|
||||
)
|
||||
|
||||
async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id):
|
||||
"""Set attributes that will be used for homekit device info."""
|
||||
ent_cfg = self._config.setdefault(entity_id, {})
|
||||
if ent_reg_ent.device_id:
|
||||
dev_reg_ent = dev_reg.async_get(ent_reg_ent.device_id)
|
||||
if dev_reg_ent.manufacturer:
|
||||
ent_cfg[ATTR_MANUFACTURER] = dev_reg_ent.manufacturer
|
||||
if dev_reg_ent.model:
|
||||
ent_cfg[ATTR_MODEL] = dev_reg_ent.model
|
||||
if dev_reg_ent.sw_version:
|
||||
ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version
|
||||
if ATTR_MANUFACTURER not in ent_cfg:
|
||||
integration = await async_get_integration(self.hass, ent_reg_ent.platform)
|
||||
ent_cfg[ATTR_INTERGRATION] = integration.name
|
||||
|
||||
|
||||
class HomeKitPairingQRView(HomeAssistantView):
|
||||
"""Display the homekit pairing code at a protected url."""
|
||||
|
@ -40,6 +40,10 @@ from homeassistant.util.decorator import Registry
|
||||
|
||||
from .const import (
|
||||
ATTR_DISPLAY_NAME,
|
||||
ATTR_INTERGRATION,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
ATTR_VALUE,
|
||||
BRIDGE_MODEL,
|
||||
BRIDGE_SERIAL_NUMBER,
|
||||
@ -235,15 +239,32 @@ class HomeAccessory(Accessory):
|
||||
):
|
||||
"""Initialize a Accessory object."""
|
||||
super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs)
|
||||
model = split_entity_id(entity_id)[0].replace("_", " ").title()
|
||||
self.config = config or {}
|
||||
domain = split_entity_id(entity_id)[0].replace("_", " ")
|
||||
|
||||
if ATTR_MANUFACTURER in self.config:
|
||||
manufacturer = self.config[ATTR_MANUFACTURER]
|
||||
elif ATTR_INTERGRATION in self.config:
|
||||
manufacturer = self.config[ATTR_INTERGRATION].replace("_", " ").title()
|
||||
else:
|
||||
manufacturer = f"{MANUFACTURER} {domain}".title()
|
||||
if ATTR_MODEL in self.config:
|
||||
model = self.config[ATTR_MODEL]
|
||||
else:
|
||||
model = domain.title()
|
||||
if ATTR_SOFTWARE_VERSION in self.config:
|
||||
sw_version = self.config[ATTR_SOFTWARE_VERSION]
|
||||
else:
|
||||
sw_version = __version__
|
||||
|
||||
self.set_info_service(
|
||||
firmware_revision=__version__,
|
||||
manufacturer=MANUFACTURER,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
serial_number=entity_id,
|
||||
firmware_revision=sw_version,
|
||||
)
|
||||
|
||||
self.category = category
|
||||
self.config = config or {}
|
||||
self.entity_id = entity_id
|
||||
self.hass = hass
|
||||
self.debounce = {}
|
||||
|
@ -23,6 +23,10 @@ AUDIO_CODEC_COPY = "copy"
|
||||
# #### Attributes ####
|
||||
ATTR_DISPLAY_NAME = "display_name"
|
||||
ATTR_VALUE = "value"
|
||||
ATTR_INTERGRATION = "platform"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_SOFTWARE_VERSION = "sw_version"
|
||||
|
||||
# #### Config ####
|
||||
CONF_ADVERTISE_IP = "advertise_ip"
|
||||
@ -50,6 +54,7 @@ CONF_VIDEO_MAP = "video_map"
|
||||
CONF_VIDEO_PACKET_SIZE = "video_packet_size"
|
||||
|
||||
# #### Config Defaults ####
|
||||
DEFAULT_SUPPORT_AUDIO = False
|
||||
DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS
|
||||
DEFAULT_AUDIO_MAP = "0:a:0"
|
||||
DEFAULT_AUDIO_PACKET_SIZE = 188
|
||||
|
@ -33,10 +33,20 @@ from .const import (
|
||||
CONF_VIDEO_CODEC,
|
||||
CONF_VIDEO_MAP,
|
||||
CONF_VIDEO_PACKET_SIZE,
|
||||
DEFAULT_AUDIO_CODEC,
|
||||
DEFAULT_AUDIO_MAP,
|
||||
DEFAULT_AUDIO_PACKET_SIZE,
|
||||
DEFAULT_MAX_FPS,
|
||||
DEFAULT_MAX_HEIGHT,
|
||||
DEFAULT_MAX_WIDTH,
|
||||
DEFAULT_SUPPORT_AUDIO,
|
||||
DEFAULT_VIDEO_CODEC,
|
||||
DEFAULT_VIDEO_MAP,
|
||||
DEFAULT_VIDEO_PACKET_SIZE,
|
||||
SERV_CAMERA_RTP_STREAM_MANAGEMENT,
|
||||
)
|
||||
from .img_util import scale_jpeg_camera_image
|
||||
from .util import CAMERA_SCHEMA, pid_is_alive
|
||||
from .util import pid_is_alive
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -94,6 +104,19 @@ FFMPEG_WATCHER = "ffmpeg_watcher"
|
||||
FFMPEG_PID = "ffmpeg_pid"
|
||||
SESSION_ID = "session_id"
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
CONF_SUPPORT_AUDIO: DEFAULT_SUPPORT_AUDIO,
|
||||
CONF_MAX_WIDTH: DEFAULT_MAX_WIDTH,
|
||||
CONF_MAX_HEIGHT: DEFAULT_MAX_HEIGHT,
|
||||
CONF_MAX_FPS: DEFAULT_MAX_FPS,
|
||||
CONF_AUDIO_CODEC: DEFAULT_AUDIO_CODEC,
|
||||
CONF_AUDIO_MAP: DEFAULT_AUDIO_MAP,
|
||||
CONF_VIDEO_MAP: DEFAULT_VIDEO_MAP,
|
||||
CONF_VIDEO_CODEC: DEFAULT_VIDEO_CODEC,
|
||||
CONF_AUDIO_PACKET_SIZE: DEFAULT_AUDIO_PACKET_SIZE,
|
||||
CONF_VIDEO_PACKET_SIZE: DEFAULT_VIDEO_PACKET_SIZE,
|
||||
}
|
||||
|
||||
|
||||
@TYPES.register("Camera")
|
||||
class Camera(HomeAccessory, PyhapCamera):
|
||||
@ -104,11 +127,13 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._cur_session = None
|
||||
self._camera = hass.data[DOMAIN_CAMERA]
|
||||
config_w_defaults = CAMERA_SCHEMA(config)
|
||||
for config_key in CONFIG_DEFAULTS:
|
||||
if config_key not in config:
|
||||
config[config_key] = CONFIG_DEFAULTS[config_key]
|
||||
|
||||
max_fps = config_w_defaults[CONF_MAX_FPS]
|
||||
max_width = config_w_defaults[CONF_MAX_WIDTH]
|
||||
max_height = config_w_defaults[CONF_MAX_HEIGHT]
|
||||
max_fps = config[CONF_MAX_FPS]
|
||||
max_width = config[CONF_MAX_WIDTH]
|
||||
max_height = config[CONF_MAX_HEIGHT]
|
||||
resolutions = [
|
||||
(w, h, fps)
|
||||
for w, h, fps in SLOW_RESOLUTIONS
|
||||
@ -136,7 +161,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
}
|
||||
audio_options = {"codecs": [{"type": "OPUS", "samplerate": 24}]}
|
||||
|
||||
stream_address = config_w_defaults.get(CONF_STREAM_ADDRESS, get_local_ip())
|
||||
stream_address = config.get(CONF_STREAM_ADDRESS, get_local_ip())
|
||||
|
||||
options = {
|
||||
"video": video_options,
|
||||
@ -151,7 +176,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
name,
|
||||
entity_id,
|
||||
aid,
|
||||
config_w_defaults,
|
||||
config,
|
||||
category=CATEGORY_CAMERA,
|
||||
options=options,
|
||||
)
|
||||
|
@ -49,6 +49,7 @@ from .const import (
|
||||
DEFAULT_MAX_FPS,
|
||||
DEFAULT_MAX_HEIGHT,
|
||||
DEFAULT_MAX_WIDTH,
|
||||
DEFAULT_SUPPORT_AUDIO,
|
||||
DEFAULT_VIDEO_CODEC,
|
||||
DEFAULT_VIDEO_MAP,
|
||||
DEFAULT_VIDEO_PACKET_SIZE,
|
||||
@ -98,7 +99,7 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
vol.Optional(CONF_AUDIO_CODEC, default=DEFAULT_AUDIO_CODEC): vol.In(
|
||||
VALID_AUDIO_CODECS
|
||||
),
|
||||
vol.Optional(CONF_SUPPORT_AUDIO, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SUPPORT_AUDIO, default=DEFAULT_SUPPORT_AUDIO): cv.boolean,
|
||||
vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int,
|
||||
vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int,
|
||||
|
@ -14,6 +14,10 @@ from homeassistant.components.homekit.accessories import (
|
||||
)
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_DISPLAY_NAME,
|
||||
ATTR_INTERGRATION,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
ATTR_VALUE,
|
||||
BRIDGE_MODEL,
|
||||
BRIDGE_NAME,
|
||||
@ -80,11 +84,17 @@ async def test_debounce(hass):
|
||||
|
||||
async def test_home_accessory(hass, hk_driver):
|
||||
"""Test HomeAccessory class."""
|
||||
entity_id = "homekit.accessory"
|
||||
entity_id = "sensor.accessory"
|
||||
entity_id2 = "light.accessory"
|
||||
|
||||
hass.states.async_set(entity_id, None)
|
||||
hass.states.async_set(entity_id2, None)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None)
|
||||
acc = HomeAccessory(
|
||||
hass, hk_driver, "Home Accessory", entity_id, 2, {"platform": "isy994"}
|
||||
)
|
||||
assert acc.hass == hass
|
||||
assert acc.display_name == "Home Accessory"
|
||||
assert acc.aid == 2
|
||||
@ -93,9 +103,35 @@ async def test_home_accessory(hass, hk_driver):
|
||||
serv = acc.services[0] # SERV_ACCESSORY_INFO
|
||||
assert serv.display_name == SERV_ACCESSORY_INFO
|
||||
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Homekit"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "homekit.accessory"
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Isy994"
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Sensor"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "sensor.accessory"
|
||||
|
||||
acc2 = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 3, {})
|
||||
serv = acc2.services[0] # SERV_ACCESSORY_INFO
|
||||
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == f"{MANUFACTURER} Light"
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Light"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory"
|
||||
|
||||
acc3 = HomeAccessory(
|
||||
hass,
|
||||
hk_driver,
|
||||
"Home Accessory",
|
||||
entity_id2,
|
||||
3,
|
||||
{
|
||||
ATTR_MODEL: "Awesome",
|
||||
ATTR_MANUFACTURER: "Lux Brands",
|
||||
ATTR_SOFTWARE_VERSION: "0.4.3",
|
||||
ATTR_INTERGRATION: "luxe",
|
||||
},
|
||||
)
|
||||
serv = acc3.services[0] # SERV_ACCESSORY_INFO
|
||||
assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory"
|
||||
assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands"
|
||||
assert serv.get_characteristic(CHAR_MODEL).value == "Awesome"
|
||||
assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory"
|
||||
|
||||
hass.states.async_set(entity_id, "on")
|
||||
await hass.async_block_till_done()
|
||||
@ -441,9 +477,7 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog):
|
||||
hass.states.async_set(entity_id, None, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc = HomeAccessory(
|
||||
hass, hk_driver, "Accessory without battery", entity_id, 2, None
|
||||
)
|
||||
acc = HomeAccessory(hass, hk_driver, "Accessory without battery", entity_id, 2, {})
|
||||
assert acc._char_battery is None
|
||||
|
||||
with patch(
|
||||
@ -469,7 +503,7 @@ async def test_call_service(hass, hk_driver, events):
|
||||
hass.states.async_set(entity_id, None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, None)
|
||||
acc = HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {})
|
||||
call_service = async_mock_service(hass, "cover", "open_cover")
|
||||
|
||||
test_domain = "cover"
|
||||
|
@ -5,6 +5,7 @@ import homeassistant.components.climate as climate
|
||||
import homeassistant.components.cover as cover
|
||||
from homeassistant.components.homekit.accessories import TYPES, get_accessory
|
||||
from homeassistant.components.homekit.const import (
|
||||
ATTR_INTERGRATION,
|
||||
CONF_FEATURE_LIST,
|
||||
FEATURE_ON_OFF,
|
||||
TYPE_FAUCET,
|
||||
@ -61,10 +62,12 @@ def test_not_supported_media_player():
|
||||
def test_customize_options(config, name):
|
||||
"""Test with customized options."""
|
||||
mock_type = Mock()
|
||||
conf = config.copy()
|
||||
conf[ATTR_INTERGRATION] = "platform_name"
|
||||
with patch.dict(TYPES, {"Light": mock_type}):
|
||||
entity_state = State("light.demo", "on")
|
||||
get_accessory(None, None, entity_state, 2, config)
|
||||
mock_type.assert_called_with(None, None, name, "light.demo", 2, config)
|
||||
get_accessory(None, None, entity_state, 2, conf)
|
||||
mock_type.assert_called_with(None, None, name, "light.demo", 2, conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -254,7 +257,7 @@ def test_type_switches(type_name, entity_id, state, attrs, config):
|
||||
| vacuum.SUPPORT_RETURN_HOME
|
||||
},
|
||||
),
|
||||
("Switch", "vacuum.basic_vacuum", "off", {},),
|
||||
("Switch", "vacuum.basic_vacuum", "off", {}),
|
||||
],
|
||||
)
|
||||
def test_type_vacuum(type_name, entity_id, state, attrs):
|
||||
|
@ -703,25 +703,28 @@ async def test_homekit_finds_linked_batteries(
|
||||
config_entry.add_to_hass(hass)
|
||||
device_entry = device_reg.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
sw_version="0.16.0",
|
||||
model="Powerwall 2",
|
||||
manufacturer="Tesla",
|
||||
connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
)
|
||||
|
||||
binary_charging_sensor = entity_reg.async_get_or_create(
|
||||
"binary_sensor",
|
||||
"light",
|
||||
"powerwall",
|
||||
"battery_charging",
|
||||
device_id=device_entry.id,
|
||||
device_class=DEVICE_CLASS_BATTERY_CHARGING,
|
||||
)
|
||||
battery_sensor = entity_reg.async_get_or_create(
|
||||
"sensor",
|
||||
"light",
|
||||
"powerwall",
|
||||
"battery",
|
||||
device_id=device_entry.id,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
)
|
||||
light = entity_reg.async_get_or_create(
|
||||
"light", "light", "demo", device_id=device_entry.id
|
||||
"light", "powerwall", "demo", device_id=device_entry.id
|
||||
)
|
||||
|
||||
hass.states.async_set(
|
||||
@ -751,8 +754,11 @@ async def test_homekit_finds_linked_batteries(
|
||||
ANY,
|
||||
ANY,
|
||||
{
|
||||
"linked_battery_charging_sensor": "binary_sensor.light_battery_charging",
|
||||
"linked_battery_sensor": "sensor.light_battery",
|
||||
"manufacturer": "Tesla",
|
||||
"model": "Powerwall 2",
|
||||
"sw_version": "0.16.0",
|
||||
"linked_battery_charging_sensor": "binary_sensor.powerwall_battery_charging",
|
||||
"linked_battery_sensor": "sensor.powerwall_battery",
|
||||
},
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user