Coverage Report

Created: 2026-08-14 07:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/connectedhomeip/src/app/clusters/smoke-co-alarm-server/SmokeCoAlarmCluster.cpp
Line
Count
Source
1
/**
2
 *
3
 *    Copyright (c) 2023 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
#include "SmokeCoAlarmCluster.h"
19
#include <app/server-cluster/AttributeListBuilder.h>
20
#include <clusters/SmokeCoAlarm/Metadata.h>
21
22
using namespace chip::app::Clusters::SmokeCoAlarm;
23
using namespace chip::app::Clusters::SmokeCoAlarm::Attributes;
24
using chip::Protocols::InteractionModel::Status;
25
26
namespace chip {
27
namespace app {
28
namespace Clusters {
29
30
0
SmokeCoAlarmCluster::SmokeCoAlarmCluster(EndpointId endpointId) : SmokeCoAlarmCluster(endpointId, Config{}) {}
31
32
SmokeCoAlarmCluster::SmokeCoAlarmCluster(EndpointId endpointId, const Config & config) :
33
0
    DefaultServerCluster({ endpointId, Id }), mConfig(config), mInoperativeWhenUnmounted(config.inoperativeWhenUnmounted)
34
0
{}
35
36
bool SmokeCoAlarmCluster::SetSmokeState(AlarmStateEnum newSmokeState)
37
0
{
38
0
    VerifyOrReturnValue(SupportsSmokeAlarm(), false);
39
0
    if (!SetAttributeValue(mSmokeState, newSmokeState, SmokeState::Id))
40
0
    {
41
0
        return true;
42
0
    }
43
44
0
    if (mContext != nullptr && (newSmokeState == AlarmStateEnum::kWarning || newSmokeState == AlarmStateEnum::kCritical))
45
0
    {
46
0
        Events::SmokeAlarm::Type event{ newSmokeState };
47
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
48
0
    }
49
50
0
    if (newSmokeState == AlarmStateEnum::kCritical)
51
0
    {
52
0
        SetDeviceMuted(MuteStateEnum::kNotMuted);
53
0
    }
54
55
0
    return true;
56
0
}
57
58
bool SmokeCoAlarmCluster::SetCOState(AlarmStateEnum newCOState)
59
0
{
60
0
    VerifyOrReturnValue(SupportsCOAlarm(), false);
61
0
    if (!SetAttributeValue(mCOState, newCOState, COState::Id))
62
0
    {
63
0
        return true;
64
0
    }
65
66
0
    if (mContext != nullptr && (newCOState == AlarmStateEnum::kWarning || newCOState == AlarmStateEnum::kCritical))
67
0
    {
68
0
        Events::COAlarm::Type event{ newCOState };
69
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
70
0
    }
71
72
0
    if (newCOState == AlarmStateEnum::kCritical)
73
0
    {
74
0
        SetDeviceMuted(MuteStateEnum::kNotMuted);
75
0
    }
76
77
0
    return true;
78
0
}
79
80
void SmokeCoAlarmCluster::SetBatteryAlert(AlarmStateEnum newBatteryAlert)
81
0
{
82
0
    if (!SetAttributeValue(mBatteryAlert, newBatteryAlert, BatteryAlert::Id))
83
0
    {
84
0
        return;
85
0
    }
86
87
0
    if (mContext != nullptr && (newBatteryAlert == AlarmStateEnum::kWarning || newBatteryAlert == AlarmStateEnum::kCritical))
88
0
    {
89
0
        Events::LowBattery::Type event{ newBatteryAlert };
90
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
91
0
    }
92
93
0
    if (newBatteryAlert == AlarmStateEnum::kCritical)
94
0
    {
95
0
        SetDeviceMuted(MuteStateEnum::kNotMuted);
96
0
    }
97
0
}
98
99
bool SmokeCoAlarmCluster::SetDeviceMuted(MuteStateEnum newDeviceMuted)
100
0
{
101
0
    VerifyOrReturnValue(mConfig.optionalAttribs.IsSet(DeviceMuted::Id), false);
102
0
    if (mDeviceMuted == newDeviceMuted)
103
0
    {
104
0
        return true;
105
0
    }
106
107
0
    if (newDeviceMuted == MuteStateEnum::kMuted)
108
0
    {
109
        // Cannot mute while any alarm is in Critical state.
110
        // All alarm state members are always present and default to kNormal
111
0
        VerifyOrReturnValue(mSmokeState != AlarmStateEnum::kCritical, false);
112
0
        VerifyOrReturnValue(mCOState != AlarmStateEnum::kCritical, false);
113
0
        VerifyOrReturnValue(mBatteryAlert != AlarmStateEnum::kCritical, false);
114
0
        VerifyOrReturnValue(mInterconnectSmokeAlarm != AlarmStateEnum::kCritical, false);
115
0
        VerifyOrReturnValue(mInterconnectCOAlarm != AlarmStateEnum::kCritical, false);
116
0
    }
117
118
0
    SetAttributeValue(mDeviceMuted, newDeviceMuted, DeviceMuted::Id);
119
120
0
    if (mContext != nullptr)
121
0
    {
122
0
        if (newDeviceMuted == MuteStateEnum::kMuted)
123
0
        {
124
0
            Events::AlarmMuted::Type event{};
125
0
            mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
126
0
        }
127
0
        else if (newDeviceMuted == MuteStateEnum::kNotMuted)
128
0
        {
129
0
            Events::MuteEnded::Type event{};
130
0
            mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
131
0
        }
132
0
    }
133
134
0
    return true;
135
0
}
136
137
void SmokeCoAlarmCluster::SetTestInProgress(bool newTestInProgress)
138
0
{
139
0
    if (!SetAttributeValue(mTestInProgress, newTestInProgress, TestInProgress::Id))
140
0
    {
141
0
        return;
142
0
    }
143
144
0
    if (!newTestInProgress && mContext != nullptr)
145
0
    {
146
0
        Events::SelfTestComplete::Type event{};
147
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
148
0
    }
149
0
}
150
151
void SmokeCoAlarmCluster::SetHardwareFaultAlert(bool newHardwareFaultAlert)
152
0
{
153
0
    if (!SetAttributeValue(mHardwareFaultAlert, newHardwareFaultAlert, HardwareFaultAlert::Id))
154
0
    {
155
0
        return;
156
0
    }
157
158
0
    if (newHardwareFaultAlert && mContext != nullptr)
159
0
    {
160
0
        Events::HardwareFault::Type event{};
161
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
162
0
    }
163
0
}
164
165
void SmokeCoAlarmCluster::SetEndOfServiceAlert(EndOfServiceEnum newEndOfServiceAlert)
166
0
{
167
0
    if (!SetAttributeValue(mEndOfServiceAlert, newEndOfServiceAlert, EndOfServiceAlert::Id))
168
0
    {
169
0
        return;
170
0
    }
171
172
0
    if (newEndOfServiceAlert == EndOfServiceEnum::kExpired && mContext != nullptr)
173
0
    {
174
0
        Events::EndOfService::Type event{};
175
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
176
0
    }
177
0
}
178
179
bool SmokeCoAlarmCluster::SetInterconnectSmokeAlarm(AlarmStateEnum newInterconnectSmokeAlarm)
180
0
{
181
0
    VerifyOrReturnValue(mConfig.optionalAttribs.IsSet(InterconnectSmokeAlarm::Id), false);
182
0
    if (!SetAttributeValue(mInterconnectSmokeAlarm, newInterconnectSmokeAlarm, InterconnectSmokeAlarm::Id))
183
0
    {
184
0
        return true;
185
0
    }
186
187
0
    if (mContext != nullptr &&
188
0
        (newInterconnectSmokeAlarm == AlarmStateEnum::kWarning || newInterconnectSmokeAlarm == AlarmStateEnum::kCritical))
189
0
    {
190
0
        Events::InterconnectSmokeAlarm::Type event{ newInterconnectSmokeAlarm };
191
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
192
0
    }
193
194
0
    if (newInterconnectSmokeAlarm == AlarmStateEnum::kCritical)
195
0
    {
196
0
        SetDeviceMuted(MuteStateEnum::kNotMuted);
197
0
    }
198
199
0
    return true;
200
0
}
201
202
bool SmokeCoAlarmCluster::SetInterconnectCOAlarm(AlarmStateEnum newInterconnectCOAlarm)
203
0
{
204
0
    VerifyOrReturnValue(mConfig.optionalAttribs.IsSet(InterconnectCOAlarm::Id), false);
205
0
    if (!SetAttributeValue(mInterconnectCOAlarm, newInterconnectCOAlarm, InterconnectCOAlarm::Id))
206
0
    {
207
0
        return true;
208
0
    }
209
210
0
    if (mContext != nullptr &&
211
0
        (newInterconnectCOAlarm == AlarmStateEnum::kWarning || newInterconnectCOAlarm == AlarmStateEnum::kCritical))
212
0
    {
213
0
        Events::InterconnectCOAlarm::Type event{ newInterconnectCOAlarm };
214
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
215
0
    }
216
217
0
    if (newInterconnectCOAlarm == AlarmStateEnum::kCritical)
218
0
    {
219
0
        SetDeviceMuted(MuteStateEnum::kNotMuted);
220
0
    }
221
222
0
    return true;
223
0
}
224
225
void SmokeCoAlarmCluster::SetContaminationState(ContaminationStateEnum newContaminationState)
226
0
{
227
0
    VerifyOrReturn(SupportsSmokeAlarm());
228
0
    SetAttributeValue(mContaminationState, newContaminationState, ContaminationState::Id);
229
0
}
230
231
void SmokeCoAlarmCluster::SetSmokeSensitivityLevel(SensitivityEnum newSmokeSensitivityLevel)
232
0
{
233
0
    VerifyOrReturn(SupportsSmokeAlarm());
234
0
    SetAttributeValue(mSmokeSensitivityLevel, newSmokeSensitivityLevel, SmokeSensitivityLevel::Id);
235
0
    if (mDelegate != nullptr)
236
0
    {
237
0
        mDelegate->OnSmokeSensitivityLevelChanged(newSmokeSensitivityLevel);
238
0
    }
239
0
}
240
241
void SmokeCoAlarmCluster::SetExpiryDate(uint32_t newExpiryDate)
242
0
{
243
0
    VerifyOrReturn(mConfig.optionalAttribs.IsSet(ExpiryDate::Id));
244
0
    SetAttributeValue(mExpiryDate, newExpiryDate, ExpiryDate::Id);
245
0
}
246
247
bool SmokeCoAlarmCluster::SetUnmountedState(bool isUnmounted)
248
0
{
249
0
    VerifyOrReturnValue(mConfig.optionalAttribs.IsSet(Unmounted::Id), false);
250
0
    if (!SetAttributeValue(mUnmounted, isUnmounted, Unmounted::Id))
251
0
    {
252
0
        return true;
253
0
    }
254
255
0
    if (mInoperativeWhenUnmounted)
256
0
    {
257
0
        if (isUnmounted)
258
0
        {
259
0
            SetExpressedState(ExpressedStateEnum::kInoperative);
260
0
        }
261
0
        else if (mExpressedState == ExpressedStateEnum::kInoperative)
262
0
        {
263
0
            SetExpressedState(ExpressedStateEnum::kNormal);
264
0
        }
265
0
    }
266
267
0
    return true;
268
0
}
269
270
void SmokeCoAlarmCluster::SetExpressedStateByPriority(const std::array<ExpressedStateEnum, kPriorityOrderLength> & priorityOrder)
271
0
{
272
0
    for (ExpressedStateEnum priority : priorityOrder)
273
0
    {
274
0
        AlarmStateEnum alarmState          = AlarmStateEnum::kNormal;
275
0
        EndOfServiceEnum endOfServiceState = EndOfServiceEnum::kNormal;
276
0
        bool active                        = false;
277
0
        bool unmounted                     = false;
278
279
0
        switch (priority)
280
0
        {
281
0
        case ExpressedStateEnum::kSmokeAlarm:
282
0
            alarmState = GetSmokeState();
283
0
            break;
284
0
        case ExpressedStateEnum::kCOAlarm:
285
0
            alarmState = GetCOState();
286
0
            break;
287
0
        case ExpressedStateEnum::kBatteryAlert:
288
0
            alarmState = GetBatteryAlert();
289
0
            break;
290
0
        case ExpressedStateEnum::kTesting:
291
0
            active = GetTestInProgress();
292
0
            break;
293
0
        case ExpressedStateEnum::kHardwareFault:
294
0
            active = GetHardwareFaultAlert();
295
0
            break;
296
0
        case ExpressedStateEnum::kEndOfService:
297
0
            endOfServiceState = GetEndOfServiceAlert();
298
0
            break;
299
0
        case ExpressedStateEnum::kInterconnectSmoke:
300
0
            alarmState = GetInterconnectSmokeAlarm();
301
0
            break;
302
0
        case ExpressedStateEnum::kInterconnectCO:
303
0
            alarmState = GetInterconnectCOAlarm();
304
0
            break;
305
0
        case ExpressedStateEnum::kInoperative:
306
0
            unmounted = GetUnmountedState();
307
0
            break;
308
0
        default:
309
0
            break;
310
0
        }
311
312
0
        if ((alarmState != AlarmStateEnum::kNormal) || (endOfServiceState != EndOfServiceEnum::kNormal) || active || unmounted)
313
0
        {
314
0
            SetExpressedState(priority);
315
0
            return;
316
0
        }
317
0
    }
318
319
0
    SetExpressedState(ExpressedStateEnum::kNormal);
320
0
}
321
322
bool SmokeCoAlarmCluster::RequestSelfTest()
323
0
{
324
0
    if (mExpressedState == ExpressedStateEnum::kSmokeAlarm || mExpressedState == ExpressedStateEnum::kCOAlarm ||
325
0
        mExpressedState == ExpressedStateEnum::kTesting || mExpressedState == ExpressedStateEnum::kInterconnectSmoke ||
326
0
        mExpressedState == ExpressedStateEnum::kInterconnectCO)
327
0
    {
328
0
        return false;
329
0
    }
330
331
0
    SetTestInProgress(true);
332
0
    SetExpressedState(ExpressedStateEnum::kTesting);
333
0
    if (mDelegate != nullptr)
334
0
    {
335
0
        mDelegate->OnSelfTestRequested();
336
0
    }
337
0
    return true;
338
0
}
339
340
DataModel::ActionReturnStatus SmokeCoAlarmCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
341
                                                                 AttributeValueEncoder & encoder)
342
0
{
343
0
    switch (request.path.mAttributeId)
344
0
    {
345
0
    case ClusterRevision::Id:
346
0
        return encoder.Encode(SmokeCoAlarm::kRevision);
347
0
    case FeatureMap::Id:
348
0
        return encoder.Encode(mConfig.featureMap);
349
0
    case ExpressedState::Id:
350
0
        return encoder.Encode(mExpressedState);
351
0
    case SmokeState::Id:
352
0
        return encoder.Encode(mSmokeState);
353
0
    case COState::Id:
354
0
        return encoder.Encode(mCOState);
355
0
    case BatteryAlert::Id:
356
0
        return encoder.Encode(mBatteryAlert);
357
0
    case DeviceMuted::Id:
358
0
        return encoder.Encode(mDeviceMuted);
359
0
    case TestInProgress::Id:
360
0
        return encoder.Encode(mTestInProgress);
361
0
    case HardwareFaultAlert::Id:
362
0
        return encoder.Encode(mHardwareFaultAlert);
363
0
    case EndOfServiceAlert::Id:
364
0
        return encoder.Encode(mEndOfServiceAlert);
365
0
    case InterconnectSmokeAlarm::Id:
366
0
        return encoder.Encode(mInterconnectSmokeAlarm);
367
0
    case InterconnectCOAlarm::Id:
368
0
        return encoder.Encode(mInterconnectCOAlarm);
369
0
    case ContaminationState::Id:
370
0
        return encoder.Encode(mContaminationState);
371
0
    case SmokeSensitivityLevel::Id:
372
0
        return encoder.Encode(mSmokeSensitivityLevel);
373
0
    case ExpiryDate::Id:
374
0
        return encoder.Encode(mExpiryDate);
375
0
    case Unmounted::Id:
376
0
        return encoder.Encode(mUnmounted);
377
0
    default:
378
0
        return Status::UnsupportedAttribute;
379
0
    }
380
0
}
381
382
DataModel::ActionReturnStatus SmokeCoAlarmCluster::WriteAttribute(const DataModel::WriteAttributeRequest & request,
383
                                                                  AttributeValueDecoder & decoder)
384
0
{
385
0
    if (request.path.mAttributeId != SmokeSensitivityLevel::Id)
386
0
    {
387
0
        return Status::UnsupportedWrite;
388
0
    }
389
390
0
    SensitivityEnum value;
391
0
    ReturnErrorOnFailure(decoder.Decode(value));
392
0
    SetSmokeSensitivityLevel(value);
393
0
    return Status::Success;
394
0
}
395
396
CHIP_ERROR SmokeCoAlarmCluster::Attributes(const ConcreteClusterPath & path,
397
                                           ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
398
0
{
399
0
    AttributeListBuilder listBuilder(builder);
400
401
0
    const AttributeListBuilder::OptionalAttributeEntry optionalAttributes[] = {
402
0
        { mConfig.featureMap.Has(Feature::kSmokeAlarm), SmokeState::kMetadataEntry },
403
0
        { mConfig.featureMap.Has(Feature::kCoAlarm), COState::kMetadataEntry },
404
0
        { mConfig.optionalAttribs.IsSet(DeviceMuted::Id), DeviceMuted::kMetadataEntry },
405
0
        { mConfig.optionalAttribs.IsSet(InterconnectSmokeAlarm::Id), InterconnectSmokeAlarm::kMetadataEntry },
406
0
        { mConfig.optionalAttribs.IsSet(InterconnectCOAlarm::Id), InterconnectCOAlarm::kMetadataEntry },
407
0
        { mConfig.featureMap.Has(Feature::kSmokeAlarm), ContaminationState::kMetadataEntry },
408
0
        { mConfig.featureMap.Has(Feature::kSmokeAlarm), SmokeSensitivityLevel::kMetadataEntry },
409
0
        { mConfig.optionalAttribs.IsSet(ExpiryDate::Id), ExpiryDate::kMetadataEntry },
410
0
        { mConfig.optionalAttribs.IsSet(Unmounted::Id), Unmounted::kMetadataEntry },
411
0
    };
412
413
0
    return listBuilder.Append(Span(kMandatoryMetadata), Span(optionalAttributes));
414
0
}
415
416
std::optional<DataModel::ActionReturnStatus> SmokeCoAlarmCluster::InvokeCommand(const DataModel::InvokeRequest & request,
417
                                                                                chip::TLV::TLVReader & input_arguments,
418
                                                                                CommandHandler * handler)
419
0
{
420
0
    switch (request.path.mCommandId)
421
0
    {
422
0
    case Commands::SelfTestRequest::Id:
423
0
        return HandleRemoteSelfTestRequest();
424
0
    default:
425
0
        return Status::UnsupportedCommand;
426
0
    }
427
0
}
428
429
CHIP_ERROR SmokeCoAlarmCluster::AcceptedCommands(const ConcreteClusterPath & path,
430
                                                 ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
431
0
{
432
0
    return builder.AppendElements({ Commands::SelfTestRequest::kMetadataEntry });
433
0
}
434
435
void SmokeCoAlarmCluster::SetExpressedState(ExpressedStateEnum newExpressedState)
436
0
{
437
0
    if (!SetAttributeValue(mExpressedState, newExpressedState, ExpressedState::Id))
438
0
    {
439
0
        return;
440
0
    }
441
442
0
    if (newExpressedState == ExpressedStateEnum::kNormal && mContext != nullptr)
443
0
    {
444
0
        Events::AllClear::Type event{};
445
0
        mContext->interactionContext.eventsGenerator.GenerateEvent(event, mPath.mEndpointId);
446
0
    }
447
448
0
    if (mDelegate != nullptr)
449
0
    {
450
0
        mDelegate->OnExpressedStateChanged(newExpressedState);
451
0
    }
452
0
}
453
454
Status SmokeCoAlarmCluster::HandleRemoteSelfTestRequest()
455
0
{
456
0
    if (mExpressedState == ExpressedStateEnum::kSmokeAlarm || mExpressedState == ExpressedStateEnum::kCOAlarm ||
457
0
        mExpressedState == ExpressedStateEnum::kTesting || mExpressedState == ExpressedStateEnum::kInterconnectSmoke ||
458
0
        mExpressedState == ExpressedStateEnum::kInterconnectCO)
459
0
    {
460
0
        return Status::Busy;
461
0
    }
462
463
0
    SetTestInProgress(true);
464
0
    SetExpressedState(ExpressedStateEnum::kTesting);
465
0
    if (mDelegate != nullptr)
466
0
    {
467
0
        mDelegate->OnSelfTestRequested();
468
0
    }
469
0
    return Status::Success;
470
0
}
471
472
} // namespace Clusters
473
} // namespace app
474
} // namespace chip