3 Commits

Author SHA1 Message Date
abratchik
16001822ac Merge 24342db40e into 0f4fa2e406 2025-08-14 20:55:04 +02:00
dependabot[bot]
0f4fa2e406 chore(deps): bump actions/checkout from 4 to 5 (#169)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 13:24:55 +03:00
abratchik
24342db40e Adding ext device support 2025-07-11 12:54:52 +03:00
14 changed files with 384 additions and 156 deletions

View File

@@ -15,7 +15,7 @@ jobs:
name: run PlatformIO Dependabot
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: run PlatformIO Dependabot
uses: peterus/platformio_dependabot@v1.2.0
with:

View File

@@ -418,25 +418,26 @@ public:
bool publishSwitchHeatingTurbo(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_turbo"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Turbo heating");
doc[FPSTR(HA_ICON)] = F("mdi:rocket-launch-outline");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_STATE_ON)] = true;
doc[FPSTR(HA_STATE_OFF)] = false;
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.turbo }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"heating\": {\"turbo\" : true}}");
doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"heating\": {\"turbo\" : false}}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return publishSwitch(
F("heating_turbo"),
F("Turbo heating"),
F("mdi:rocket-launch-outline"),
F("{{ value_json.heating.turbo }}"),
F("{\"heating\": {\"turbo\" : true}}"),
F("{\"heating\": {\"turbo\" : false}}"),
enabledByDefault
);
}
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_turbo")).c_str(), doc);
bool publishSwitchExtDevice(const String& caption, bool enabledByDefault = true) {
return publishSwitch(
F("extdev"),
caption,
F("mdi:toggle-switch-outline"),
F("{{ value_json.externalDev.state }}"),
F("{\"externalDev\": {\"state\" : true}}"),
F("{\"externalDev\": {\"state\" : false}}"),
enabledByDefault );
}
bool publishInputHeatingHysteresis(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) {
@@ -634,26 +635,18 @@ public:
bool publishSwitchPid(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("PID");
doc[FPSTR(HA_ICON)] = F("mdi:chart-bar-stacked");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_STATE_ON)] = true;
doc[FPSTR(HA_STATE_OFF)] = false;
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.enabled }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"pid\": {\"enabled\" : true}}");
doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"pid\": {\"enabled\" : false}}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("pid")).c_str(), doc);
return publishSwitch(
F("pid"),
F("PID"),
F("mdi:chart-bar-stacked"),
F("{{ value_json.pid.enabled }}"),
F("{\"pid\": {\"enabled\" : true}}"),
F("{\"pid\": {\"enabled\" : false}}"),
enabledByDefault
);
}
bool publishInputPidFactorP(bool enabledByDefault = true) {
JsonDocument doc;
@@ -819,25 +812,16 @@ public:
bool publishSwitchEquitherm(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm");
doc[FPSTR(HA_ICON)] = F("mdi:sun-snowflake-variant");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_STATE_ON)] = true;
doc[FPSTR(HA_STATE_OFF)] = false;
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.enabled }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"equitherm\": {\"enabled\" : true}}");
doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"equitherm\": {\"enabled\" : false}}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("equitherm")).c_str(), doc);
return publishSwitch(
F("equitherm"),
F("Equitherm"),
F("mdi:sun-snowflake-variant"),
F("{{ value_json.equitherm.enabled }}"),
F("{\"equitherm\": {\"enabled\" : true}}"),
F("{\"equitherm\": {\"enabled\" : false}}"),
enabledByDefault
);
}
bool publishInputEquithermFactorN(bool enabledByDefault = true) {
@@ -967,126 +951,87 @@ public:
}
bool publishHeatingState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
//doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("running");
doc[FPSTR(HA_NAME)] = F("Heating");
doc[FPSTR(HA_ICON)] = F("mdi:radiator");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.heating.active, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("heating")).c_str(), doc);
return publishBinarySensorState(
F("heating"),
F("Heating"),
F("mdi:radiator"),
F("{{ iif(value_json.slave.heating.active, 'ON', 'OFF') }}"),
F("running"),
enabledByDefault
);
}
bool publishDhwState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
//doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("running");
doc[FPSTR(HA_NAME)] = F("DHW");
doc[FPSTR(HA_ICON)] = F("mdi:faucet");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.dhw.active, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str(), doc);
return publishBinarySensorState(
F("dhw"),
F("DHW"),
F("mdi:faucet"),
F("{{ iif(value_json.slave.dhw.active, 'ON', 'OFF') }}"),
F("running"),
enabledByDefault
);
}
bool publishFlameState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("flame"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
//doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("running");
doc[FPSTR(HA_NAME)] = F("Flame");
doc[FPSTR(HA_ICON)] = F("mdi:gas-burner");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.flame, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("flame")).c_str(), doc);
return publishBinarySensorState(
F("flame"),
F("Flame"),
F("mdi:gas-burner"),
F("{{ iif(value_json.slave.flame, 'ON', 'OFF') }}"),
F("running"),
enabledByDefault
);
}
bool publishFaultState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("fault"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("problem");
doc[FPSTR(HA_NAME)] = F("Fault");
doc[FPSTR(HA_ICON)] = F("mdi:alert-remove-outline");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.fault.active, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("fault")).c_str(), doc);
return publishBinarySensorState(
F("fault"),
F("Fault"),
F("mdi:alert-remove-outline"),
F("{{ iif(value_json.slave.fault.active, 'ON', 'OFF') }}"),
F("problem"),
enabledByDefault
);
}
bool publishDiagState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("problem");
doc[FPSTR(HA_NAME)] = F("Diagnostic");
doc[FPSTR(HA_ICON)] = F("mdi:account-wrench");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.diag.active, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC)).c_str(), doc);
return publishBinarySensorState(
F("diag"),
F("Diagnostic"),
F("mdi:account-wrench"),
F("{{ iif(value_json.slave.diag.active, 'ON', 'OFF') }}"),
F("problem"),
enabledByDefault
);
}
bool publishExternalPumpState(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("ext_pump"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC);
doc[FPSTR(HA_DEVICE_CLASS)] = F("running");
doc[FPSTR(HA_NAME)] = F("External pump");
doc[FPSTR(HA_ICON)] = F("mdi:pump");
doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.master.externalPump.state, 'ON', 'OFF') }}");
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ext_pump")).c_str(), doc);
return publishBinarySensorState(
F("ext_pump"),
F("External pump"),
F("mdi:pump"),
F("{{ iif(value_json.master.externalPump.state, 'ON', 'OFF') }}"),
F("running"),
enabledByDefault
);
}
bool publishExtDevState(const String& caption, bool enabledByDefault = true) {
return publishBinarySensorState(
F("extdev"),
caption,
F("mdi:toggle-switch"),
F("{{ iif(value_json.master.externalDev.state, 'ON', 'OFF') }}"),
F("running"),
enabledByDefault
);
}
bool publishFaultCode(bool enabledByDefault = true) {
@@ -1357,6 +1302,79 @@ public:
protected:
unsigned short expireAfter = 300u;
String statusTopic, stateTopic, setStateTopic, settingsTopic, setSettingsTopic;
void initCommonDocFields(JsonDocument& doc,
const String& uniqueId,
const String& name,
const String& icon,
const String& stateTopic,
const String& entityCategory,
bool enabledByDefault) {
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = uniqueId;
doc[FPSTR(HA_OBJECT_ID)] = uniqueId;
doc[FPSTR(HA_ENTITY_CATEGORY)] = entityCategory;
doc[FPSTR(HA_NAME)] = name;
doc[FPSTR(HA_ICON)] = icon;
doc[FPSTR(HA_STATE_TOPIC)] = stateTopic;
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
}
bool publishSwitch(const String& id,
const String& name,
const String& icon,
const String& valueTemplate,
const String& payloadOn,
const String& payloadOff,
bool enabledByDefault) {
JsonDocument doc;
initCommonDocFields(doc,
this->getObjectIdWithPrefix(id),
name,
icon,
this->settingsTopic.c_str(),
FPSTR(HA_ENTITY_CATEGORY_CONFIG),
enabledByDefault );
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = valueTemplate;
doc[FPSTR(HA_STATE_ON)] = true;
doc[FPSTR(HA_STATE_OFF)] = false;
doc[FPSTR(HA_PAYLOAD_ON)] = payloadOn;
doc[FPSTR(HA_PAYLOAD_OFF)] = payloadOff;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), id).c_str(), doc);
}
bool publishBinarySensorState(const String& id,
const String& name,
const String& icon,
const String& valueTemplate,
const String& deviceClass,
bool enabledByDefault) {
JsonDocument doc;
initCommonDocFields(doc,
this->getObjectIdWithPrefix(id),
name,
icon,
this->stateTopic.c_str(),
FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC),
enabledByDefault );
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = JsonString(AVAILABILITY_OT_CONN, true);
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_DEVICE_CLASS)] = deviceClass;
doc[FPSTR(HA_VALUE_TEMPLATE)] = valueTemplate;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), id).c_str(), doc);
}
};
const char HaHelper::AVAILABILITY_OT_CONN[] = "{{ iif(value_json.slave.connected, 'online', 'offline') }}";

View File

@@ -199,6 +199,7 @@ protected:
this->emergency();
this->cascadeControl();
this->externalPump();
this->externalDev();
this->miscRunned = millis();
return true;
@@ -688,4 +689,46 @@ protected:
Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: anti stuck"));
}
}
};
void externalDev() {
static uint8_t configuredGpio = GPIO_IS_NOT_CONFIGURED;
if(!settings.externalDev.use) return;
// configure output
// if settings are different than the configured GPIO, update
if (settings.externalDev.gpio != configuredGpio) {
if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
digitalWrite(configuredGpio, LOW);
}
if (GPIO_IS_VALID(settings.externalDev.gpio)) {
configuredGpio = settings.externalDev.gpio;
pinMode(configuredGpio, OUTPUT);
digitalWrite(configuredGpio, LOW);
} else if (configuredGpio != GPIO_IS_NOT_CONFIGURED) {
configuredGpio = GPIO_IS_NOT_CONFIGURED;
}
}
if (configuredGpio == GPIO_IS_NOT_CONFIGURED) {
if (vars.externalDev.state) {
vars.externalDev.state = false;
Log.sinfoln(FPSTR(L_EXTDEV), F("Disabled: use = off"));
}
return;
}
// output configured update relay if required
if(settings.externalDev.state != vars.externalDev.state ) {
digitalWrite(configuredGpio, settings.externalDev.state? HIGH:LOW );
vars.externalDev.state = settings.externalDev.state;
}
}
};

View File

@@ -506,6 +506,9 @@ protected:
this->haHelper->publishInputEquithermFactorK(false);
this->haHelper->publishInputEquithermFactorT(false);
// ext device
this->haHelper->publishSwitchExtDevice(String(settings.externalDev.caption), false);
// states
this->haHelper->publishStatusState();
this->haHelper->publishEmergencyState();
@@ -515,6 +518,8 @@ protected:
this->haHelper->publishFaultState();
this->haHelper->publishDiagState();
this->haHelper->publishExternalPumpState(false);
this->haHelper->publishExtDevState(String(settings.externalDev.caption), false);
// sensors
this->haHelper->publishFaultCode();

View File

@@ -167,6 +167,13 @@ struct Settings {
unsigned short antiStuckTime = 300;
} externalPump;
struct {
bool use = false;
byte gpio = DEFAULT_EXT_DEV_GPIO;
char caption[41] = DEFAULT_EXT_DEV_CAPTION;
bool state = false;
} externalDev;
struct {
struct {
bool enabled = false;
@@ -279,6 +286,10 @@ struct Variables {
unsigned long lastEnabledTime = 0;
} externalPump;
struct {
bool state = false;
} externalDev;
struct {
bool input = false;
bool output = false;

View File

@@ -146,6 +146,14 @@
#define DEFAULT_EXT_PUMP_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_EXT_DEV_GPIO
#define DEFAULT_EXT_DEV_GPIO GPIO_IS_NOT_CONFIGURED
#endif
#ifndef DEFAULT_EXT_DEV_CAPTION
#define DEFAULT_EXT_DEV_CAPTION "Device"
#endif
#ifndef PROGMEM
#define PROGMEM
#endif

View File

@@ -32,6 +32,7 @@ const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHE
const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT";
const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT";
const char L_EXTPUMP[] PROGMEM = "EXTPUMP";
const char L_EXTDEV[] PROGMEM = "EXTDEV";
const char S_ACTIONS[] PROGMEM = "actions";
@@ -82,6 +83,8 @@ const char S_ENV[] PROGMEM = "env";
const char S_EPC[] PROGMEM = "epc";
const char S_EQUITHERM[] PROGMEM = "equitherm";
const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump";
const char S_EXTERNAL_DEV[] PROGMEM = "externalDev";
const char S_EXTERNAL_DEV_CAPTION[] PROGMEM = "caption";
const char S_FACTOR[] PROGMEM = "factor";
const char S_FAULT[] PROGMEM = "fault";
const char S_FREEZE_PROTECTION[] PROGMEM = "freezeProtection";

View File

@@ -561,6 +561,15 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
cascadeControlOutput[FPSTR(S_ON_LOSS_CONNECTION)] = src.cascadeControl.output.onLossConnection;
cascadeControlOutput[FPSTR(S_ON_ENABLED_HEATING)] = src.cascadeControl.output.onEnabledHeating;
}
if(!safe ) {
dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)] = src.externalDev.use;
dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)] = src.externalDev.gpio;
dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)] = src.externalDev.caption;
}
if(src.externalDev.use)
dst[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)] = src.externalDev.state;
}
inline void safeSettingsToJson(const Settings& src, JsonVariant dst) {
@@ -1531,6 +1540,42 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
// external device
if (src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)].is<bool>()) {
bool value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_USE)].as<bool>();
if (value != dst.externalDev.use) {
dst.externalDev.use = value;
changed = true;
}
}
if (!src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].isNull()) {
if (src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].is<JsonString>() &&
src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].as<JsonString>().size() == 0) {
if (dst.externalDev.gpio != GPIO_IS_NOT_CONFIGURED) {
dst.externalDev.gpio = GPIO_IS_NOT_CONFIGURED;
changed = true;
}
} else {
unsigned char value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_GPIO)].as<unsigned char>();
if (GPIO_IS_VALID(value) && value != dst.externalDev.gpio) {
dst.externalDev.gpio = value;
changed = true;
}
}
}
if (!src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)].isNull()) {
String value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_EXTERNAL_DEV_CAPTION)].as<String>();
if (value.length() < sizeof(dst.externalDev.caption) && !String(dst.externalDev.caption).equals(value)) {
strcpy(dst.externalDev.caption, value.c_str());
changed = true;
}
}
// cascade control
if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_ENABLED)].is<bool>()) {
@@ -1653,6 +1698,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
if (dst.externalDev.use && src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)].is<bool>()) {
bool value = src[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)].as<bool>();
if (value != dst.externalDev.state) {
dst.externalDev.state = value;
changed = true;
}
}
// force check emergency target
{
float value = !src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].isNull() ? src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].as<float>() : dst.emergency.target;
@@ -2118,6 +2172,7 @@ void varsToJson(const Variables& src, JsonVariant dst) {
master[FPSTR(S_MQTT)][FPSTR(S_CONNECTED)] = src.mqtt.connected;
master[FPSTR(S_EMERGENCY)][FPSTR(S_STATE)] = src.emergency.state;
master[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_STATE)] = src.externalPump.state;
master[FPSTR(S_EXTERNAL_DEV)][FPSTR(S_STATE)] = src.externalDev.state;
auto mCascadeControl = master[FPSTR(S_CASCADE_CONTROL)].to<JsonObject>();
mCascadeControl[FPSTR(S_INPUT)] = src.cascadeControl.input;

View File

@@ -104,6 +104,7 @@
"mMqttConnected": "MQTT服务器连接状态",
"mEmergencyState": "应急模式",
"mExtPumpState": "外置循环泵",
"mExtDevState": "外置设备",
"mCascadeControlInput": "Cascade 控制 (input)",
"mCascadeControlOutput": "Cascade 控制 (output)",
@@ -289,6 +290,7 @@
"ot": "OpenTherm协议设置",
"mqtt": "MQTT 服务器设置",
"extPump": "外置循环泵设置",
"extDev": "外置设备设置",
"cascadeControl": "Cascade 级联控制设置"
},
@@ -459,6 +461,13 @@
"antiStuckTime": "防卡死运行时长<small>(分钟)</small>"
},
"extDev": {
"use": "使用外置设备",
"gpio": "GPIO 继电器",
"state": "状态",
"caption": "说明"
},
"cascadeControl": {
"input": {
"desc": "仅当另一台锅炉发生故障时启用本锅炉加热。另一台锅炉的控制器需在故障发生时切换GPIO输入状态以触发本功能。",

View File

@@ -104,6 +104,7 @@
"mMqttConnected": "MQTT connection",
"mEmergencyState": "Emergency mode",
"mExtPumpState": "External pump",
"mExtDevState": "External device",
"mCascadeControlInput": "Cascade control (input)",
"mCascadeControlOutput": "Cascade control (output)",
@@ -289,6 +290,7 @@
"ot": "OpenTherm settings",
"mqtt": "MQTT settings",
"extPump": "External pump settings",
"extDev": "External device settings",
"cascadeControl": "Cascade control settings"
},
@@ -458,6 +460,13 @@
"antiStuckInterval": "Anti stuck interval <small>(days)</small>",
"antiStuckTime": "Anti stuck time <small>(min)</small>"
},
"extDev": {
"use": "Use external device",
"gpio": "Relay GPIO",
"state": "State",
"caption": "Caption"
},
"cascadeControl": {
"input": {

View File

@@ -104,6 +104,7 @@
"mMqttConnected": "Connessione MQTT",
"mEmergencyState": "Modo Emergenza",
"mExtPumpState": "Pompa esterna",
"mExtDevState": "Dispositivo esterno",
"mCascadeControlInput": "Controllo a cascata (input)",
"mCascadeControlOutput": "Controllo a cascata (output)",
@@ -289,6 +290,7 @@
"ot": "Impostazioni OpenTherm",
"mqtt": "Impostazioni MQTT",
"extPump": "Impostazioni pompa esterna",
"extDev": "Impostazioni dispositivo esterno",
"cascadeControl": "Impostazioni controllo a cascata"
},
@@ -459,6 +461,13 @@
"antiStuckTime": "Tempo antiblocco <small>(min)</small>"
},
"extDev": {
"use": "Usa dispositivo esterno",
"gpio": "GPIO relè",
"state": "Stato",
"caption": "Didascalia"
},
"cascadeControl": {
"input": {
"desc": "Può essere attivata la caldaia se un'altra ha fallito. Il controllo dell'altra caldaia cambia lo stato dell'ingresso del GPIO in caso di errore.",

View File

@@ -104,6 +104,7 @@
"mMqttConnected": "Подключение к MQTT",
"mEmergencyState": "Аварийный режим",
"mExtPumpState": "Внешний насос",
"mExtDevState": "Внешнее устройство",
"mCascadeControlInput": "Каскадное управление (вход)",
"mCascadeControlOutput": "Каскадное управление (выход)",
@@ -289,6 +290,7 @@
"ot": "Настройки OpenTherm",
"mqtt": "Настройки MQTT",
"extPump": "Настройки дополнительного насоса",
"extDev": "Настройки внешнего устройства",
"cascadeControl": "Настройки каскадного управления"
},
@@ -459,6 +461,13 @@
"antiStuckTime": "Время работы насоса <small>(в минутах)</small>"
},
"extDev": {
"use": "Использовать внешнее устройство",
"gpio": "GPIO реле",
"state": "Состояние",
"caption": "Назначение"
},
"cascadeControl": {
"input": {
"desc": "Может использоваться для включения отопления только при неисправности другого котла. Контроллер другого котла должен изменить состояние входа GPIO в случае неисправности.",

View File

@@ -135,6 +135,10 @@
<th scope="row" data-i18n>dashboard.states.mExtPumpState</th>
<td><i class="mExtPumpState"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mExtDevState</th>
<td><i class="mExtDevState"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mCascadeControlInput</th>
<td><i class="mCascadeControlInput"></i></td>
@@ -658,6 +662,7 @@
result.master.emergency.state ? "red" : "green"
);
setState('.mExtPumpState', result.master.externalPump.state);
setState('.mExtDevState', result.master.externalDev.state);
setState('.mCascadeControlInput', result.master.cascadeControl.input);
setState('.mCascadeControlOutput', result.master.cascadeControl.output);

View File

@@ -763,6 +763,42 @@
<hr />
<details>
<summary><b data-i18n>settings.section.extDev</b></summary>
<div>
<div id="extdev-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="extdev-settings" class="hidden">
<fieldset>
<label for="extdev-use">
<input type="checkbox" id="extdev-use" name="externalDev[use]" value="false">
<span data-i18n>settings.extDev.use</span>
</label>
<label for="extdev-state">
<input type="checkbox" id="extdev-state" name="externalDev[state]" value="false">
<span data-i18n>settings.extDev.state</span>
</label>
</fieldset>
<div class="grid">
<label for="extdev-gpio">
<span data-i18n>settings.extDev.gpio</span>
<input type="number" inputmode="numeric" id="extdev-gpio" name="externalDev[gpio]" min="0" max="254" step="1">
</label>
<label for="extdev-caption">
<span data-i18n>settings.extDev.caption</span>
<input type="text" id="extdev-caption" name="externalDev[caption]" maxlength="40" required>
</label>
</div>
<button type="submit" data-i18n>button.save</button>
</form>
</div>
</details>
<hr />
<details>
<summary><b data-i18n>settings.section.cascadeControl</b></summary>
<div>
@@ -932,6 +968,13 @@
setInputValue("[name='externalPump[antiStuckTime]']", data.externalPump.antiStuckTime);
setBusy('#extpump-settings-busy', '#extpump-settings', false);
// Extdev
setCheckboxValue('#extdev-use', data.externalDev.use);
setInputValue('#extdev-gpio', data.externalDev.gpio < 255 ? data.externalDev.gpio : '');
setInputValue('#extdev-caption', data.externalDev.caption);
setCheckboxValue('#extdev-state', data.externalDev.state);
setBusy('#extdev-settings-busy', '#extdev-settings', false);
// Cascade control
setCheckboxValue("[name='cascadeControl[input][enabled]']", data.cascadeControl.input.enabled);
setInputValue("[name='cascadeControl[input][gpio]']", data.cascadeControl.input.gpio < 255 ? data.cascadeControl.input.gpio : '');
@@ -1085,6 +1128,7 @@
setupForm('#ot-settings', fillData);
setupForm('#mqtt-settings', fillData, ['mqtt.user', 'mqtt.password', 'mqtt.prefix']);
setupForm('#extpump-settings', fillData);
setupForm('#extdev-settings', fillData);
setupForm('#cc-settings', fillData);
} catch (error) {