mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 15:30:21 +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()); }
|
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
|
||||||
|
|
||||||
/// Get the current time as the UTC epoch since January 1st 1970.
|
/// 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) {
|
template<typename F> void add_on_time_sync_callback(F &&callback) {
|
||||||
this->time_sync_callback_.add(std::forward<F>(callback));
|
this->time_sync_callback_.add(std::forward<F>(callback));
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
time:
|
time:
|
||||||
- platform: sntp # Required for datetime
|
- platform: sntp # Required for datetime
|
||||||
id: sntp_time
|
id: sntp_time
|
||||||
|
- platform: template
|
||||||
|
id: template_offset_time
|
||||||
|
lambda: |-
|
||||||
|
return id(sntp_time).timestamp_now() + 3600;
|
||||||
|
|
||||||
wifi: # Required for sntp time
|
wifi: # Required for sntp time
|
||||||
ap:
|
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