diff --git a/CODEOWNERS b/CODEOWNERS index c6332e3933..7861871323 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu +esphome/components/ade7953_base/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu esphome/components/ads1118/* @solomondg1 diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py index 42b6c8ba24..4fc35352f9 100644 --- a/esphome/components/ade7953_base/__init__.py +++ b/esphome/components/ade7953_base/__init__.py @@ -24,6 +24,8 @@ from esphome.const import ( UNIT_WATT, ) +CODEOWNERS = ["@angelnu"] + CONF_CURRENT_A = "current_a" CONF_CURRENT_B = "current_b" CONF_ACTIVE_POWER_A = "active_power_a" diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 5ecc715ea1..9c981d3c44 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -189,7 +189,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(std::string, name) TEMPLATABLE_VALUE(uint8_t, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto name = this->name_.value(x...); auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); this->parent_->enroll_face(name, direction); @@ -200,7 +200,7 @@ template class DeleteAction : public Action, public Paren public: TEMPLATABLE_VALUE(int16_t, face_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto face_id = this->face_id_.value(x...); this->parent_->delete_face(face_id); } @@ -208,17 +208,17 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_faces(); } + void play(const Ts &...x) override { this->parent_->delete_all_faces(); } }; template class ScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->scan_face(); } + void play(const Ts &...x) override { this->parent_->scan_face(); } }; template class ResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; } // namespace esphome::hlk_fm22x diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index cba2c3fdda..011c6cc6af 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,26 +49,18 @@ template class LockCondition : public Condition { bool state_; }; -class LockLockTrigger : public Trigger<> { +template class LockStateTrigger : public Trigger<> { public: - LockLockTrigger(Lock *a_lock) { + explicit LockStateTrigger(Lock *a_lock) { a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_LOCKED) { + if (a_lock->state == State) { this->trigger(); } }); } }; -class LockUnlockTrigger : public Trigger<> { - public: - LockUnlockTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) { - this->trigger(); - } - }); - } -}; +using LockLockTrigger = LockStateTrigger; +using LockUnlockTrigger = LockStateTrigger; } // namespace esphome::lock diff --git a/tests/integration/fixtures/lock_automations.yaml b/tests/integration/fixtures/lock_automations.yaml new file mode 100644 index 0000000000..fe11e656fa --- /dev/null +++ b/tests/integration/fixtures/lock_automations.yaml @@ -0,0 +1,17 @@ +esphome: + name: lock-automations-test + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +lock: + - platform: template + id: test_lock + name: "Test Lock" + optimistic: true + on_lock: + - logger.log: "TRIGGER: on_lock fired" + on_unlock: + - logger.log: "TRIGGER: on_unlock fired" diff --git a/tests/integration/test_lock_automations.py b/tests/integration/test_lock_automations.py new file mode 100644 index 0000000000..e200a2eacd --- /dev/null +++ b/tests/integration/test_lock_automations.py @@ -0,0 +1,58 @@ +"""Integration test for lock automation triggers. + +Tests that on_lock and on_unlock triggers work correctly. +""" + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_lock_automations( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test lock on_lock and on_unlock triggers.""" + loop = asyncio.get_running_loop() + + # Futures for log line detection + on_lock_future: asyncio.Future[bool] = loop.create_future() + on_unlock_future: asyncio.Future[bool] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for trigger messages.""" + if "TRIGGER: on_lock fired" in line and not on_lock_future.done(): + on_lock_future.set_result(True) + elif "TRIGGER: on_unlock fired" in line and not on_unlock_future.done(): + on_unlock_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Import here to avoid import errors when aioesphomeapi is not installed + from aioesphomeapi import LockCommand + + # Get entities + entities = await client.list_entities_services() + lock = next(e for e in entities[0] if e.object_id == "test_lock") + + # Test 1: Lock - should trigger on_lock + client.lock_command(key=lock.key, command=LockCommand.LOCK) + + try: + await asyncio.wait_for(on_lock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_lock trigger did not fire") + + # Test 2: Unlock - should trigger on_unlock + client.lock_command(key=lock.key, command=LockCommand.UNLOCK) + + try: + await asyncio.wait_for(on_unlock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_unlock trigger did not fire")