Skip to main content

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}