Quality improvement on LOQED integration (#95725)

Remove generated translation
Raise error correctly
Remove obsolete consts
Remove callback, hass assignment and info log
Use name from LOQED API instead of default name
Correct entity name for assertion
This commit is contained in:
Mike Woudenberg 2023-07-03 03:52:52 +02:00 committed by GitHub
parent 33bc1f01a4
commit ab50069918
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 30 additions and 81 deletions

View file

@ -44,9 +44,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) )
cloud_client = cloud_loqed.LoqedCloudAPI(cloud_api_client) cloud_client = cloud_loqed.LoqedCloudAPI(cloud_api_client)
lock_data = await cloud_client.async_get_locks() lock_data = await cloud_client.async_get_locks()
except aiohttp.ClientError: except aiohttp.ClientError as err:
_LOGGER.error("HTTP Connection error to loqed API") _LOGGER.error("HTTP Connection error to loqed API")
raise CannotConnect from aiohttp.ClientError raise CannotConnect from err
try: try:
selected_lock = next( selected_lock = next(
@ -137,7 +137,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
else: else:
await self.async_set_unique_id( await self.async_set_unique_id(
re.sub(r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"]) re.sub(
r"LOQED-([a-f0-9]+)\.local", r"\1", info["bridge_mdns_hostname"]
),
raise_on_progress=False,
) )
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()

View file

@ -2,5 +2,3 @@
DOMAIN = "loqed" DOMAIN = "loqed"
OAUTH2_AUTHORIZE = "https://app.loqed.com/API/integration_oauth3/login.php"
OAUTH2_TOKEN = "https://app.loqed.com/API/integration_oauth3/token.php"

View file

@ -8,8 +8,8 @@ from loqedAPI import loqed
from homeassistant.components import webhook from homeassistant.components import webhook
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.const import CONF_NAME, CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
@ -79,17 +79,16 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
) -> None: ) -> None:
"""Initialize the Loqed Data Update coordinator.""" """Initialize the Loqed Data Update coordinator."""
super().__init__(hass, _LOGGER, name="Loqed sensors") super().__init__(hass, _LOGGER, name="Loqed sensors")
self._hass = hass
self._api = api self._api = api
self._entry = entry self._entry = entry
self.lock = lock self.lock = lock
self.device_name = self._entry.data[CONF_NAME]
async def _async_update_data(self) -> StatusMessage: async def _async_update_data(self) -> StatusMessage:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
async with async_timeout.timeout(10): async with async_timeout.timeout(10):
return await self._api.async_get_lock_details() return await self._api.async_get_lock_details()
@callback
async def _handle_webhook( async def _handle_webhook(
self, hass: HomeAssistant, webhook_id: str, request: Request self, hass: HomeAssistant, webhook_id: str, request: Request
) -> None: ) -> None:
@ -116,7 +115,7 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook self.hass, DOMAIN, "Loqed", webhook_id, self._handle_webhook
) )
webhook_url = webhook.async_generate_url(self.hass, webhook_id) webhook_url = webhook.async_generate_url(self.hass, webhook_id)
_LOGGER.info("Webhook URL: %s", webhook_url) _LOGGER.debug("Webhook URL: %s", webhook_url)
webhooks = await self.lock.getWebhooks() webhooks = await self.lock.getWebhooks()

View file

@ -23,7 +23,7 @@ class LoqedEntity(CoordinatorEntity[LoqedDataCoordinator]):
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, lock_id)}, identifiers={(DOMAIN, lock_id)},
manufacturer="LOQED", manufacturer="LOQED",
name="LOQED Lock", name=coordinator.device_name,
model="Touch Smart Lock", model="Touch Smart Lock",
connections={(CONNECTION_NETWORK_MAC, lock_id)}, connections={(CONNECTION_NETWORK_MAC, lock_id)},
) )

View file

@ -24,7 +24,7 @@ async def async_setup_entry(
"""Set up the Loqed lock platform.""" """Set up the Loqed lock platform."""
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([LoqedLock(coordinator, entry.data["name"])]) async_add_entities([LoqedLock(coordinator)])
class LoqedLock(LoqedEntity, LockEntity): class LoqedLock(LoqedEntity, LockEntity):
@ -32,17 +32,17 @@ class LoqedLock(LoqedEntity, LockEntity):
_attr_supported_features = LockEntityFeature.OPEN _attr_supported_features = LockEntityFeature.OPEN
def __init__(self, coordinator: LoqedDataCoordinator, name: str) -> None: def __init__(self, coordinator: LoqedDataCoordinator) -> None:
"""Initialize the lock.""" """Initialize the lock."""
super().__init__(coordinator) super().__init__(coordinator)
self._lock = coordinator.lock self._lock = coordinator.lock
self._attr_unique_id = self._lock.id self._attr_unique_id = self._lock.id
self._attr_name = name self._attr_name = None
@property @property
def changed_by(self) -> str: def changed_by(self) -> str:
"""Return internal ID of last used key.""" """Return internal ID of last used key."""
return "KeyID " + str(self._lock.last_key_id) return f"KeyID {self._lock.last_key_id}"
@property @property
def is_locking(self) -> bool | None: def is_locking(self) -> bool | None:

View file

@ -12,8 +12,7 @@
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
"unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"

View file

@ -1,22 +0,0 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"flow_title": "LOQED Touch Smartlock setup",
"step": {
"user": {
"data": {
"api_key": "API Key",
"name": "Name of your lock in the LOQED app."
},
"description": "Login at {config_url} and: \n* Create an API-key by clicking 'Create' \n* Copy the created access token."
}
}
}
}

View file

@ -15,31 +15,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture
async def test_webhook_rejects_invalid_message(
hass: HomeAssistant,
hass_client_no_auth,
integration: MockConfigEntry,
lock: loqed.Lock,
):
"""Test webhook called with invalid message."""
await async_setup_component(hass, "http", {"http": {}})
client = await hass_client_no_auth()
coordinator = hass.data[DOMAIN][integration.entry_id]
lock.receiveWebhook = AsyncMock(return_value={"error": "invalid hash"})
with patch.object(coordinator, "async_set_updated_data") as mock:
message = load_fixture("loqed/battery_update.json")
timestamp = 1653304609
await client.post(
f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}",
data=message,
headers={"timestamp": str(timestamp), "hash": "incorrect hash"},
)
mock.assert_not_called()
async def test_webhook_accepts_valid_message( async def test_webhook_accepts_valid_message(
hass: HomeAssistant, hass: HomeAssistant,
hass_client_no_auth, hass_client_no_auth,
@ -49,20 +24,17 @@ async def test_webhook_accepts_valid_message(
"""Test webhook called with valid message.""" """Test webhook called with valid message."""
await async_setup_component(hass, "http", {"http": {}}) await async_setup_component(hass, "http", {"http": {}})
client = await hass_client_no_auth() client = await hass_client_no_auth()
processed_message = json.loads(load_fixture("loqed/battery_update.json")) processed_message = json.loads(load_fixture("loqed/lock_going_to_nightlock.json"))
coordinator = hass.data[DOMAIN][integration.entry_id]
lock.receiveWebhook = AsyncMock(return_value=processed_message) lock.receiveWebhook = AsyncMock(return_value=processed_message)
with patch.object(coordinator, "async_update_listeners") as mock: message = load_fixture("loqed/battery_update.json")
message = load_fixture("loqed/battery_update.json") timestamp = 1653304609
timestamp = 1653304609 await client.post(
await client.post( f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}",
f"/api/webhook/{integration.data[CONF_WEBHOOK_ID]}", data=message,
data=message, headers={"timestamp": str(timestamp), "hash": "incorrect hash"},
headers={"timestamp": str(timestamp), "hash": "incorrect hash"}, )
) lock.receiveWebhook.assert_called()
mock.assert_called()
async def test_setup_webhook_in_bridge( async def test_setup_webhook_in_bridge(

View file

@ -21,7 +21,7 @@ async def test_lock_entity(
integration: MockConfigEntry, integration: MockConfigEntry,
) -> None: ) -> None:
"""Test the lock entity.""" """Test the lock entity."""
entity_id = "lock.loqed_lock_home" entity_id = "lock.home"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -37,7 +37,7 @@ async def test_lock_responds_to_bolt_state_updates(
lock.bolt_state = "night_lock" lock.bolt_state = "night_lock"
coordinator.async_update_listeners() coordinator.async_update_listeners()
entity_id = "lock.loqed_lock_home" entity_id = "lock.home"
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
@ -50,7 +50,7 @@ async def test_lock_transition_to_unlocked(
) -> None: ) -> None:
"""Tests the lock transitions to unlocked state.""" """Tests the lock transitions to unlocked state."""
entity_id = "lock.loqed_lock_home" entity_id = "lock.home"
await hass.services.async_call( await hass.services.async_call(
"lock", SERVICE_UNLOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True "lock", SERVICE_UNLOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True
@ -64,7 +64,7 @@ async def test_lock_transition_to_locked(
) -> None: ) -> None:
"""Tests the lock transitions to locked state.""" """Tests the lock transitions to locked state."""
entity_id = "lock.loqed_lock_home" entity_id = "lock.home"
await hass.services.async_call( await hass.services.async_call(
"lock", SERVICE_LOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True "lock", SERVICE_LOCK, {ATTR_ENTITY_ID: entity_id}, blocking=True
@ -78,7 +78,7 @@ async def test_lock_transition_to_open(
) -> None: ) -> None:
"""Tests the lock transitions to open state.""" """Tests the lock transitions to open state."""
entity_id = "lock.loqed_lock_home" entity_id = "lock.home"
await hass.services.async_call( await hass.services.async_call(
"lock", SERVICE_OPEN, {ATTR_ENTITY_ID: entity_id}, blocking=True "lock", SERVICE_OPEN, {ATTR_ENTITY_ID: entity_id}, blocking=True