rs_matter/dm/clusters/power_source.rs
1/*
2 *
3 * Copyright (c) 2026 Project CHIP Authors
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18//! PowerSource cluster handler.
19//!
20//! Describes *one* source of power feeding the node - mains, a battery, or
21//! PoE - and which endpoints it powers. A node with two sources (say, mains
22//! plus a backup battery) hosts two instances on two endpoints, ordered by
23//! [`PowerSourceConfig::order`].
24//!
25//! This handler implements the minimal conformant *wired* shape of the
26//! cluster: the four attributes that are mandatory regardless of features
27//! (`Status`, `Order`, `Description`, `EndpointList`), plus the `WIRED`
28//! feature and its single mandatory attribute, `WiredCurrentType`.
29//!
30//! `WIRED` is not optional decoration: the `WIRED` / `BAT` features form an
31//! at-least-one-of choice conformance (`O.a` in the spec), so a `FeatureMap`
32//! of zero is non-conformant - `TC_DeviceConformance` flags it as
33//! "choice conformance .a - 0 selected". The battery-side features
34//! (`BAT` / `RECHG` / `REPLC`) are not implemented; the generated trait
35//! defaults answer `AttributeNotFound` for their attributes.
36//!
37//! That featureless shape is deliberately useful on its own: test harnesses
38//! and ecosystems probe `PowerSource` to decide whether a device is
39//! battery-powered (`FeatureMap & BATTERY`), and a device with no PowerSource
40//! at all makes that probe *fail* rather than answer "no". Hosting this
41//! handler turns that into a clean negative answer.
42//!
43//! The cluster is optional in Matter - nothing requires a node to host it.
44//!
45//! Application wiring:
46//!
47//! ```ignore
48//! const POWER: PowerSourceConfig = PowerSourceConfig {
49//! status: PowerSourceStatusEnum::Active,
50//! order: 0,
51//! description: "Mains",
52//! endpoint_list: &[1],
53//! };
54//!
55//! let handler = PowerSourceHandler::new(Dataver::new_rand(rand), &POWER);
56//! ```
57
58use crate::dm::{ArrayAttributeRead, Cluster, Dataver, ReadContext};
59use crate::error::{Error, ErrorCode};
60use crate::im::EndptId;
61use crate::tlv::{TLVBuilderParent, ToTLVArrayBuilder, ToTLVBuilder, Utf8StrBuilder};
62use crate::with;
63
64pub use crate::dm::clusters::decl::power_source::*;
65
66/// Cluster metadata exposed by [`PowerSourceHandler`].
67///
68/// Exposed as a free constant so callers can spell out
69/// `EpClMatcher::new(Some(ep), Some(power_source::CLUSTER.id))` without
70/// naming the lifetime-parameterised handler type.
71pub const CLUSTER: Cluster<'static> = FULL_CLUSTER
72 .with_features(Feature::WIRED.bits())
73 .with_attrs(with!(required; AttributeId::WiredCurrentType))
74 .with_cmds(with!());
75
76/// The application-supplied description of one power source.
77///
78/// Borrowed rather than owned, and expected to be a `'static` constant: these
79/// are firmware identity, in the same spirit as
80/// [`super::fixed_label::FixedLabelEntry`]. A source whose *status* genuinely
81/// changes at runtime is better served by a bespoke [`ClusterHandler`] impl
82/// than by making this struct mutable.
83#[derive(Debug, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "defmt", derive(defmt::Format))]
85pub struct PowerSourceConfig<'a> {
86 /// Whether this source is currently supplying power.
87 pub status: PowerSourceStatusEnum,
88 /// Relative preference, 0 being the most preferred. Per the spec, the
89 /// values across a node's sources SHALL be distinct.
90 pub order: u8,
91 /// Human-readable name of the source, e.g. `"Mains"`. At most 60
92 /// characters; not enforced here, as with the other config-carrying
93 /// clusters.
94 pub description: &'a str,
95 /// The endpoints this source powers. May be empty, which per the spec
96 /// means the source powers the node as a whole.
97 pub endpoint_list: &'a [EndptId],
98 /// The current type delivered by this wired source (mandatory under the
99 /// `WIRED` feature, which this handler always claims - see the module
100 /// docs on the `WIRED`/`BAT` choice conformance).
101 pub wired_current_type: WiredCurrentTypeEnum,
102}
103
104impl PowerSourceConfig<'_> {
105 /// A mains-powered source that powers the whole node.
106 ///
107 /// The common case for a wired accessory, and the one that makes
108 /// "is this device battery powered?" answerable with a plain no.
109 pub const MAINS: Self = Self {
110 status: PowerSourceStatusEnum::Active,
111 order: 0,
112 description: "Mains",
113 endpoint_list: &[],
114 wired_current_type: WiredCurrentTypeEnum::AC,
115 };
116}
117
118/// The system implementation of a handler for the PowerSource Matter cluster.
119///
120/// Per-endpoint instance: a node with several power sources hosts one handler
121/// per source, each on its own endpoint with its own `Dataver`.
122pub struct PowerSourceHandler<'a> {
123 dataver: Dataver,
124 config: &'a PowerSourceConfig<'a>,
125}
126
127impl<'a> PowerSourceHandler<'a> {
128 /// Create a new handler describing `config`.
129 pub const fn new(dataver: Dataver, config: &'a PowerSourceConfig<'a>) -> Self {
130 Self { dataver, config }
131 }
132
133 /// Adapt the handler instance to the generic `rs-matter` `Handler` trait.
134 pub const fn adapt(self) -> HandlerAdaptor<Self> {
135 HandlerAdaptor(self)
136 }
137}
138
139impl ClusterHandler for PowerSourceHandler<'_> {
140 const CLUSTER: Cluster<'static> = CLUSTER;
141
142 fn dataver(&self) -> u32 {
143 self.dataver.get()
144 }
145
146 fn dataver_changed(&self) {
147 self.dataver.changed();
148 }
149
150 fn status(&self, _ctx: impl ReadContext) -> Result<PowerSourceStatusEnum, Error> {
151 Ok(self.config.status)
152 }
153
154 fn order(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
155 Ok(self.config.order)
156 }
157
158 fn description<P: TLVBuilderParent>(
159 &self,
160 _ctx: impl ReadContext,
161 out: Utf8StrBuilder<P>,
162 ) -> Result<P, Error> {
163 out.set(self.config.description)
164 }
165
166 fn wired_current_type(&self, _ctx: impl ReadContext) -> Result<WiredCurrentTypeEnum, Error> {
167 Ok(self.config.wired_current_type)
168 }
169
170 fn endpoint_list<P: TLVBuilderParent>(
171 &self,
172 _ctx: impl ReadContext,
173 builder: ArrayAttributeRead<ToTLVArrayBuilder<P, EndptId>, ToTLVBuilder<P, EndptId>>,
174 ) -> Result<P, Error> {
175 match builder {
176 ArrayAttributeRead::ReadAll(mut builder) => {
177 for endpoint in self.config.endpoint_list {
178 builder = builder.push(endpoint)?;
179 }
180
181 builder.end()
182 }
183 ArrayAttributeRead::ReadOne(index, builder) => {
184 let Some(endpoint) = self.config.endpoint_list.get(index as usize) else {
185 // List-element index out of bounds - IM convention is
186 // `ConstraintError`, as in `FixedLabelHandler`.
187 return Err(ErrorCode::ConstraintError.into());
188 };
189
190 builder.set(endpoint)
191 }
192 ArrayAttributeRead::ReadNone(builder) => builder.end(),
193 }
194 }
195}