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:
parent
33bc1f01a4
commit
ab50069918
9 changed files with 30 additions and 81 deletions
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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%]"
|
||||||
|
|
|
@ -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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue