mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
[template][time] Add template time platform (#14838)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
co-authored by
clydebarrow
parent
b8af6cb005
commit
ea4754e798
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
TemplateRealTimeClock = template_ns.class_("TemplateRealTimeClock", time_.RealTimeClock)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TemplateRealTimeClock),
|
||||
cv.Required(CONF_LAMBDA): cv.returning_lambda,
|
||||
cv.Optional(time_.CONF_ON_TIME_SYNC): cv.invalid(
|
||||
"on_time_sync is not supported for template time, use on the source clock instead."
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("never"))
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
|
||||
template_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.int64)
|
||||
)
|
||||
cg.add(var.set_template(template_))
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "template_real_time_clock.h"
|
||||
|
||||
namespace esphome::template_ {
|
||||
|
||||
static const char *const TAG = "template.time";
|
||||
|
||||
time_t TemplateRealTimeClock::timestamp_now() {
|
||||
auto val = this->f_.call();
|
||||
if (val.has_value()) {
|
||||
auto value = *val;
|
||||
if (value < 0 || value > std::numeric_limits<time_t>::max()) {
|
||||
ESP_LOGW(TAG, "timestamp value out of range: %lld", static_cast<long long>(value));
|
||||
return 0;
|
||||
}
|
||||
return static_cast<time_t>(value);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace esphome::template_
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome::template_ {
|
||||
|
||||
class TemplateRealTimeClock : public time::RealTimeClock {
|
||||
public:
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
/// The time is computed on demand and never written to the system clock, so there is nothing to poll.
|
||||
void update() override {}
|
||||
|
||||
/// Returns 0 (an invalid time) if the lambda gives no value.
|
||||
time_t timestamp_now() override;
|
||||
|
||||
protected:
|
||||
TemplateLambda<int64_t> f_;
|
||||
};
|
||||
|
||||
} // namespace esphome::template_
|
||||
@@ -29,7 +29,7 @@ class RealTimeClock : public PollingComponent {
|
||||
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
|
||||
|
||||
/// Get the current time as the UTC epoch since January 1st 1970.
|
||||
time_t timestamp_now() { return ::time(nullptr); }
|
||||
virtual time_t timestamp_now() { return ::time(nullptr); }
|
||||
|
||||
template<typename F> void add_on_time_sync_callback(F &&callback) {
|
||||
this->time_sync_callback_.add(std::forward<F>(callback));
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
time:
|
||||
- platform: sntp # Required for datetime
|
||||
id: sntp_time
|
||||
- platform: template
|
||||
id: template_offset_time
|
||||
lambda: |-
|
||||
return id(sntp_time).timestamp_now() + 3600;
|
||||
|
||||
wifi: # Required for sntp time
|
||||
ap:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
esphome:
|
||||
name: template-time-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
time:
|
||||
- platform: template
|
||||
id: fixed_time
|
||||
lambda: |-
|
||||
return 1700000000;
|
||||
|
||||
# Derived from another time source, as when offsetting a real clock.
|
||||
- platform: template
|
||||
id: offset_time
|
||||
lambda: |-
|
||||
return id(fixed_time).timestamp_now() + 3600;
|
||||
|
||||
# A lambda that gives no value reports an invalid time.
|
||||
- platform: template
|
||||
id: empty_time
|
||||
lambda: |-
|
||||
return {};
|
||||
|
||||
text_sensor:
|
||||
# Timestamps are exposed as text (rather than a 32-bit float sensor state,
|
||||
# which cannot represent a UNIX epoch exactly) so the exact value can be checked.
|
||||
- platform: template
|
||||
name: "Fixed Timestamp"
|
||||
id: fixed_timestamp
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
return std::to_string(id(fixed_time).now().timestamp);
|
||||
|
||||
- platform: template
|
||||
name: "Offset Timestamp"
|
||||
id: offset_timestamp
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
return std::to_string(id(offset_time).now().timestamp);
|
||||
|
||||
- platform: template
|
||||
name: "Empty Time Valid"
|
||||
id: empty_time_valid
|
||||
update_interval: 100ms
|
||||
lambda: |-
|
||||
return std::string(id(empty_time).now().is_valid() ? "valid" : "invalid");
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Integration test for the template time platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, TextSensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
# Timestamps are exposed as text sensors (rather than 32-bit float sensor states,
|
||||
# which cannot represent a UNIX epoch exactly) so the exact value can be checked.
|
||||
EXPECTED_STATES = {
|
||||
"fixed_timestamp": "1700000000",
|
||||
"offset_timestamp": "1700003600",
|
||||
"empty_time_valid": "invalid",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_time(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Verify the template time platform evaluates its lambda on demand."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
key_to_name = build_key_to_entity_mapping(entities, list(EXPECTED_STATES))
|
||||
events = {name: asyncio.Event() for name in EXPECTED_STATES}
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, TextSensorState) and not state.missing_state:
|
||||
name = key_to_name.get(state.key)
|
||||
if name is not None and state.state == EXPECTED_STATES[name]:
|
||||
events[name].set()
|
||||
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
for name, event in events.items():
|
||||
try:
|
||||
await asyncio.wait_for(event.wait(), timeout=3.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Timeout waiting for {name} to report its expected value")
|
||||
Reference in New Issue
Block a user