Spring cleaning ☀️ (#23)

* Spring cleaning

* Actions

* Fix branches

* Changes for config_flow
This commit is contained in:
Joakim Sørensen
2020-04-17 19:42:59 +02:00
committed by GitHub
parent d408fbec4a
commit a53b0f75a1
28 changed files with 349 additions and 731 deletions

View File

@ -13,9 +13,17 @@
},
"error": {
"auth": "Username/Password is wrong."
},
"abort": {
"single_instance_allowed": "Only a single configuration of Blueprint is allowed."
}
},
"options": {
"step": {
"user": {
"data": {
"binary_sensor": "Binary sensor enabled",
"sensor": "Sensor enabled",
"switch": "Switch enabled"
}
}
}
}
}

View File

@ -13,9 +13,17 @@
},
"error": {
"auth": "Brukernavn/Passord er feil."
},
"abort": {
"single_instance_allowed": "Du kan konfigurere Blueprint kun en gang."
}
},
"options": {
"step": {
"user": {
"data": {
"binary_sensor": "Binær sensor aktivert",
"sensor": "Sensor aktivert",
"switch": "Bryter aktivert"
}
}
}
}
}

View File

@ -1,244 +1,107 @@
"""
Component to integrate with blueprint.
Custom integration to integrate blueprint with Home Assistant.
For more details about this component, please refer to
For more details about this integration, please refer to
https://github.com/custom-components/blueprint
"""
import os
from datetime import timedelta
import asyncio
import logging
import voluptuous as vol
from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from sampleclient.client import Client
from integrationhelper.const import CC_STARTUP_VERSION
from .const import (
CONF_BINARY_SENSOR,
CONF_ENABLED,
CONF_NAME,
CONF_PASSWORD,
CONF_SENSOR,
CONF_SWITCH,
CONF_USERNAME,
DEFAULT_NAME,
DOMAIN_DATA,
DOMAIN,
ISSUE_URL,
PLATFORMS,
REQUIRED_FILES,
VERSION,
STARTUP_MESSAGE,
)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BINARY_SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
SWITCH_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_BINARY_SENSOR): vol.All(
cv.ensure_list, [BINARY_SENSOR_SCHEMA]
),
vol.Optional(CONF_SENSOR): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
vol.Optional(CONF_SWITCH): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up this component using YAML."""
if config.get(DOMAIN) is None:
# We get here if the integration is set up using config flow
return True
# Print startup message
_LOGGER.info(
CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL)
)
# Check that all required files are present
file_check = await check_files(hass)
if not file_check:
return False
# Create DATA dict
hass.data[DOMAIN_DATA] = {}
# Get "global" configuration.
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
# Configure the client.
client = Client(username, password)
hass.data[DOMAIN_DATA]["client"] = BlueprintData(hass, client)
# Load platforms
for platform in PLATFORMS:
# Get platform specific configuration
platform_config = config[DOMAIN].get(platform, {})
# If platform is not enabled, skip.
if not platform_config:
continue
for entry in platform_config:
entry_config = entry
# If entry is not enabled, skip.
if not entry_config[CONF_ENABLED]:
continue
hass.async_create_task(
discovery.async_load_platform(
hass, platform, DOMAIN, entry_config, config
)
)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={}
)
)
async def async_setup(hass: HomeAssistant, config: Config):
"""Set up this integration using YAML is not supported."""
return True
async def async_setup_entry(hass, config_entry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up this integration using UI."""
conf = hass.data.get(DOMAIN_DATA)
if config_entry.source == config_entries.SOURCE_IMPORT:
if conf is None:
hass.async_create_task(
hass.config_entries.async_remove(config_entry.entry_id)
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})
_LOGGER.info(STARTUP_MESSAGE)
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
coordinator = BlueprintDataUpdateCoordinator(
hass, username=username, password=password
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
hass.data[DOMAIN][entry.entry_id] = coordinator
for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
return False
# Print startup message
_LOGGER.info(
CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL)
)
# Check that all required files are present
file_check = await check_files(hass)
if not file_check:
return False
# Create DATA dict
hass.data[DOMAIN_DATA] = {}
# Get "global" configuration.
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
# Configure the client.
client = Client(username, password)
hass.data[DOMAIN_DATA]["client"] = BlueprintData(hass, client)
# Add binary_sensor
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
)
# Add sensor
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
)
# Add switch
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(config_entry, "switch")
)
entry.add_update_listener(async_reload_entry)
return True
class BlueprintData:
"""This class handle communication and stores the data."""
class BlueprintDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(self, hass, client):
"""Initialize the class."""
self.hass = hass
self.client = client
def __init__(self, hass, username, password):
"""Initialize."""
self.api = Client(username, password)
self.platforms = []
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update_data(self):
"""Update data."""
# This is where the main logic to update platform data goes.
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self):
"""Update data via library."""
try:
data = self.client.get_data()
self.hass.data[DOMAIN_DATA]["data"] = data
except Exception as error: # pylint: disable=broad-except
_LOGGER.error("Could not update data - %s", error)
data = await self.api.async_get_data()
return data.get("data", {})
except Exception as exception:
raise UpdateFailed(exception)
async def check_files(hass):
"""Return bool that indicates if all files are present."""
# Verify that the user downloaded all files.
base = f"{hass.config.path()}/custom_components/{DOMAIN}/"
missing = []
for file in REQUIRED_FILES:
fullpath = "{}{}".format(base, file)
if not os.path.exists(fullpath):
missing.append(file)
if missing:
_LOGGER.critical("The following files are missing: %s", str(missing))
returnvalue = False
else:
returnvalue = True
return returnvalue
async def async_remove_entry(hass, config_entry):
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Handle removal of an entry."""
try:
await hass.config_entries.async_forward_entry_unload(
config_entry, "binary_sensor"
coordinator = hass.data[DOMAIN][entry.entry_id]
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
if platform in coordinator.platforms
]
)
_LOGGER.info(
"Successfully removed binary_sensor from the blueprint integration"
)
except ValueError:
pass
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)
try:
await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
_LOGGER.info("Successfully removed sensor from the blueprint integration")
except ValueError:
pass
return unloaded
try:
await hass.config_entries.async_forward_entry_unload(config_entry, "switch")
_LOGGER.info("Successfully removed switch from the blueprint integration")
except ValueError:
pass
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View File

@ -1,73 +1,28 @@
"""Binary sensor platform for blueprint."""
from homeassistant.components.binary_sensor import BinarySensorDevice
from .const import (
ATTRIBUTION,
from custom_components.blueprint.const import (
BINARY_SENSOR,
BINARY_SENSOR_DEVICE_CLASS,
DEFAULT_NAME,
DOMAIN_DATA,
DOMAIN,
)
from custom_components.blueprint.entity import BlueprintEntity
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None
): # pylint: disable=unused-argument
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup binary_sensor platform."""
async_add_entities([BlueprintBinarySensor(hass, discovery_info)], True)
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([BlueprintBinarySensor(coordinator, entry)])
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Setup sensor platform."""
async_add_devices([BlueprintBinarySensor(hass, {})], True)
class BlueprintBinarySensor(BinarySensorDevice):
class BlueprintBinarySensor(BlueprintEntity, BinarySensorDevice):
"""blueprint binary_sensor class."""
def __init__(self, hass, config):
self.hass = hass
self.attr = {}
self._status = False
self._name = config.get("name", DEFAULT_NAME)
async def async_update(self):
"""Update the binary_sensor."""
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()
# Get new data (if any)
updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {})
# Check the data and update the value.
if updated.get("bool_on") is None:
self._status = self._status
else:
self._status = updated.get("bool_on")
# Set/update attributes
self.attr["attribution"] = ATTRIBUTION
self.attr["time"] = str(updated.get("time"))
self.attr["static"] = updated.get("static")
@property
def unique_id(self):
"""Return a unique ID to use for this binary_sensor."""
return (
"0919a0cd-745c-48fd"
) # Don't hard code this, use something from the device/service.
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Blueprint",
}
@property
def name(self):
"""Return the name of the binary_sensor."""
return self._name
return f"{DEFAULT_NAME}_{BINARY_SENSOR}"
@property
def device_class(self):
@ -77,9 +32,4 @@ class BlueprintBinarySensor(BinarySensorDevice):
@property
def is_on(self):
"""Return true if the binary_sensor is on."""
return self._status
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self.attr
return self.coordinator.data.get("bool_on", False)

View File

@ -4,12 +4,17 @@ from collections import OrderedDict
import voluptuous as vol
from sampleclient.client import Client
from homeassistant import config_entries
from homeassistant.core import callback
from .const import DOMAIN
from custom_components.blueprint.const import (
DOMAIN,
CONF_PASSWORD,
CONF_USERNAME,
PLATFORMS,
)
@config_entries.HANDLERS.register(DOMAIN)
class BlueprintFlowHandler(config_entries.ConfigFlow):
class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Blueprint."""
VERSION = 1
@ -24,17 +29,14 @@ class BlueprintFlowHandler(config_entries.ConfigFlow):
): # pylint: disable=dangerous-default-value
"""Handle a flow initialized by the user."""
self._errors = {}
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")
if user_input is not None:
valid = await self._test_credentials(
user_input["username"], user_input["password"]
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
if valid:
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
else:
self._errors["base"] = "auth"
@ -42,42 +44,62 @@ class BlueprintFlowHandler(config_entries.ConfigFlow):
return await self._show_config_form(user_input)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return BlueprintOptionsFlowHandler(config_entry)
async def _show_config_form(self, user_input):
"""Show the configuration form to edit location data."""
# Defaults
username = ""
password = ""
if user_input is not None:
if "username" in user_input:
username = user_input["username"]
if "password" in user_input:
password = user_input["password"]
data_schema = OrderedDict()
data_schema[vol.Required("username", default=username)] = str
data_schema[vol.Required("password", default=password)] = str
return self.async_show_form(
step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str,}
),
errors=self._errors,
)
async def async_step_import(self, user_input): # pylint: disable=unused-argument
"""Import a config entry.
Special type of import, we're not actually going to store any data.
Instead, we're going to rely on the values that are in config file.
"""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="configuration.yaml", data={})
async def _test_credentials(self, username, password):
"""Return true if credentials is valid."""
try:
client = Client(username, password)
client.get_data()
await client.async_get_data()
return True
except Exception: # pylint: disable=broad-except
pass
return False
class BlueprintOptionsFlowHandler(config_entries.OptionsFlow):
"""Blueprint config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
self.options.update(user_input)
return await self._update_options()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(x, default=self.options.get(x, True)): bool
for x in sorted(PLATFORMS)
}
),
)
async def _update_options(self):
"""Update config entry options."""
return self.async_create_entry(
title=self.config_entry.data.get(CONF_USERNAME), data=self.options
)

View File

@ -1,20 +1,11 @@
"""Constants for blueprint."""
# Base component constants
NAME = "Blueprint"
DOMAIN = "blueprint"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.0.1"
PLATFORMS = ["binary_sensor", "sensor", "switch"]
REQUIRED_FILES = [
".translations/en.json",
"binary_sensor.py",
"const.py",
"config_flow.py",
"manifest.json",
"sensor.py",
"switch.py",
]
ISSUE_URL = "https://github.com/custom-components/blueprint/issues"
ATTRIBUTION = "Data from this is provided by blueprint."
# Icons
ICON = "mdi:format-quote-close"
@ -22,14 +13,28 @@ ICON = "mdi:format-quote-close"
# Device classes
BINARY_SENSOR_DEVICE_CLASS = "connectivity"
# Configuration
CONF_BINARY_SENSOR = "binary_sensor"
CONF_SENSOR = "sensor"
CONF_SWITCH = "switch"
# Platforms
BINARY_SENSOR = "binary_sensor"
SENSOR = "sensor"
SWITCH = "switch"
PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH]
# Configuration and options
CONF_ENABLED = "enabled"
CONF_NAME = "name"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
# Defaults
DEFAULT_NAME = DOMAIN
STARTUP_MESSAGE = f"""
-------------------------------------------------------------------
{NAME}
Version: {VERSION}
This is a custom integration!
If you have any issues with this you need to open an issue here:
{ISSUE_URL}
-------------------------------------------------------------------
"""

View File

@ -0,0 +1,54 @@
"""BlueprintEntity class"""
from homeassistant.helpers import entity
from custom_components.blueprint.const import DOMAIN, VERSION, NAME
class BlueprintEntity(entity.Entity):
def __init__(self, coordinator, config_entry):
self.coordinator = coordinator
self.config_entry = config_entry
@property
def should_poll(self):
"""No need to poll. Coordinator notifies entity of updates."""
return False
@property
def available(self):
"""Return if entity is available."""
return self.coordinator.last_update_success
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return self.config_entry.entry_id
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": NAME,
"model": VERSION,
"manufacturer": NAME,
}
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
"time": str(self.coordinator.data.get("time")),
"static": self.coordinator.data.get("static"),
}
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self.coordinator.async_add_listener(self.async_write_ha_state)
async def async_will_remove_from_hass(self):
"""Disconnect from update signal."""
self.coordinator.async_remove_listener(self.async_write_ha_state)
async def async_update(self):
"""Update Brother entity."""
await self.coordinator.async_request_refresh()

View File

@ -8,7 +8,6 @@
"@ludeeus"
],
"requirements": [
"sampleclient",
"integrationhelper"
"sampleclient"
]
}

View File

@ -1,79 +1,28 @@
"""Sensor platform for blueprint."""
from homeassistant.helpers.entity import Entity
from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN_DATA, ICON, DOMAIN
from custom_components.blueprint.const import DEFAULT_NAME, DOMAIN, ICON, SENSOR
from custom_components.blueprint.entity import BlueprintEntity
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None
): # pylint: disable=unused-argument
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup sensor platform."""
async_add_entities([BlueprintSensor(hass, discovery_info)], True)
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([BlueprintSensor(coordinator, entry)])
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Setup sensor platform."""
async_add_devices([BlueprintSensor(hass, {})], True)
class BlueprintSensor(Entity):
class BlueprintSensor(BlueprintEntity):
"""blueprint Sensor class."""
def __init__(self, hass, config):
self.hass = hass
self.attr = {}
self._state = None
self._name = config.get("name", DEFAULT_NAME)
async def async_update(self):
"""Update the sensor."""
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()
# Get new data (if any)
updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {})
# Check the data and update the value.
if updated.get("static") is None:
self._state = self._state
else:
self._state = updated.get("static")
# Set/update attributes
self.attr["attribution"] = ATTRIBUTION
self.attr["time"] = str(updated.get("time"))
self.attr["none"] = updated.get("none")
@property
def unique_id(self):
"""Return a unique ID to use for this sensor."""
return (
"0717a0cd-745c-48fd"
) # Don't hard code this, use something from the device/service.
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Blueprint",
}
@property
def name(self):
"""Return the name of the sensor."""
return self._name
return f"{DEFAULT_NAME}_{SENSOR}"
@property
def state(self):
"""Return the state of the sensor."""
return self._state
return self.coordinator.data.get("static")
@property
def icon(self):
"""Return the icon of the sensor."""
return ICON
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self.attr

View File

@ -1,72 +1,34 @@
"""Switch platform for blueprint."""
from homeassistant.components.switch import SwitchDevice
from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN_DATA, ICON, DOMAIN
from custom_components.blueprint.const import DEFAULT_NAME, DOMAIN, ICON, SWITCH
from custom_components.blueprint.entity import BlueprintEntity
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None
): # pylint: disable=unused-argument
"""Setup switch platform."""
async_add_entities([BlueprintBinarySwitch(hass, discovery_info)], True)
async def async_setup_entry(hass, config_entry, async_add_devices):
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup sensor platform."""
async_add_devices([BlueprintBinarySwitch(hass, {})], True)
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([BlueprintBinarySwitch(coordinator, entry)])
class BlueprintBinarySwitch(SwitchDevice):
class BlueprintBinarySwitch(BlueprintEntity, SwitchDevice):
"""blueprint switch class."""
def __init__(self, hass, config):
self.hass = hass
self.attr = {}
self._status = False
self._name = config.get("name", DEFAULT_NAME)
async def async_update(self):
"""Update the switch."""
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()
# Get new data (if any)
updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {})
# Check the data and update the value.
self._status = self.hass.data[DOMAIN_DATA]["client"].client.something
# Set/update attributes
self.attr["attribution"] = ATTRIBUTION
self.attr["time"] = str(updated.get("time"))
self.attr["static"] = updated.get("static")
async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument
"""Turn on the switch."""
await self.hass.data[DOMAIN_DATA]["client"].client.change_something(True)
await self.coordinator.api.async_change_something(True)
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument
"""Turn off the switch."""
await self.hass.data[DOMAIN_DATA]["client"].client.change_something(False)
@property
def unique_id(self):
"""Return a unique ID to use for this switch."""
return (
"0818a0cd-745c-48fd"
) # Don't hard code this, use something from the device/service.
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Blueprint",
}
await self.coordinator.api.async_change_something(False)
await self.coordinator.async_request_refresh()
@property
def name(self):
"""Return the name of the switch."""
return self._name
return f"{DEFAULT_NAME}_{SWITCH}"
@property
def icon(self):
@ -76,9 +38,4 @@ class BlueprintBinarySwitch(SwitchDevice):
@property
def is_on(self):
"""Return true if the switch is on."""
return self._status
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self.attr
return self.coordinator.api.something