mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add pglab integration (#109725)
* Add PG LAB Electronics integration * Add time from last boot sensor diagnostic * Limit the initial new pglab integration to only one platform * Update FlowHandler with the new return type ConfigFlowResult * Fix docstring file with the right integration name to PG LAB. * There is no need for default value in the callback definition. * Move all mqtt callbacks to be global and also renamed with a better name. * Removed unused member variables. * Renaming functions with a better name. * Adding miss docstring to __build_device. * Renamed CreateDiscovery with a better name. * Removing not so meaning comment. * Avoid to populate hass.data with pglab discovery information. Use hass.data[DOMAIN] instead. * Revert "Removed unused member variables." This reverts commit 4193c491ec3c31d5c589abac59028ee9be898785. * Removed unused member variables. * Refactoring of const. Be sure to have in const.py constant that are used in at least two other modules * Restoring back the process to unregister the plaform when unload the integration. * fix spelling mistake * Revert "Move all mqtt callbacks to be global and also renamed with a better name." This reverts commit d94d8010d5d11d3febfcb075859483d9e2beae3c. * Main refactoring to avoid to store PG Lab discovery in hass.data * Change class name BaseEntity in PGLabEntity. And named PyPGLab... what imported from external python module pypglab. * Avoid to use dict to create DeviceInfo * Removing unused parameter * Removing not necessary call to base class * Update entity name/id to be compatible with the new integration policy. * Upate test to new entity id * Add new line after file description * avoid to store in local variable data for calling function * Move PGLABConfigEntry in __init__.py * change function to pure callback * to avoid hang, dont' trust the split of the discovery topic... introduce a max split count * rename method with a more meaning name * use assignment operator * rename variable with a better name * removing unecessary test * Raise exception in case of unexpected error during discovery * Review comments all other the intergration. * Rename classes to be consistent in integration * Using new feature single_config_entry to allow single instance integration * rename class FlowHandler to PGLabFlowHandler * using __package__ to initialize integration logger * missing to catch the exception when for some reason is not possible to create the discovery instance. This can happen when the discovery MQTT message is not in valid json format. * using ATTR_ENTITY_ID instead of the string * using SOURCE_MQTT, SOURCE_USER instead of config_entries.SOURCE_MQTT, config_entries.SOURCE_USER * Using FlowResultType.ABORT instead of the string value * Code refactoring for tests of configuration from USER and MQTT * Remove to the user the possibility to add PGLab integration manually, and remove not needed tests. * Change test_device_update to use snapshot to check test result * Raise exeception in case of unexpected device and entity_id * Avoid to log on info channel. * Renamed _LOGGER in LOGGER * Propage the call to the base class * Remove not needed code because from the manifest it's only allows a single instance * Using specific type for result test instead of string value * Code refactoring, avoid not necessary function * update to the new way to import mqtt components * Avoid runtime check * add err variable for catching the exception * add doc string to mqtt_publish * add doc string to mqtt_subscribe * Rename DiscoverDeviceInfo.add_entity_id in add_entity * add doc string * removing not meaning documentation string * fix spelling * fix wrong case in docstring * fix spelling mistake in PyPGLab callback name * rename mqtt message received callback * Avoid to store hard coded discovery_prefix * Removing unused strings from strings.json * Give to the user more information during config_flow, and add the possibility to add manually the integration * Fix to avoid fails of auto test * update discovery test * Be sure to always subscribe to MQTT topic when entity is added to HA * Update codeowner of PGLAB integration and test * Add control to check if mqtt is available during integration setup * New test for check no state change for disable entity switch * Remore not more used file * update pypglab to version 0.0.3 and improve the symmetry to subscribe/unsubscribe to mqtt entity topic and to register/deregister the status update callback * Update codeowner of pglab integration * Adding quality_scale * removing async_setup * Fix spelling mistake * Added test to cover config_flow.async_step_user --------- Co-authored-by: Pierluigi <p.garaventa@gmail.com>
This commit is contained in:
parent
15223b3679
commit
8c602d74f3
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1138,6 +1138,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/permobil/ @IsakNyberg
|
||||
/homeassistant/components/persistent_notification/ @home-assistant/core
|
||||
/tests/components/persistent_notification/ @home-assistant/core
|
||||
/homeassistant/components/pglab/ @pglab-electronics
|
||||
/tests/components/pglab/ @pglab-electronics
|
||||
/homeassistant/components/philips_js/ @elupus
|
||||
/tests/components/philips_js/ @elupus
|
||||
/homeassistant/components/pi_hole/ @shenxn
|
||||
|
85
homeassistant/components/pglab/__init__.py
Normal file
85
homeassistant/components/pglab/__init__.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""PG LAB Electronics integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pypglab.mqtt import (
|
||||
Client as PyPGLabMqttClient,
|
||||
Sub_State as PyPGLabSubState,
|
||||
Subcribe_CallBack as PyPGLabSubscribeCallBack,
|
||||
)
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.mqtt import (
|
||||
ReceiveMessage,
|
||||
async_prepare_subscribe_topics,
|
||||
async_subscribe_topics,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .discovery import PGLabDiscovery
|
||||
|
||||
type PGLABConfigEntry = ConfigEntry[PGLabDiscovery]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PGLABConfigEntry) -> bool:
|
||||
"""Set up PG LAB Electronics integration from a config entry."""
|
||||
|
||||
async def mqtt_publish(topic: str, payload: str, qos: int, retain: bool) -> None:
|
||||
"""Publish an MQTT message using the Home Assistant MQTT client."""
|
||||
await mqtt.async_publish(hass, topic, payload, qos, retain)
|
||||
|
||||
async def mqtt_subscribe(
|
||||
sub_state: PyPGLabSubState, topic: str, callback_func: PyPGLabSubscribeCallBack
|
||||
) -> PyPGLabSubState:
|
||||
"""Subscribe to MQTT topics using the Home Assistant MQTT client."""
|
||||
|
||||
@callback
|
||||
def mqtt_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle PGLab mqtt messages."""
|
||||
callback_func(msg.topic, msg.payload)
|
||||
|
||||
topics = {
|
||||
"pglab_subscribe_topic": {
|
||||
"topic": topic,
|
||||
"msg_callback": mqtt_message_received,
|
||||
}
|
||||
}
|
||||
|
||||
sub_state = async_prepare_subscribe_topics(hass, sub_state, topics)
|
||||
await async_subscribe_topics(hass, sub_state)
|
||||
return sub_state
|
||||
|
||||
async def mqtt_unsubscribe(sub_state: PyPGLabSubState) -> None:
|
||||
async_unsubscribe_topics(hass, sub_state)
|
||||
|
||||
if not await mqtt.async_wait_for_mqtt_client(hass):
|
||||
LOGGER.error("MQTT integration not available")
|
||||
raise ConfigEntryNotReady("MQTT integration not available")
|
||||
|
||||
# Create an MQTT client for PGLab used for PGLab python module.
|
||||
pglab_mqtt = PyPGLabMqttClient(mqtt_publish, mqtt_subscribe, mqtt_unsubscribe)
|
||||
|
||||
# Setup PGLab device discovery.
|
||||
entry.runtime_data = PGLabDiscovery()
|
||||
|
||||
# Start to discovery PG Lab devices.
|
||||
await entry.runtime_data.start(hass, pglab_mqtt, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PGLABConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
# Stop PGLab device discovery.
|
||||
pglab_discovery = entry.runtime_data
|
||||
await pglab_discovery.stop(hass, entry)
|
||||
|
||||
return True
|
73
homeassistant/components/pglab/config_flow.py
Normal file
73
homeassistant/components/pglab/config_flow.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""Config flow for PG LAB Electronics integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
|
||||
|
||||
from .const import DISCOVERY_TOPIC, DOMAIN
|
||||
|
||||
|
||||
class PGLabFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_mqtt(
|
||||
self, discovery_info: MqttServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by MQTT discovery."""
|
||||
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
|
||||
# Validate the message, abort if it fails.
|
||||
if not discovery_info.topic.endswith("/config"):
|
||||
# Not a PGLab Electronics discovery message.
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
if not discovery_info.payload:
|
||||
# Empty payload, unexpected payload.
|
||||
return self.async_abort(reason="invalid_discovery_info")
|
||||
|
||||
return await self.async_step_confirm_from_mqtt()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
try:
|
||||
if not mqtt.is_connected(self.hass):
|
||||
return self.async_abort(reason="mqtt_not_connected")
|
||||
except KeyError:
|
||||
return self.async_abort(reason="mqtt_not_configured")
|
||||
|
||||
return await self.async_step_confirm_from_user()
|
||||
|
||||
def step_confirm(
|
||||
self, step_id: str, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title="PG LAB Electronics",
|
||||
data={
|
||||
"discovery_prefix": DISCOVERY_TOPIC,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id=step_id)
|
||||
|
||||
async def async_step_confirm_from_mqtt(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup from MQTT discovered."""
|
||||
return self.step_confirm(step_id="confirm_from_mqtt", user_input=user_input)
|
||||
|
||||
async def async_step_confirm_from_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup from user add integration."""
|
||||
return self.step_confirm(step_id="confirm_from_user", user_input=user_input)
|
12
homeassistant/components/pglab/const.py
Normal file
12
homeassistant/components/pglab/const.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""Constants used by PG LAB Electronics integration."""
|
||||
|
||||
import logging
|
||||
|
||||
# The domain of the integration.
|
||||
DOMAIN = "pglab"
|
||||
|
||||
# The message logger.
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
# The MQTT message used to subscribe to get a new PG LAB device.
|
||||
DISCOVERY_TOPIC = "pglab/discovery"
|
277
homeassistant/components/pglab/discovery.py
Normal file
277
homeassistant/components/pglab/discovery.py
Normal file
@ -0,0 +1,277 @@
|
||||
"""Discovery PG LAB Electronics devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pypglab.device import Device as PyPGLabDevice
|
||||
from pypglab.mqtt import Client as PyPGLabMqttClient
|
||||
|
||||
from homeassistant.components.mqtt import (
|
||||
EntitySubscription,
|
||||
ReceiveMessage,
|
||||
async_prepare_subscribe_topics,
|
||||
async_subscribe_topics,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DISCOVERY_TOPIC, DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import PGLABConfigEntry
|
||||
|
||||
# Supported platforms.
|
||||
PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
# Used to create a new component entity.
|
||||
CREATE_NEW_ENTITY = {
|
||||
Platform.SWITCH: "pglab_create_new_entity_switch",
|
||||
}
|
||||
|
||||
|
||||
class PGLabDiscoveryError(Exception):
|
||||
"""Raised when a discovery has failed."""
|
||||
|
||||
|
||||
def get_device_id_from_discovery_topic(topic: str) -> str | None:
|
||||
"""From the discovery topic get the PG LAB Electronics device id."""
|
||||
|
||||
# The discovery topic has the following format "pglab/discovery/[Device ID]/config"
|
||||
split_topic = topic.split("/", 5)
|
||||
|
||||
# Do a sanity check on the string.
|
||||
if len(split_topic) != 4:
|
||||
return None
|
||||
|
||||
if split_topic[3] != "config":
|
||||
return None
|
||||
|
||||
return split_topic[2]
|
||||
|
||||
|
||||
class DiscoverDeviceInfo:
|
||||
"""Keeps information of the PGLab discovered device."""
|
||||
|
||||
def __init__(self, pglab_device: PyPGLabDevice) -> None:
|
||||
"""Initialize the device discovery info."""
|
||||
|
||||
# Hash string represents the devices actual configuration,
|
||||
# it depends on the number of available relays and shutters.
|
||||
# When the hash string changes the devices entities must be rebuilt.
|
||||
self._hash = pglab_device.hash
|
||||
self._entities: list[tuple[str, str]] = []
|
||||
|
||||
def add_entity(self, entity: Entity) -> None:
|
||||
"""Add an entity."""
|
||||
|
||||
# PGLabEntity always have unique IDs
|
||||
if TYPE_CHECKING:
|
||||
assert entity.unique_id is not None
|
||||
self._entities.append((entity.platform.domain, entity.unique_id))
|
||||
|
||||
@property
|
||||
def hash(self) -> int:
|
||||
"""Return the hash for this configuration."""
|
||||
return self._hash
|
||||
|
||||
@property
|
||||
def entities(self) -> list[tuple[str, str]]:
|
||||
"""Return array of entities available."""
|
||||
return self._entities
|
||||
|
||||
|
||||
@dataclass
|
||||
class PGLabDiscovery:
|
||||
"""Discovery a PGLab device with the following MQTT topic format pglab/discovery/[device]/config."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the discovery class."""
|
||||
self._substate: dict[str, EntitySubscription] = {}
|
||||
self._discovery_topic = DISCOVERY_TOPIC
|
||||
self._mqtt_client = None
|
||||
self._discovered: dict[str, DiscoverDeviceInfo] = {}
|
||||
self._disconnect_platform: list = []
|
||||
|
||||
async def __build_device(
|
||||
self, mqtt: PyPGLabMqttClient, msg: ReceiveMessage
|
||||
) -> PyPGLabDevice:
|
||||
"""Build a PGLab device."""
|
||||
|
||||
# Check if the discovery message is in valid json format.
|
||||
try:
|
||||
payload = json.loads(msg.payload)
|
||||
except ValueError as err:
|
||||
raise PGLabDiscoveryError(
|
||||
f"Can't decode discovery payload: {msg.payload!r}"
|
||||
) from err
|
||||
|
||||
device_id = "id"
|
||||
|
||||
# Check if the key id is present in the payload. It must always be present.
|
||||
if device_id not in payload:
|
||||
raise PGLabDiscoveryError(
|
||||
"Unexpected discovery payload format, id key not present"
|
||||
)
|
||||
|
||||
# Do a sanity check: the id must match the discovery topic /pglab/discovery/[id]/config
|
||||
topic = msg.topic
|
||||
if not topic.endswith(f"{payload[device_id]}/config"):
|
||||
raise PGLabDiscoveryError("Unexpected discovery topic format")
|
||||
|
||||
# Build and configure the PGLab device.
|
||||
pglab_device = PyPGLabDevice()
|
||||
if not await pglab_device.config(mqtt, payload):
|
||||
raise PGLabDiscoveryError("Error during setup of a new discovered device")
|
||||
|
||||
return pglab_device
|
||||
|
||||
def __clean_discovered_device(self, hass: HomeAssistant, device_id: str) -> None:
|
||||
"""Destroy the device and any entities connected to the device."""
|
||||
|
||||
if device_id not in self._discovered:
|
||||
return
|
||||
|
||||
discovery_info = self._discovered[device_id]
|
||||
|
||||
# Destroy all entities connected to the device.
|
||||
entity_registry = er.async_get(hass)
|
||||
for platform, unique_id in discovery_info.entities:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
platform, DOMAIN, unique_id
|
||||
):
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
# Destroy the device.
|
||||
device_registry = dr.async_get(hass)
|
||||
if device_entry := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, device_id)}
|
||||
):
|
||||
device_registry.async_remove_device(device_entry.id)
|
||||
|
||||
# Clean the discovery info.
|
||||
del self._discovered[device_id]
|
||||
|
||||
async def start(
|
||||
self, hass: HomeAssistant, mqtt: PyPGLabMqttClient, entry: PGLABConfigEntry
|
||||
) -> None:
|
||||
"""Start discovering a PGLab devices."""
|
||||
|
||||
async def discovery_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Received a new discovery message."""
|
||||
|
||||
# Create a PGLab device and add entities.
|
||||
try:
|
||||
pglab_device = await self.__build_device(mqtt, msg)
|
||||
except PGLabDiscoveryError as err:
|
||||
LOGGER.warning("Can't create PGLabDiscovery instance(%s) ", str(err))
|
||||
|
||||
# For some reason it's not possible to create the device with the discovery message,
|
||||
# be sure that any previous device with the same topic is now destroyed.
|
||||
device_id = get_device_id_from_discovery_topic(msg.topic)
|
||||
|
||||
# If there is a valid topic device_id clean everything relative to the device.
|
||||
if device_id:
|
||||
self.__clean_discovered_device(hass, device_id)
|
||||
|
||||
return
|
||||
|
||||
# Create a new device.
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
configuration_url=f"http://{pglab_device.ip}/",
|
||||
connections={(CONNECTION_NETWORK_MAC, pglab_device.mac)},
|
||||
identifiers={(DOMAIN, pglab_device.id)},
|
||||
manufacturer=pglab_device.manufactor,
|
||||
model=pglab_device.type,
|
||||
name=pglab_device.name,
|
||||
sw_version=pglab_device.firmware_version,
|
||||
hw_version=pglab_device.hardware_version,
|
||||
)
|
||||
|
||||
# Do some checking if previous entities must be updated.
|
||||
if pglab_device.id in self._discovered:
|
||||
# The device is already been discovered,
|
||||
# get the old discovery info data.
|
||||
discovery_info = self._discovered[pglab_device.id]
|
||||
|
||||
if discovery_info.hash == pglab_device.hash:
|
||||
# Best case, there is nothing to do.
|
||||
# The device is still in the same configuration. Same name, same shutters, same relay etc.
|
||||
return
|
||||
|
||||
LOGGER.warning(
|
||||
"Changed internal configuration of device(%s). Rebuilding all entities",
|
||||
pglab_device.id,
|
||||
)
|
||||
|
||||
# Something has changed, all previous entities must be destroyed and re-created.
|
||||
self.__clean_discovered_device(hass, pglab_device.id)
|
||||
|
||||
# Add a new device.
|
||||
discovery_info = DiscoverDeviceInfo(pglab_device)
|
||||
self._discovered[pglab_device.id] = discovery_info
|
||||
|
||||
# Create all new relay entities.
|
||||
for r in pglab_device.relays:
|
||||
# The HA entity is not yet created, send a message to create it.
|
||||
async_dispatcher_send(
|
||||
hass, CREATE_NEW_ENTITY[Platform.SWITCH], pglab_device, r
|
||||
)
|
||||
|
||||
topics = {
|
||||
"discovery_topic": {
|
||||
"topic": f"{self._discovery_topic}/#",
|
||||
"msg_callback": discovery_message_received,
|
||||
}
|
||||
}
|
||||
|
||||
# Forward setup all HA supported platforms.
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
self._mqtt_client = mqtt
|
||||
self._substate = async_prepare_subscribe_topics(hass, self._substate, topics)
|
||||
await async_subscribe_topics(hass, self._substate)
|
||||
|
||||
async def register_platform(
|
||||
self, hass: HomeAssistant, platform: Platform, target: Callable[..., Any]
|
||||
):
|
||||
"""Register a callback to create entity of a specific HA platform."""
|
||||
disconnect_callback = async_dispatcher_connect(
|
||||
hass, CREATE_NEW_ENTITY[platform], target
|
||||
)
|
||||
self._disconnect_platform.append(disconnect_callback)
|
||||
|
||||
async def stop(self, hass: HomeAssistant, entry: PGLABConfigEntry) -> None:
|
||||
"""Stop to discovery PG LAB devices."""
|
||||
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
# Disconnect all registered platforms.
|
||||
for disconnect_callback in self._disconnect_platform:
|
||||
disconnect_callback()
|
||||
|
||||
async_unsubscribe_topics(hass, self._substate)
|
||||
|
||||
async def add_entity(self, entity: Entity, device_id: str):
|
||||
"""Save a new PG LAB device entity."""
|
||||
|
||||
# Be sure that the device is been discovered.
|
||||
if device_id not in self._discovered:
|
||||
raise PGLabDiscoveryError("Unknown device, device_id not discovered")
|
||||
|
||||
discovery_info = self._discovered[device_id]
|
||||
discovery_info.add_entity(entity)
|
70
homeassistant/components/pglab/entity.py
Normal file
70
homeassistant/components/pglab/entity.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""Entity for PG LAB Electronics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pypglab.device import Device as PyPGLabDevice
|
||||
from pypglab.entity import Entity as PyPGLabEntity
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .discovery import PGLabDiscovery
|
||||
|
||||
|
||||
class PGLabEntity(Entity):
|
||||
"""Representation of a PGLab entity in Home Assistant."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
discovery: PGLabDiscovery,
|
||||
device: PyPGLabDevice,
|
||||
entity: PyPGLabEntity,
|
||||
) -> None:
|
||||
"""Initialize the class."""
|
||||
|
||||
self._id = entity.id
|
||||
self._device_id = device.id
|
||||
self._entity = entity
|
||||
self._discovery = discovery
|
||||
|
||||
# Information about the device that is partially visible in the UI.
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.id)},
|
||||
name=device.name,
|
||||
sw_version=device.firmware_version,
|
||||
hw_version=device.hardware_version,
|
||||
model=device.type,
|
||||
manufacturer=device.manufactor,
|
||||
configuration_url=f"http://{device.ip}/",
|
||||
connections={(CONNECTION_NETWORK_MAC, device.mac)},
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Update the device discovery info."""
|
||||
|
||||
self._entity.set_on_state_callback(self.state_updated)
|
||||
await self._entity.subscribe_topics()
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Inform PGLab discovery instance that a new entity is available.
|
||||
# This is important to know in case the device needs to be reconfigured
|
||||
# and the entity can be potentially destroyed.
|
||||
await self._discovery.add_entity(self, self._device_id)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Unsubscribe when removed."""
|
||||
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
await self._entity.unsubscribe_topics()
|
||||
self._entity.set_on_state_callback(None)
|
||||
|
||||
@callback
|
||||
def state_updated(self, payload: str) -> None:
|
||||
"""Handle state updates."""
|
||||
self.async_write_ha_state()
|
14
homeassistant/components/pglab/manifest.json
Normal file
14
homeassistant/components/pglab/manifest.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"domain": "pglab",
|
||||
"name": "PG LAB Electronics",
|
||||
"codeowners": ["@pglab-electronics"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["mqtt"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/pglab",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pglab"],
|
||||
"mqtt": ["pglab/discovery/#"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pypglab==0.0.3"],
|
||||
"single_config_entry": true
|
||||
}
|
80
homeassistant/components/pglab/quality_scale.yaml
Normal file
80
homeassistant/components/pglab/quality_scale.yaml
Normal file
@ -0,0 +1,80 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: The integration does not provide any additional actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: The integration does not poll.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: The integration does not provide any additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure:
|
||||
status: exempt
|
||||
comment: The integration relies solely on auto-discovery.
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: The integration does not provide any additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No options flow.
|
||||
docs-installation-parameters:
|
||||
status: exempt
|
||||
comment: There are no parameters.
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: The integration does not require authentication.
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: The integration has no settings.
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: The integration does not make HTTP requests.
|
||||
strict-typing: todo
|
24
homeassistant/components/pglab/strings.json
Normal file
24
homeassistant/components/pglab/strings.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm_from_user": {
|
||||
"description": "In order to be found PG LAB Electronics devices need to be connected to the same broker as the Home Assistant MQTT integration client. Do you want to continue?"
|
||||
},
|
||||
"confirm_from_mqtt": {
|
||||
"description": "Do you want to set up PG LAB Electronics?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"mqtt_not_connected": "Home Assistant MQTT integration not connected to MQTT broker.",
|
||||
"mqtt_not_configured": "Home Assistant MQTT integration not configured."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"switch": {
|
||||
"relay": {
|
||||
"name": "Relay {relay_id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
homeassistant/components/pglab/switch.py
Normal file
76
homeassistant/components/pglab/switch.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Switch for PG LAB Electronics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pypglab.device import Device as PyPGLabDevice
|
||||
from pypglab.relay import Relay as PyPGLabRelay
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import PGLABConfigEntry
|
||||
from .discovery import PGLabDiscovery
|
||||
from .entity import PGLabEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: PGLABConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up switches for device."""
|
||||
|
||||
@callback
|
||||
def async_discover(pglab_device: PyPGLabDevice, pglab_relay: PyPGLabRelay) -> None:
|
||||
"""Discover and add a PGLab Relay."""
|
||||
pglab_discovery = config_entry.runtime_data
|
||||
pglab_switch = PGLabSwitch(pglab_discovery, pglab_device, pglab_relay)
|
||||
async_add_entities([pglab_switch])
|
||||
|
||||
# Register the callback to create the switch entity when discovered.
|
||||
pglab_discovery = config_entry.runtime_data
|
||||
await pglab_discovery.register_platform(hass, Platform.SWITCH, async_discover)
|
||||
|
||||
|
||||
class PGLabSwitch(PGLabEntity, SwitchEntity):
|
||||
"""A PGLab switch."""
|
||||
|
||||
_attr_translation_key = "relay"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pglab_discovery: PGLabDiscovery,
|
||||
pglab_device: PyPGLabDevice,
|
||||
pglab_relay: PyPGLabRelay,
|
||||
) -> None:
|
||||
"""Initialize the Switch class."""
|
||||
|
||||
super().__init__(
|
||||
discovery=pglab_discovery,
|
||||
device=pglab_device,
|
||||
entity=pglab_relay,
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{pglab_device.id}_relay{pglab_relay.id}"
|
||||
self._attr_translation_placeholders = {"relay_id": pglab_relay.id}
|
||||
|
||||
self._relay = pglab_relay
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
await self._relay.turn_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self._relay.turn_off()
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on."""
|
||||
return self._relay.state
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -467,6 +467,7 @@ FLOWS = {
|
||||
"peco",
|
||||
"pegel_online",
|
||||
"permobil",
|
||||
"pglab",
|
||||
"philips_js",
|
||||
"pi_hole",
|
||||
"picnic",
|
||||
|
@ -4739,6 +4739,13 @@
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "opower"
|
||||
},
|
||||
"pglab": {
|
||||
"name": "PG LAB Electronics",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"single_config_entry": true
|
||||
},
|
||||
"philips": {
|
||||
"name": "Philips",
|
||||
"integrations": {
|
||||
|
3
homeassistant/generated/mqtt.py
generated
3
homeassistant/generated/mqtt.py
generated
@ -16,6 +16,9 @@ MQTT = {
|
||||
"fully_kiosk": [
|
||||
"fully/deviceInfo/+",
|
||||
],
|
||||
"pglab": [
|
||||
"pglab/discovery/#",
|
||||
],
|
||||
"qbus": [
|
||||
"cloudapp/QBUSMQTTGW/state",
|
||||
"cloudapp/QBUSMQTTGW/config",
|
||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -2207,6 +2207,9 @@ pypca==0.0.7
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.5
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.3
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -1800,6 +1800,9 @@ pypalazzetti==0.1.19
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.5
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.3
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
|
||||
|
1
tests/components/pglab/__init__.py
Normal file
1
tests/components/pglab/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the PG LAB Electronics integration."""
|
41
tests/components/pglab/conftest.py
Normal file
41
tests/components/pglab/conftest.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Common fixtures for the PG LAB Electronics tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.pglab.const import DISCOVERY_TOPIC, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, mock_device_registry, mock_registry
|
||||
|
||||
CONF_DISCOVERY_PREFIX = "discovery_prefix"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_reg(hass: HomeAssistant):
|
||||
"""Return an empty, loaded, registry."""
|
||||
return mock_device_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entity_reg(hass: HomeAssistant):
|
||||
"""Return an empty, loaded, registry."""
|
||||
return mock_registry(hass)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def setup_pglab(hass: HomeAssistant):
|
||||
"""Set up PG LAB Electronics."""
|
||||
hass.config.components.add("pglab")
|
||||
|
||||
entry = MockConfigEntry(
|
||||
data={CONF_DISCOVERY_PREFIX: DISCOVERY_TOPIC},
|
||||
domain=DOMAIN,
|
||||
title="PG LAB Electronics",
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "pglab" in hass.config.components
|
133
tests/components/pglab/test_config_flow.py
Normal file
133
tests/components/pglab/test_config_flow.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""Test the PG LAB Electronics config flow."""
|
||||
|
||||
from homeassistant.components.mqtt import MQTT_CONNECTION_STATE
|
||||
from homeassistant.components.pglab.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_MQTT, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import MqttMockHAClient
|
||||
|
||||
|
||||
async def test_mqtt_config_single_instance(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
|
||||
) -> None:
|
||||
"""Test MQTT flow aborts when an entry already exist."""
|
||||
|
||||
MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_MQTT}
|
||||
)
|
||||
|
||||
# Be sure that result is abort. Only single instance is allowed.
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None:
|
||||
"""Test we can finish a config flow through MQTT with custom prefix."""
|
||||
discovery_info = MqttServiceInfo(
|
||||
topic="pglab/discovery/E-Board-DD53AC85/config",
|
||||
payload=(
|
||||
'{"ip":"192.168.1.16", "mac":"80:34:28:1B:18:5A", "name":"e-board-office",'
|
||||
'"hw":"255.255.255", "fw":"255.255.255", "type":"E-Board", "id":"E-Board-DD53AC85",'
|
||||
'"manufacturer":"PG LAB Electronics", "params":{"shutters":0, "boards":"10000000" } }'
|
||||
),
|
||||
qos=0,
|
||||
retain=False,
|
||||
subscribed_topic="pglab/discovery/#",
|
||||
timestamp=None,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {"discovery_prefix": "pglab/discovery"}
|
||||
|
||||
|
||||
async def test_mqtt_abort_invalid_topic(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
|
||||
) -> None:
|
||||
"""Check MQTT flow aborts if discovery topic is invalid."""
|
||||
discovery_info = MqttServiceInfo(
|
||||
topic="pglab/discovery/E-Board-DD53AC85/wrong_topic",
|
||||
payload=(
|
||||
'{"ip":"192.168.1.16", "mac":"80:34:28:1B:18:5A", "name":"e-board-office",'
|
||||
'"hw":"255.255.255", "fw":"255.255.255", "type":"E-Board", "id":"E-Board-DD53AC85",'
|
||||
'"manufacturer":"PG LAB Electronics", "params":{"shutters":0, "boards":"10000000" } }'
|
||||
),
|
||||
qos=0,
|
||||
retain=False,
|
||||
subscribed_topic="pglab/discovery/#",
|
||||
timestamp=None,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_discovery_info"
|
||||
|
||||
discovery_info = MqttServiceInfo(
|
||||
topic="pglab/discovery/E-Board-DD53AC85/config",
|
||||
payload="",
|
||||
qos=0,
|
||||
retain=False,
|
||||
subscribed_topic="pglab/discovery/#",
|
||||
timestamp=None,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_MQTT}, data=discovery_info
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_discovery_info"
|
||||
|
||||
|
||||
async def test_user_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> None:
|
||||
"""Test if the user can finish a config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
"discovery_prefix": "pglab/discovery",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_setup_mqtt_not_connected(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient
|
||||
) -> None:
|
||||
"""Test that the user setup is aborted when MQTT is not connected."""
|
||||
|
||||
mqtt_mock.connected = False
|
||||
async_dispatcher_send(hass, MQTT_CONNECTION_STATE, False)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "mqtt_not_connected"
|
||||
|
||||
|
||||
async def test_user_setup_mqtt_not_configured(hass: HomeAssistant) -> None:
|
||||
"""Test that the user setup is aborted when MQTT is not configured."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "mqtt_not_configured"
|
154
tests/components/pglab/test_discovery.py
Normal file
154
tests/components/pglab/test_discovery.py
Normal file
@ -0,0 +1,154 @@
|
||||
"""The tests for the PG LAB Electronics discovery device."""
|
||||
|
||||
import json
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from tests.common import async_fire_mqtt_message
|
||||
from tests.typing import MqttMockHAClient
|
||||
|
||||
|
||||
async def test_device_discover(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
device_reg,
|
||||
entity_reg,
|
||||
setup_pglab,
|
||||
) -> None:
|
||||
"""Test setting up a device."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "11000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify device and registry entries are created
|
||||
device_entry = device_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])}
|
||||
)
|
||||
assert device_entry is not None
|
||||
assert device_entry.configuration_url == f"http://{payload['ip']}/"
|
||||
assert device_entry.manufacturer == "PG LAB Electronics"
|
||||
assert device_entry.model == payload["type"]
|
||||
assert device_entry.name == payload["name"]
|
||||
assert device_entry.sw_version == payload["fw"]
|
||||
|
||||
|
||||
async def test_device_update(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
device_reg,
|
||||
entity_reg,
|
||||
setup_pglab,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test update a device."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "11000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify device is created
|
||||
device_entry = device_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])}
|
||||
)
|
||||
assert device_entry is not None
|
||||
|
||||
# update device
|
||||
payload["fw"] = "1.0.1"
|
||||
payload["hw"] = "1.0.8"
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify device is created
|
||||
device_entry = device_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])}
|
||||
)
|
||||
assert device_entry is not None
|
||||
assert device_entry.sw_version == "1.0.1"
|
||||
assert device_entry.hw_version == "1.0.8"
|
||||
|
||||
|
||||
async def test_device_remove(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
device_reg,
|
||||
entity_reg,
|
||||
setup_pglab,
|
||||
) -> None:
|
||||
"""Test remove a device."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "11000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify device is created
|
||||
device_entry = device_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])}
|
||||
)
|
||||
assert device_entry is not None
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
"",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify device entry is removed
|
||||
device_entry = device_reg.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, payload["mac"])}
|
||||
)
|
||||
assert device_entry is None
|
1
tests/components/pglab/test_init.py
Normal file
1
tests/components/pglab/test_init.py
Normal file
@ -0,0 +1 @@
|
||||
"""Test the PG LAB Electronics integration."""
|
318
tests/components/pglab/test_switch.py
Normal file
318
tests/components/pglab/test_switch.py
Normal file
@ -0,0 +1,318 @@
|
||||
"""The tests for the PG LAB Electronics switch."""
|
||||
|
||||
from datetime import timedelta
|
||||
import json
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ASSUMED_STATE,
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_mqtt_message, async_fire_time_changed
|
||||
from tests.typing import MqttMockHAClient
|
||||
|
||||
|
||||
async def call_service(hass: HomeAssistant, entity_id, service, **kwargs):
|
||||
"""Call a service."""
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id, **kwargs},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_available_relay(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab
|
||||
) -> None:
|
||||
"""Check if relay are properly created when two E-Relay boards are connected."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "11000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for i in range(16):
|
||||
state = hass.states.get(f"switch.test_relay_{i}")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
|
||||
|
||||
async def test_change_state_via_mqtt(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab
|
||||
) -> None:
|
||||
"""Test state update via MQTT."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "10000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Simulate response from the device
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
|
||||
# Turn relay OFF
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "OFF")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Turn relay ON
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# Turn relay OFF
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "OFF")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Turn relay ON
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_mqtt_state_by_calling_service(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab
|
||||
) -> None:
|
||||
"""Calling service to turn ON/OFF relay and check mqtt state."""
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "10000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Turn relay ON
|
||||
await call_service(hass, "switch.test_relay_0", SERVICE_TURN_ON)
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
"pglab/test/relay/0/set", "ON", 0, False
|
||||
)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
# Turn relay OFF
|
||||
await call_service(hass, "switch.test_relay_0", SERVICE_TURN_OFF)
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
"pglab/test/relay/0/set", "OFF", 0, False
|
||||
)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
# Turn relay ON
|
||||
await call_service(hass, "switch.test_relay_3", SERVICE_TURN_ON)
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
"pglab/test/relay/3/set", "ON", 0, False
|
||||
)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
# Turn relay OFF
|
||||
await call_service(hass, "switch.test_relay_3", SERVICE_TURN_OFF)
|
||||
mqtt_mock.async_publish.assert_called_once_with(
|
||||
"pglab/test/relay/3/set", "OFF", 0, False
|
||||
)
|
||||
mqtt_mock.async_publish.reset_mock()
|
||||
|
||||
|
||||
async def test_discovery_update(
|
||||
hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_pglab
|
||||
) -> None:
|
||||
"""Update discovery message and check if relay are property updated."""
|
||||
|
||||
# publish the first discovery message
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "first_test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "10000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# test the available relay in the first configuration
|
||||
for i in range(8):
|
||||
state = hass.states.get(f"switch.first_test_relay_{i}")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
|
||||
# prepare a new message ... the same device but renamed
|
||||
# and with different relay configuration
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "second_test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "11000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# be sure that old relay are been removed
|
||||
for i in range(8):
|
||||
assert not hass.states.get(f"switch.first_test_relay_{i}")
|
||||
|
||||
# check new relay
|
||||
for i in range(16):
|
||||
state = hass.states.get(f"switch.second_test_relay_{i}")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
|
||||
|
||||
async def test_disable_entity_state_change_via_mqtt(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mqtt_mock: MqttMockHAClient,
|
||||
setup_pglab,
|
||||
) -> None:
|
||||
"""Test state update via MQTT of disable entity."""
|
||||
|
||||
topic = "pglab/discovery/E-Board-DD53AC85/config"
|
||||
payload = {
|
||||
"ip": "192.168.1.16",
|
||||
"mac": "80:34:28:1B:18:5A",
|
||||
"name": "test",
|
||||
"hw": "1.0.7",
|
||||
"fw": "1.0.0",
|
||||
"type": "E-Board",
|
||||
"id": "E-Board-DD53AC85",
|
||||
"manufacturer": "PG LAB Electronics",
|
||||
"params": {"shutters": 0, "boards": "10000000"},
|
||||
}
|
||||
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Be sure that the entity relay_0 is available
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||
|
||||
# Disable entity relay_0
|
||||
new_status = entity_registry.async_update_entity(
|
||||
"switch.test_relay_0", disabled_by=er.RegistryEntryDisabler.USER
|
||||
)
|
||||
|
||||
# Be sure that the entity is disabled
|
||||
assert new_status.disabled is True
|
||||
|
||||
# Try to change the state of the disabled relay_0
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Enable entity relay_0
|
||||
new_status = entity_registry.async_update_entity(
|
||||
"switch.test_relay_0", disabled_by=None
|
||||
)
|
||||
|
||||
# Be sure that the entity is enabled
|
||||
assert new_status.disabled is False
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow()
|
||||
+ timedelta(seconds=config_entries.RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Re-send the discovery message
|
||||
async_fire_mqtt_message(
|
||||
hass,
|
||||
topic,
|
||||
json.dumps(payload),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Be sure that the state is not changed
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
# Try again to change the state of the disabled relay_0
|
||||
async_fire_mqtt_message(hass, "pglab/test/relay/0/state", "ON")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Be sure that the state is been updated
|
||||
state = hass.states.get("switch.test_relay_0")
|
||||
assert state.state == STATE_ON
|
Loading…
x
Reference in New Issue
Block a user