Skip to content

Commit da94ec1

Browse files
authored
Expose water filter expiry date and remaining gallons (#101)
* Expose water filter expiry date and remaining gallons * Modify tests with new keys. This may be a bad approach of the exemplar appliances lack these keys * modify CI for fork PRs
1 parent 61b2949 commit da94ec1

11 files changed

Lines changed: 110 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,19 @@ jobs:
5959
EOF
6060
6161
- name: Patch package refs for CI
62+
env:
63+
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
6264
run: |
6365
# Point package refs to the code under test instead of a published tag.
64-
# PRs: use the source branch name. Pushes: use the commit SHA.
66+
# PRs: use the head repo + source branch name (supports fork PRs).
67+
# Pushes: stay on the default repo and use the commit SHA.
6568
# Use # as sed delimiter — branch names may contain / (so | was used
6669
# before), but extended-regex alternation also needs |, which GNU
6770
# sed eats as the delimiter. # is safe in both pattern and replacement.
6871
REF="${GITHUB_HEAD_REF:-$GITHUB_SHA}"
72+
REPO="${HEAD_REPO:-$GITHUB_REPOSITORY}"
73+
sed -i -E "s#url: https://github.com/[^[:space:]]+#url: https://github.com/${REPO}#" \
74+
cove-minimal-dishwasher.yaml wolf-minimal-range.yaml wolf-minimal-fridge.yaml
6975
sed -i -E "s#ref: (v[0-9]+\.[0-9]+\.[0-9]+|main)#ref: ${REF}#" \
7076
cove-minimal-dishwasher.yaml wolf-minimal-range.yaml wolf-minimal-fridge.yaml
7177
@@ -112,11 +118,15 @@ jobs:
112118
EOF
113119
114120
- name: Patch package refs for CI
121+
env:
122+
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
115123
run: |
116124
# Use # as sed delimiter — branch names may contain / (so | was used
117125
# before), but extended-regex alternation also needs |, which GNU
118126
# sed eats as the delimiter. # is safe in both pattern and replacement.
119127
REF="${GITHUB_HEAD_REF:-$GITHUB_SHA}"
128+
REPO="${HEAD_REPO:-$GITHUB_REPOSITORY}"
129+
sed -i -E "s#url: https://github.com/[^[:space:]]+#url: https://github.com/${REPO}#" "${{ matrix.file }}"
120130
sed -i -E "s#ref: (v[0-9]+\.[0-9]+\.[0-9]+|main)#ref: ${REF}#" "${{ matrix.file }}"
121131
122132
- name: Compile ${{ matrix.file }}

‎components/subzero_appliance/__init__.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
DEVICE_CLASS_DURATION,
6464
DEVICE_CLASS_PROBLEM,
6565
DEVICE_CLASS_TEMPERATURE,
66+
DEVICE_CLASS_TIMESTAMP,
6667
ENTITY_CATEGORY_CONFIG,
6768
ENTITY_CATEGORY_DIAGNOSTIC,
6869
STATE_CLASS_MEASUREMENT,
@@ -401,6 +402,31 @@
401402
},
402403
"hide_water_filter",
403404
),
405+
(
406+
"water_filter_gal",
407+
"Water Filter Gallons Remaining",
408+
"set_water_filter_gal_sensor",
409+
{
410+
CONF_UNIT_OF_MEASUREMENT: "gal",
411+
"state_class": STATE_CLASS_MEASUREMENT,
412+
"accuracy_decimals": 1,
413+
CONF_ICON: "mdi:water",
414+
},
415+
"hide_water_filter",
416+
),
417+
]
418+
419+
FRIDGE_TEXT_SENSORS = [
420+
(
421+
"water_filter_end_date",
422+
"Water Filter Expires",
423+
"set_water_filter_end_date_sensor",
424+
{
425+
CONF_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
426+
CONF_ICON: "mdi:calendar-clock",
427+
},
428+
"hide_water_filter",
429+
),
404430
]
405431

406432
# Writable numbers — entry shape: (suffix, name_suffix, setter, property_key,
@@ -1120,7 +1146,7 @@ async def to_code(config):
11201146
if type_ == "fridge":
11211147
bs_list = FRIDGE_BINARY_SENSORS
11221148
s_list = FRIDGE_SENSORS
1123-
ts_list = []
1149+
ts_list = FRIDGE_TEXT_SENSORS
11241150
sw_list = FRIDGE_WRITABLE_SWITCHES
11251151
n_list = FRIDGE_WRITABLE_NUMBERS
11261152
elif type_ == "dishwasher":

‎components/subzero_appliance/appliance.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class FridgeAppliance : public ApplianceBase {
8181
void set_water_filter_pct_sensor(esphome::sensor::Sensor *s) {
8282
bus_.water_filter_pct = s;
8383
}
84+
void set_water_filter_gal_sensor(esphome::sensor::Sensor *s) {
85+
bus_.water_filter_gal = s;
86+
}
87+
void set_water_filter_end_date_sensor(esphome::text_sensor::TextSensor *s) {
88+
bus_.water_filter_end_date = s;
89+
}
8490

8591
protected:
8692
SubzeroHub *hub() override { return &hub_; }

‎components/subzero_protocol/dispatch.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ inline void dispatch_fridge(const FridgeState &s, Bus &bus) {
104104
bus.publish_air_filter_pct(*s.air_filter_pct_remaining);
105105
if (s.water_filter_pct_remaining)
106106
bus.publish_water_filter_pct(*s.water_filter_pct_remaining);
107+
if (s.water_filter_gal_remaining)
108+
bus.publish_water_filter_gal(*s.water_filter_gal_remaining);
109+
if (s.water_filter_end_date)
110+
bus.publish_water_filter_end_date(*s.water_filter_end_date);
107111
}
108112

109113
template <typename Bus>

‎components/subzero_protocol/dispatch_esphome.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ struct FridgeBus : CommonBus {
140140
esphome::sensor::Sensor *crisp_set_temp = nullptr;
141141
esphome::sensor::Sensor *air_filter_pct = nullptr;
142142
esphome::sensor::Sensor *water_filter_pct = nullptr;
143+
esphome::sensor::Sensor *water_filter_gal = nullptr;
144+
esphome::text_sensor::TextSensor *water_filter_end_date = nullptr;
143145

144146
void publish_door_ajar(bool v) { detail::publish_if(door_ajar, v); }
145147
void publish_frz_door_ajar(bool v) { detail::publish_if(frz_door_ajar, v); }
@@ -167,6 +169,12 @@ struct FridgeBus : CommonBus {
167169
void publish_water_filter_pct(float v) {
168170
detail::publish_if(water_filter_pct, v);
169171
}
172+
void publish_water_filter_gal(float v) {
173+
detail::publish_if(water_filter_gal, v);
174+
}
175+
void publish_water_filter_end_date(const std::string &v) {
176+
detail::publish_if(water_filter_end_date, v);
177+
}
170178
};
171179

172180
struct DishwasherBus : CommonBus {

‎components/subzero_protocol/protocol.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,19 @@ FridgeState parse_fridge(const std::string &json) {
265265
state.air_filter_pct_remaining = opt_float(data["air_filter_pct_remaining"]);
266266
state.water_filter_pct_remaining =
267267
opt_float(data["water_filter_pct_remaining"]);
268+
state.water_filter_gal_remaining =
269+
opt_float(data["water_filter_gal_remaining"]);
270+
// Appliance sends a date-only string (e.g. "2027-05-10"). Promote to a
271+
// fully-qualified ISO8601 timestamp so HA's timestamp device_class accepts
272+
// it. Pass-through any value that already contains a 'T' separator.
273+
if (auto raw = opt_str(data["water_filter_end_date"])) {
274+
const std::string &v = *raw;
275+
if (v.size() == 10 && v[4] == '-' && v[7] == '-') {
276+
state.water_filter_end_date = v + "T00:00:00+00:00";
277+
} else {
278+
state.water_filter_end_date = v;
279+
}
280+
}
268281
return state;
269282
}
270283

‎components/subzero_protocol/protocol.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ struct FridgeState {
5555
std::optional<bool> air_filter_on;
5656
std::optional<float> air_filter_pct_remaining;
5757
std::optional<float> water_filter_pct_remaining;
58+
std::optional<float> water_filter_gal_remaining;
59+
// Raw date string from the appliance (observed as "YYYY-MM-DD"). Converted
60+
// to an ISO8601 timestamp ("YYYY-MM-DDT00:00:00+00:00") by the parser so
61+
// Home Assistant's timestamp device_class accepts it.
62+
std::optional<std::string> water_filter_end_date;
5863
};
5964

6065
struct DishwasherState {

‎tests/cpp/dispatch_test.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ struct FridgeRecorder : CommonRecorder {
7171
void publish_crisp_set_temp(float v) { floats["crisp_set_temp"] = v; }
7272
void publish_air_filter_pct(float v) { floats["air_filter_pct"] = v; }
7373
void publish_water_filter_pct(float v) { floats["water_filter_pct"] = v; }
74+
void publish_water_filter_gal(float v) { floats["water_filter_gal"] = v; }
75+
void publish_water_filter_end_date(const std::string &v) {
76+
strings["water_filter_end_date"] = v;
77+
}
7478
};
7579

7680
struct DishwasherRecorder : CommonRecorder {

‎tests/cpp/protocol_test.cpp‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ json fridge_to_json(const FridgeState &s) {
109109
OPT_PUT(o, s, air_filter_on);
110110
OPT_PUT(o, s, air_filter_pct_remaining);
111111
OPT_PUT(o, s, water_filter_pct_remaining);
112+
OPT_PUT(o, s, water_filter_gal_remaining);
113+
OPT_PUT(o, s, water_filter_end_date);
112114
return o;
113115
}
114116

@@ -289,6 +291,32 @@ TEST(ProtocolTest, StatusNonZeroIsInvalid) {
289291
EXPECT_FALSE(f.valid);
290292
}
291293

294+
TEST(ProtocolTest, FridgeWaterFilterGallonsAndEndDate) {
295+
auto f = parse_fridge(R"({"status":0,"resp":{
296+
"water_filter_pct_remaining": 50,
297+
"water_filter_gal_remaining": 162.5,
298+
"water_filter_end_date": "2027-05-10"
299+
}})");
300+
ASSERT_TRUE(f.valid);
301+
ASSERT_TRUE(f.water_filter_gal_remaining.has_value());
302+
EXPECT_FLOAT_EQ(*f.water_filter_gal_remaining, 162.5f);
303+
ASSERT_TRUE(f.water_filter_end_date.has_value());
304+
// Date-only input is promoted to a full ISO8601 timestamp so HA's
305+
// `timestamp` device_class accepts the string.
306+
EXPECT_EQ(*f.water_filter_end_date, "2027-05-10T00:00:00+00:00");
307+
}
308+
309+
TEST(ProtocolTest, FridgeWaterFilterEndDatePreservesFullTimestamp) {
310+
// If a future firmware ever sends a full timestamp, pass it through
311+
// unmodified.
312+
auto f = parse_fridge(R"({"status":0,"resp":{
313+
"water_filter_end_date": "2027-05-10T12:34:56Z"
314+
}})");
315+
ASSERT_TRUE(f.valid);
316+
ASSERT_TRUE(f.water_filter_end_date.has_value());
317+
EXPECT_EQ(*f.water_filter_end_date, "2027-05-10T12:34:56Z");
318+
}
319+
292320
TEST(ProtocolTest, FridgeDoorFallsBackToGenericDoor) {
293321
auto f =
294322
parse_fridge(R"({"seq":1,"props":{"door_ajar":true},"msg_types":2})");

‎tests/fixtures/fridge_back_2028_d5_full.expected.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"air_filter_on": true,
99
"air_filter_pct_remaining": 0,
1010
"water_filter_pct_remaining": 0,
11+
"water_filter_gal_remaining": 107,
12+
"water_filter_end_date": "2023-10-04T00:00:00+00:00",
1113
"common": {
1214
"sabbath_on": false,
1315
"service_required": false,

0 commit comments

Comments
 (0)