Add support for homekit_controller secondary entities like power usage (#44013)

This commit is contained in:
Jc2k 2021-01-26 19:45:01 +00:00 committed by GitHub
parent f53a83e084
commit 3b0a440770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 646 additions and 17 deletions

View File

@ -183,6 +183,21 @@ class AccessoryEntity(HomeKitEntity):
return f"homekit-{serial}-aid:{self._aid}"
class CharacteristicEntity(HomeKitEntity):
"""
A HomeKit entity that is related to an single characteristic rather than a whole service.
This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with
the service entity.
"""
@property
def unique_id(self) -> str:
"""Return the ID of this device."""
serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
return f"homekit-{serial}-aid:{self._aid}-sid:{self._iid}-cid:{self._iid}"
async def async_setup_entry(hass, entry):
"""Set up a HomeKit connection on a config entry."""
conn = HKDevice(hass, entry, entry.data)

View File

@ -15,7 +15,13 @@ from aiohomekit.model.services import ServicesTypes
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
from .const import CONTROLLER, DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH
from .const import (
CHARACTERISTIC_PLATFORMS,
CONTROLLER,
DOMAIN,
ENTITY_MAP,
HOMEKIT_ACCESSORY_DISPATCH,
)
from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry
DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60)
@ -82,6 +88,9 @@ class HKDevice:
# A list of callbacks that turn HK service metadata into entities
self.listeners = []
# A list of callbacks that turn HK characteristics into entities
self.char_factories = []
# The platorms we have forwarded the config entry so far. If a new
# accessory is added to a bridge we may have to load additional
# platforms. We don't want to load all platforms up front if its just
@ -306,6 +315,22 @@ class HKDevice:
self.entities.append((accessory.aid, None))
break
def add_char_factory(self, add_entities_cb):
"""Add a callback to run when discovering new entities for accessories."""
self.char_factories.append(add_entities_cb)
self._add_new_entities_for_char([add_entities_cb])
def _add_new_entities_for_char(self, handlers):
for accessory in self.entity_map.accessories:
for service in accessory.services:
for char in service.characteristics:
for handler in handlers:
if (accessory.aid, service.iid, char.iid) in self.entities:
continue
if handler(char):
self.entities.append((accessory.aid, service.iid, char.iid))
break
def add_listener(self, add_entities_cb):
"""Add a callback to run when discovering new entities for services."""
self.listeners.append(add_entities_cb)
@ -315,6 +340,7 @@ class HKDevice:
"""Process the entity map and create HA entities."""
self._add_new_entities(self.listeners)
self._add_new_entities_for_accessory(self.accessory_factories)
self._add_new_entities_for_char(self.char_factories)
def _add_new_entities(self, callbacks):
for accessory in self.entity_map.accessories:
@ -331,26 +357,33 @@ class HKDevice:
self.entities.append((aid, iid))
break
async def async_load_platform(self, platform):
"""Load a single platform idempotently."""
if platform in self.platforms:
return
self.platforms.add(platform)
try:
await self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
)
except Exception:
self.platforms.remove(platform)
raise
async def async_load_platforms(self):
"""Load any platforms needed by this HomeKit device."""
for accessory in self.accessories:
for service in accessory["services"]:
stype = ServicesTypes.get_short(service["type"].upper())
if stype not in HOMEKIT_ACCESSORY_DISPATCH:
continue
if stype in HOMEKIT_ACCESSORY_DISPATCH:
platform = HOMEKIT_ACCESSORY_DISPATCH[stype]
await self.async_load_platform(platform)
platform = HOMEKIT_ACCESSORY_DISPATCH[stype]
if platform in self.platforms:
continue
self.platforms.add(platform)
try:
await self.hass.config_entries.async_forward_entry_setup(
self.config_entry, platform
)
except Exception:
self.platforms.remove(platform)
raise
for char in service["characteristics"]:
if char["type"].upper() in CHARACTERISTIC_PLATFORMS:
platform = CHARACTERISTIC_PLATFORMS[char["type"].upper()]
await self.async_load_platform(platform)
async def async_update(self, now=None):
"""Poll state of all entities attached to this bridge/accessory."""

View File

@ -1,4 +1,6 @@
"""Constants for the homekit_controller component."""
from aiohomekit.model.characteristics import CharacteristicsTypes
DOMAIN = "homekit_controller"
KNOWN_DEVICES = f"{DOMAIN}-devices"
@ -40,3 +42,7 @@ HOMEKIT_ACCESSORY_DISPATCH = {
"valve": "switch",
"camera-rtp-stream-management": "camera",
}
CHARACTERISTIC_PLATFORMS = {
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor",
}

View File

@ -7,6 +7,7 @@ from homeassistant.const import (
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
LIGHT_LUX,
PERCENTAGE,
@ -14,7 +15,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from . import KNOWN_DEVICES, HomeKitEntity
from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity
HUMIDITY_ICON = "mdi:water-percent"
TEMP_C_ICON = "mdi:thermometer"
@ -22,6 +23,22 @@ BRIGHTNESS_ICON = "mdi:brightness-6"
CO2_ICON = "mdi:molecule-co2"
SIMPLE_SENSOR = {
CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: {
"name": "Real Time Energy",
"device_class": DEVICE_CLASS_POWER,
"unit": "watts",
"icon": "mdi:chart-line",
},
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: {
"name": "Real Time Energy",
"device_class": DEVICE_CLASS_POWER,
"unit": "watts",
"icon": "mdi:chart-line",
},
}
class HomeKitHumiditySensor(HomeKitEntity):
"""Representation of a Homekit humidity sensor."""
@ -216,6 +233,66 @@ class HomeKitBatterySensor(HomeKitEntity):
return self.service.value(CharacteristicsTypes.BATTERY_LEVEL)
class SimpleSensor(CharacteristicEntity):
"""
A simple sensor for a single characteristic.
This may be an additional secondary entity that is part of another service. An
example is a switch that has an energy sensor.
These *have* to have a different unique_id to the normal sensors as there could
be multiple entities per HomeKit service (this was not previously the case).
"""
def __init__(
self,
conn,
info,
char,
device_class=None,
unit=None,
icon=None,
name=None,
):
"""Initialise a secondary HomeKit characteristic sensor."""
self._device_class = device_class
self._unit = unit
self._icon = icon
self._name = name
self._char = char
super().__init__(conn, info)
def get_characteristic_types(self):
"""Define the homekit characteristics the entity is tracking."""
return [self._char.type]
@property
def device_class(self):
"""Return units for the sensor."""
return self._device_class
@property
def unit_of_measurement(self):
"""Return units for the sensor."""
return self._unit
@property
def icon(self):
"""Return the sensor icon."""
return self._icon
@property
def name(self) -> str:
"""Return the name of the device if any."""
return f"{super().name} - {self._name}"
@property
def state(self):
"""Return the current sensor value."""
return self._char.value
ENTITY_TYPES = {
ServicesTypes.HUMIDITY_SENSOR: HomeKitHumiditySensor,
ServicesTypes.TEMPERATURE_SENSOR: HomeKitTemperatureSensor,
@ -240,3 +317,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
return True
conn.add_listener(async_add_service)
@callback
def async_add_characteristic(char):
kwargs = SIMPLE_SENSOR.get(char.type)
if not kwargs:
return False
info = {"aid": char.service.accessory.aid, "iid": char.service.iid}
async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True)
return True
conn.add_char_factory(async_add_characteristic)

View File

@ -0,0 +1,51 @@
"""Make sure that existing Koogeek P1EU support isn't broken."""
from tests.components.homekit_controller.common import (
Helper,
setup_accessories_from_file,
setup_test_accessories,
)
async def test_koogeek_p1eu_setup(hass):
"""Test that a Koogeek P1EU can be correctly setup in HA."""
accessories = await setup_accessories_from_file(hass, "koogeek_p1eu.json")
config_entry, pairing = await setup_test_accessories(hass, accessories)
entity_registry = await hass.helpers.entity_registry.async_get_registry()
device_registry = await hass.helpers.device_registry.async_get_registry()
# Check that the switch entity is handled correctly
entry = entity_registry.async_get("switch.koogeek_p1_a00aa0")
assert entry.unique_id == "homekit-EUCP03190xxxxx48-7"
helper = Helper(
hass, "switch.koogeek_p1_a00aa0", pairing, accessories[0], config_entry
)
state = await helper.poll_and_get_state()
assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0"
device = device_registry.async_get(entry.device_id)
assert device.manufacturer == "Koogeek"
assert device.name == "Koogeek-P1-A00AA0"
assert device.model == "P1EU"
assert device.sw_version == "2.3.7"
assert device.via_device_id is None
# Assert the power sensor is detected
entry = entity_registry.async_get("sensor.koogeek_p1_a00aa0_real_time_energy")
assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:21"
helper = Helper(
hass,
"sensor.koogeek_p1_a00aa0_real_time_energy",
pairing,
accessories[0],
config_entry,
)
state = await helper.poll_and_get_state()
assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0 - Real Time Energy"
# The sensor and switch should be part of the same device
assert entry.device_id == device.id

View File

@ -9,7 +9,7 @@ from homeassistant.const import (
DEVICE_CLASS_TEMPERATURE,
)
from tests.components.homekit_controller.common import setup_test_component
from tests.components.homekit_controller.common import Helper, setup_test_component
TEMPERATURE = ("temperature", "temperature.current")
HUMIDITY = ("humidity", "relative-humidity.current")
@ -18,6 +18,7 @@ CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level")
BATTERY_LEVEL = ("battery", "battery-level")
CHARGING_STATE = ("battery", "charging-state")
LO_BATT = ("battery", "status-lo-batt")
ON = ("outlet", "on")
def create_temperature_sensor_service(accessory):
@ -183,3 +184,45 @@ async def test_battery_low(hass, utcnow):
helper.characteristics[LO_BATT].value = 1
state = await helper.poll_and_get_state()
assert state.attributes["icon"] == "mdi:battery-alert"
def create_switch_with_sensor(accessory):
"""Define battery level characteristics."""
service = accessory.add_service(ServicesTypes.OUTLET)
realtime_energy = service.add_char(
CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY
)
realtime_energy.value = 0
realtime_energy.format = "float"
cur_state = service.add_char(CharacteristicsTypes.ON)
cur_state.value = True
return service
async def test_switch_with_sensor(hass, utcnow):
"""Test a switch service that has a sensor characteristic is correctly handled."""
helper = await setup_test_component(hass, create_switch_with_sensor)
outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET)
# Helper will be for the primary entity, which is the outlet. Make a helper for the sensor.
energy_helper = Helper(
hass,
"sensor.testdevice_real_time_energy",
helper.pairing,
helper.accessory,
helper.config_entry,
)
outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET)
realtime_energy = outlet[CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY]
realtime_energy.value = 1
state = await energy_helper.poll_and_get_state()
assert state.state == "1"
realtime_energy.value = 50
state = await energy_helper.poll_and_get_state()
assert state.state == "50"

View File

@ -0,0 +1,392 @@
[
{
"aid": 1,
"services": [
{
"characteristics": [
{
"format": "string",
"iid": 2,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000023-0000-1000-8000-0026BB765291",
"value": "Koogeek-P1-A00AA0"
},
{
"format": "string",
"iid": 3,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000020-0000-1000-8000-0026BB765291",
"value": "Koogeek"
},
{
"format": "string",
"iid": 4,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000021-0000-1000-8000-0026BB765291",
"value": "P1EU"
},
{
"format": "string",
"iid": 5,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000030-0000-1000-8000-0026BB765291",
"value": "EUCP03190xxxxx48"
},
{
"format": "bool",
"iid": 6,
"perms": [
"pw"
],
"type": "00000014-0000-1000-8000-0026BB765291"
},
{
"format": "string",
"iid": 37,
"perms": [
"pr"
],
"type": "00000052-0000-1000-8000-0026BB765291",
"value": "2.3.7"
}
],
"iid": 1,
"stype": "accessory-information",
"type": "0000003E-0000-1000-8000-0026BB765291"
},
{
"characteristics": [
{
"ev": false,
"format": "bool",
"iid": 8,
"perms": [
"pr",
"pw",
"ev"
],
"type": "00000025-0000-1000-8000-0026BB765291",
"value": false
},
{
"ev": false,
"format": "bool",
"iid": 9,
"perms": [
"pr",
"ev"
],
"type": "00000026-0000-1000-8000-0026BB765291",
"value": true
},
{
"format": "string",
"iid": 10,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000023-0000-1000-8000-0026BB765291",
"value": "outlet"
}
],
"iid": 7,
"primary": true,
"stype": "outlet",
"type": "00000047-0000-1000-8000-0026BB765291"
},
{
"characteristics": [
{
"description": "TIMER_SETTINGS",
"format": "tlv8",
"iid": 12,
"perms": [
"pr",
"pw"
],
"type": "4AAAF942-0DEC-11E5-B939-0800200C9A66",
"value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
],
"iid": 11,
"stype": "Unknown Service: 4AAAF940-0DEC-11E5-B939-0800200C9A66",
"type": "4AAAF940-0DEC-11E5-B939-0800200C9A66"
},
{
"characteristics": [
{
"description": "FW Upgrade supported types",
"format": "string",
"iid": 14,
"perms": [
"pr",
"hd"
],
"type": "151909D2-3802-11E4-916C-0800200C9A66",
"value": "url,data"
},
{
"description": "FW Upgrade URL",
"format": "string",
"iid": 15,
"maxLen": 256,
"perms": [
"pw",
"hd"
],
"type": "151909D1-3802-11E4-916C-0800200C9A66"
},
{
"description": "FW Upgrade Status",
"ev": false,
"format": "int",
"iid": 16,
"perms": [
"pr",
"ev",
"hd"
],
"type": "151909D6-3802-11E4-916C-0800200C9A66",
"value": 0
},
{
"description": "FW Upgrade Data",
"format": "data",
"iid": 17,
"perms": [
"pw",
"hd"
],
"type": "151909D7-3802-11E4-916C-0800200C9A66"
}
],
"hidden": true,
"iid": 13,
"stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66",
"type": "151909D0-3802-11E4-916C-0800200C9A66"
},
{
"characteristics": [
{
"description": "Timezone",
"format": "int",
"iid": 19,
"perms": [
"pr",
"pw"
],
"type": "151909D5-3802-11E4-916C-0800200C9A66",
"value": 0
},
{
"description": "Time value since Epoch",
"format": "int",
"iid": 20,
"perms": [
"pr",
"pw"
],
"type": "151909D4-3802-11E4-916C-0800200C9A66",
"value": 1570358601
}
],
"iid": 18,
"stype": "Unknown Service: 151909D3-3802-11E4-916C-0800200C9A66",
"type": "151909D3-3802-11E4-916C-0800200C9A66"
},
{
"characteristics": [
{
"description": "1 REALTIME_ENERGY",
"ev": false,
"format": "float",
"iid": 22,
"perms": [
"pr",
"ev"
],
"type": "4AAAF931-0DEC-11E5-B939-0800200C9A66",
"value": 5
},
{
"description": "2 CURRENT_HOUR_DATA",
"ev": false,
"format": "float",
"iid": 23,
"perms": [
"pr",
"ev"
],
"type": "4AAAF932-0DEC-11E5-B939-0800200C9A66",
"value": 0
},
{
"description": "3 HOUR_DATA_TODAY",
"format": "tlv8",
"iid": 24,
"perms": [
"pr"
],
"type": "4AAAF933-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "4 HOUR_DATA_YESTERDAY",
"format": "tlv8",
"iid": 25,
"perms": [
"pr"
],
"type": "4AAAF934-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "5 HOUR_DATA_2_DAYS_BEFORE",
"format": "tlv8",
"iid": 26,
"perms": [
"pr"
],
"type": "4AAAF935-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "6 HOUR_DATA_3_DAYS_BEFORE",
"format": "tlv8",
"iid": 27,
"perms": [
"pr"
],
"type": "4AAAF936-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "7 HOUR_DATA_4_DAYS_BEFORE",
"format": "tlv8",
"iid": 28,
"perms": [
"pr"
],
"type": "4AAAF937-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "8 HOUR_DATA_5_DAYS_BEFORE",
"format": "tlv8",
"iid": 29,
"perms": [
"pr"
],
"type": "4AAAF938-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "9 HOUR_DATA_6_DAYS_BEFORE",
"format": "tlv8",
"iid": 30,
"perms": [
"pr"
],
"type": "4AAAF939-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "10 HOUR_DATA_7_DAYS_BEFORE",
"format": "tlv8",
"iid": 31,
"perms": [
"pr"
],
"type": "4AAAF93A-0DEC-11E5-B939-0800200C9A66",
"value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "11 DAY_DATA_THIS_MONTH",
"format": "tlv8",
"iid": 32,
"perms": [
"pr"
],
"type": "4AAAF93B-0DEC-11E5-B939-0800200C9A66",
"value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"description": "12 DAY_DATA_LAST_MONTH",
"format": "tlv8",
"iid": 33,
"perms": [
"pr"
],
"type": "4AAAF93C-0DEC-11E5-B939-0800200C9A66",
"value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
{
"description": "13 MONTH_DATA_THIS_YEAR",
"format": "tlv8",
"iid": 34,
"perms": [
"pr"
],
"type": "4AAAF93D-0DEC-11E5-B939-0800200C9A66",
"value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "14 MONTH_DATA_LAST_YEAR",
"format": "tlv8",
"iid": 35,
"perms": [
"pr"
],
"type": "4AAAF93E-0DEC-11E5-B939-0800200C9A66",
"value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"description": "15 RUNNING_TIME",
"ev": false,
"format": "int",
"iid": 36,
"perms": [
"pr",
"ev"
],
"type": "4AAAF93F-0DEC-11E5-B939-0800200C9A66",
"value": 0
}
],
"iid": 21,
"stype": "Unknown Service: 4AAAF930-0DEC-11E5-B939-0800200C9A66",
"type": "4AAAF930-0DEC-11E5-B939-0800200C9A66"
},
{
"characteristics": [
{
"format": "string",
"iid": 39,
"maxLen": 64,
"perms": [
"pr"
],
"type": "00000037-0000-1000-8000-0026BB765291",
"value": "1.1.0"
}
],
"iid": 38,
"stype": "service",
"type": "000000A2-0000-1000-8000-0026BB765291"
}
]
}
]