Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] weekly_schedule expose #5550

Merged
merged 5 commits into from
May 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions converters/toZigbee.js
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,29 @@ const converters = {
thermostat_weekly_schedule: {
key: ['weekly_schedule'],
convertSet: async (entity, key, value, meta) => {
/*
* We want to support a simple human creatable format to send a schedule:
{"weekly_schedule": {
"dayofweek": ["monday", "tuesday"],
"transitions": [
{"heatSetpoint": 16, "transitionTime": "0:00"},
{"heatSetpoint": 20, "transitionTime": "18:00"},
{"heatSetpoint": 16, "transitionTime": "19:30"}
]}}

* However exposes is not flexible enough to describe something like this. There is a
* much more verbose format we also support so that exposes work.
{"weekly_schedule": {
"dayofweek": [
{"day": "monday"},
{"day": "tuesday"}
],
"transitions": [
{"heatSetpoint": 16, "transitionTime": {"hour": 0, "minute": 0}},
{"heatSetpoint": 20, "transitionTime": {"hour": 18, "minute": 0}},
{"heatSetpoint": 16, "transitionTime": {"hour": 19, "minute": 30}}
]}}
*/
const payload = {
dayofweek: value.dayofweek,
transitions: value.transitions,
Expand All @@ -1229,16 +1252,19 @@ const converters = {
if (Array.isArray(payload.transitions)) {
// calculate numoftrans
if (typeof value.numoftrans !== 'undefined') {
meta.logger.warn(`weekly_schedule: igonoring provided numoftrans value (${JSON.stringify(value.numoftrans)}),\
this is now calculated automatically`);
meta.logger.warn(
`weekly_schedule: ignoring provided numoftrans value (${JSON.stringify(value.numoftrans)}), ` +
'this is now calculated automatically',
);
}
payload.numoftrans = payload.transitions.length;

// mode is calculated below
if (typeof value.mode !== 'undefined') {
meta.logger.warn(
`weekly_schedule: igonoring provided mode value (${JSON.stringify(value.mode)}),\
this is now calculated automatically`);
`weekly_schedule: ignoring provided mode value (${JSON.stringify(value.mode)}), ` +
'this is now calculated automatically',
);
}
payload.mode = [];

Expand Down Expand Up @@ -1270,6 +1296,27 @@ const converters = {
} else {
elem['transitionTime'] = ((parseInt(time[0]) * 60) + parseInt(time[1]));
}
} else if (typeof elem['transitionTime'] === 'object') {
if (!elem['transitionTime'].hasOwnProperty('hour') || !elem['transitionTime'].hasOwnProperty('minute')) {
throw new Error(
'weekly_schedule: expected 24h time object (e.g. {"hour": 19, "minute": 30}), ' +
`but got '${JSON.stringify(elem['transitionTime'])}'!`,
);
} else if (isNaN(elem['transitionTime']['hour'])) {
throw new Error(
'weekly_schedule: expected time.hour to be a number, ' +
`but got '${elem['transitionTime']['hour']}'!`,
);
} else if (isNaN(elem['transitionTime']['minute'])) {
throw new Error(
'weekly_schedule: expected time.minute to be a number, ' +
`but got '${elem['transitionTime']['minute']}'!`,
);
} else {
elem['transitionTime'] = (
(parseInt(elem['transitionTime']['hour']) * 60) + parseInt(elem['transitionTime']['minute'])
);
}
}
}
} else {
Expand All @@ -1291,6 +1338,15 @@ const converters = {
if (Array.isArray(payload.dayofweek)) {
let dayofweek = 0;
for (let d of payload.dayofweek) {
if (typeof d === 'object') {
if (!d.hasOwnProperty('day')) {
throw new Error(
'weekly_schedule: expected dayofweek to be string or {"day": "str"}, ' +
`but got '${JSON.strinify(d)}'!`,
);
}
d = d.day;
}
// lookup dayofweek bit
d = utils.getKey(constants.thermostatDayOfWeek, d.toLowerCase(), d, Number);
dayofweek |= (1 << d);
Expand Down
3 changes: 2 additions & 1 deletion devices/ubisys.js
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,8 @@ module.exports = [
.withRunningMode(['off', 'heat'])
.withSetpoint('occupied_heating_setpoint', 7, 30, 0.5)
.withLocalTemperature()
.withPiHeatingDemand(ea.STATE_GET),
.withPiHeatingDemand(ea.STATE_GET)
.withWeeklySchedule(['heat']),
exposes.binary('vacation_mode', ea.STATE_GET, true, false)
.withDescription('When Vacation Mode is active the schedule is disabled and unoccupied_heating_setpoint is used.'),
],
Expand Down
6 changes: 4 additions & 2 deletions devices/viessmann.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ module.exports = [
fromZigbee: [fz.legacy.viessmann_thermostat_att_report, fz.battery, fz.legacy.hvac_user_interface],
toZigbee: [tz.thermostat_local_temperature, tz.thermostat_occupied_heating_setpoint, tz.thermostat_control_sequence_of_operation,
tz.thermostat_system_mode, tz.thermostat_keypad_lockout, tz.viessmann_window_open, tz.viessmann_window_open_force,
tz.viessmann_assembly_mode,
tz.viessmann_assembly_mode, tz.thermostat_weekly_schedule, tz.thermostat_clear_weekly_schedule,
],
exposes: [
exposes.climate().withSetpoint('occupied_heating_setpoint', 7, 30, 1)
.withLocalTemperature().withSystemMode(['heat', 'sleep']),
.withLocalTemperature()
.withSystemMode(['heat', 'sleep'])
.withWeeklySchedule(['heat']),
exposes.binary('window_open', ea.STATE_GET, true, false)
.withDescription('Detected by sudden temperature drop or set manually.'),
exposes.binary('window_open_force', ea.ALL, true, false)
Expand Down
32 changes: 32 additions & 0 deletions lib/exposes.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,38 @@ class Climate extends Base {
this.features.push(new Enum('ac_louver_position', access, positions).withDescription('Ac louver position of this device'));
return this;
}

withWeeklySchedule(modes, access=a.ALL) {
assert(!this.endpoint, 'Cannot add feature after adding endpoint');
const allowed = ['heat', 'cool'];
modes.forEach((m) => assert(allowed.includes(m)));

const featureDayOfWeek = new List('dayofweek', a.SET, new Composite('day', 'dayofweek', a.SET).withFeature(new Enum('day', a.SET, [
'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
'saturday', 'sunday', 'away_or_vacation',
]))).withLengthMin(1).withLengthMax(8).withDescription('Days on which the schedule will be active.');

const featureTransitionTime = new Composite('time', 'transitionTime', a.SET)
.withFeature(new Numeric('hour', a.SET))
.withFeature(new Numeric('minute', a.SET))
.withDescription('Trigger transition X minutes after 00:00.');
const featureTransitionHeatSetPoint = new Numeric('heatSetpoint', a.SET)
.withDescription('Target heat setpoint');
const featureTransitionCoolSetPoint = new Numeric('coolSetpoint', a.SET)
.withDescription('Target cool setpoint');
let featureTransition = new Composite('transition', 'transition', a.SET).withFeature(featureTransitionTime);
if (modes.includes('heat')) featureTransition = featureTransition.withFeature(featureTransitionHeatSetPoint);
if (modes.includes('cool')) featureTransition = featureTransition.withFeature(featureTransitionCoolSetPoint);
const featureTransitions = new List('transitions', a.SET, featureTransition)
.withLengthMin(1).withLengthMax(10);

const schedule = new Composite('schedule', 'weekly_schedule', access)
.withFeature(featureDayOfWeek)
.withFeature(featureTransitions);

this.features.push(schedule);
return this;
}
}
/**
* The access property is a 3-bit bitmask.
Expand Down