From 3b0a440770fefc56411beea24eeaac8a2450d4da Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 26 Jan 2021 19:45:01 +0000 Subject: [PATCH] Add support for homekit_controller secondary entities like power usage (#44013) --- .../components/homekit_controller/__init__.py | 15 + .../homekit_controller/connection.py | 63 ++- .../components/homekit_controller/const.py | 6 + .../components/homekit_controller/sensor.py | 91 +++- .../specific_devices/test_koogeek_p1eu.py | 51 +++ .../homekit_controller/test_sensor.py | 45 +- .../homekit_controller/koogeek_p1eu.json | 392 ++++++++++++++++++ 7 files changed, 646 insertions(+), 17 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py create mode 100644 tests/fixtures/homekit_controller/koogeek_p1eu.json diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ef0fb531b1b..0a8f376fb33 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -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) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 2b37d3e3d20..677b8dab5f6 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -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.""" diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index c3af1033148..a3f7a9b7921 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -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", +} diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 677f1bc67f1..094d0a500d1 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -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) diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py new file mode 100644 index 00000000000..f97821ef111 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -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 diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 8b0528ea46d..a79e94c4bb7 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -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" diff --git a/tests/fixtures/homekit_controller/koogeek_p1eu.json b/tests/fixtures/homekit_controller/koogeek_p1eu.json new file mode 100644 index 00000000000..d9d252b4cb7 --- /dev/null +++ b/tests/fixtures/homekit_controller/koogeek_p1eu.json @@ -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" + } + ] + } +] \ No newline at end of file