Merge pull request #4 from wserr/feat/add-power-control
Feat/add power control
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -8,3 +8,12 @@ configuration-production.yaml
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
config/
|
config/
|
||||||
|
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
|
||||||
|
[Oo]bj/
|
||||||
|
.aider*
|
||||||
|
.env
|
||||||
|
|||||||
@@ -135,3 +135,7 @@ services:
|
|||||||
# Only enable if external services present
|
# Only enable if external services present
|
||||||
# ports:
|
# ports:
|
||||||
# - 4317:4317 # OTLP gRPC receiver
|
# - 4317:4317 # OTLP gRPC receiver
|
||||||
|
power_control:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./dockerfiles/power_control.Dockerfile
|
||||||
|
|||||||
24
dockerfiles/power_control.Dockerfile
Normal file
24
dockerfiles/power_control.Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0.1-noble-arm64v8 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0.101-noble-arm64v8 AS build
|
||||||
|
COPY ./microservices/power_control/*.csproj /microservices/power_control/
|
||||||
|
|
||||||
|
WORKDIR /microservices/power_control
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
COPY ./microservices/power_control /microservices/power_control
|
||||||
|
|
||||||
|
WORKDIR /microservices/power_control
|
||||||
|
# This stage is used to publish the service project to be copied to the final stage
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION
|
||||||
|
RUN dotnet publish "./power_control.csproj" -c Release -o /app/publish
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "power_control.dll"]
|
||||||
86
microservices/power_control/Program.cs
Normal file
86
microservices/power_control/Program.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add JSON serialization services
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.PropertyNameCaseInsensitive = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapPost(
|
||||||
|
"/high_power_alert",
|
||||||
|
async (AlertBody body) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("high power alert was received.");
|
||||||
|
using var client = new HttpClient();
|
||||||
|
var heatPumpState = await client.GetFromJsonAsync<HeatPumpStatus>("http://192.168.1.121/control");
|
||||||
|
var message = "";
|
||||||
|
Console.WriteLine(heatPumpState.heatpump.power);
|
||||||
|
switch (body.Status)
|
||||||
|
{
|
||||||
|
case "firing":
|
||||||
|
Console.WriteLine("Peak too high. Shutting down heat pump.");
|
||||||
|
message = "@everyone peak is too high. Disabling heat pump. 🥶";
|
||||||
|
if (heatPumpState.heatpump.power == "on")
|
||||||
|
{
|
||||||
|
await client.GetAsync(
|
||||||
|
"http://192.168.1.121/control?cmd=heatpump&set_power_mode=off"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = "@everyone high power alert, but heat pump could not be disabled because state was not 'ON'";
|
||||||
|
}
|
||||||
|
await client.PostAsJsonAsync<DiscordMessage>(
|
||||||
|
"https://discord.com/api/webhooks/1455072237207158981/C9qvSIGMZVc60VwpZizxqKigyzvA182RDSdt9k8qtWTq-fBgzlJHh53wAYIqYkGNkBM3",
|
||||||
|
new DiscordMessage(message, "willemBot")
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "resolved":
|
||||||
|
Console.WriteLine("Peak acceptable. Re enabling heat pump. 🌶️");
|
||||||
|
message = "Re enabled heat pump.";
|
||||||
|
if (heatPumpState.heatpump.power == "off")
|
||||||
|
{
|
||||||
|
await client.GetAsync(
|
||||||
|
"http://192.168.1.121/control?cmd=heatpump&set_power_mode=on"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = "@everyone Heat pump could not be re enabled because state was not 'ON'";
|
||||||
|
}
|
||||||
|
await client.PostAsJsonAsync<DiscordMessage>(
|
||||||
|
"https://discord.com/api/webhooks/1455072237207158981/C9qvSIGMZVc60VwpZizxqKigyzvA182RDSdt9k8qtWTq-fBgzlJHh53wAYIqYkGNkBM3",
|
||||||
|
new DiscordMessage(message, "willemBot")
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.WriteLine("Unknown body type");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.WithName("HighPowerAlert");
|
||||||
|
|
||||||
|
app.MapPost(
|
||||||
|
"/low_power_alert",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("low power alert was received.");
|
||||||
|
return new OkResult();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.WithName("Low Power Alert");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
record AlertBody(string Status);
|
||||||
|
|
||||||
|
record DiscordMessage(string content, string userName);
|
||||||
|
|
||||||
|
record HeatPumpStatus(HeatPumpStatusBody heatpump);
|
||||||
|
|
||||||
|
record HeatPumpStatusBody(string power);
|
||||||
51
microservices/power_control/example_body_firing.json
Normal file
51
microservices/power_control/example_body_firing.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"receiver": "REST API",
|
||||||
|
"status": "firing",
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"status": "firing",
|
||||||
|
"labels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"domain": "sensor",
|
||||||
|
"entity_id": "thienpont_serruys_load_grid",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"description": "Disable some devices to lower the peak.",
|
||||||
|
"summary": "Peak is too high!"
|
||||||
|
},
|
||||||
|
"startsAt": "2025-12-29T04:58:50Z",
|
||||||
|
"endsAt": "0001-01-01T00:00:00Z",
|
||||||
|
"generatorURL": "http://localhost:3000/alerting/grafana/ef6m5j923ruv4f/view?orgId=1",
|
||||||
|
"fingerprint": "2c3f0f9ffb7d8c5d",
|
||||||
|
"silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=__alert_rule_uid__%3Def6m5j923ruv4f\u0026matcher=domain%3Dsensor\u0026matcher=entity_id%3Dthienpont_serruys_load_grid\u0026orgId=1",
|
||||||
|
"dashboardURL": "http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984335061",
|
||||||
|
"panelURL": "http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984335061\u0026viewPanel=3",
|
||||||
|
"values": { "B": 4015.25, "C": 1 },
|
||||||
|
"valueString": "[ var=\u0027B\u0027 labels={domain=sensor, entity_id=thienpont_serruys_load_grid} value=4015.25 ], [ var=\u0027C\u0027 labels={domain=sensor, entity_id=thienpont_serruys_load_grid} value=1 ]",
|
||||||
|
"orgId": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupLabels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"commonLabels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"domain": "sensor",
|
||||||
|
"entity_id": "thienpont_serruys_load_grid",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"commonAnnotations": {
|
||||||
|
"description": "Disable some devices to lower the peak.",
|
||||||
|
"summary": "Peak is too high!"
|
||||||
|
},
|
||||||
|
"externalURL": "http://localhost:3000/",
|
||||||
|
"version": "1",
|
||||||
|
"groupKey": "{}/{__grafana_autogenerated__=\u0022true\u0022}/{__grafana_receiver__=\u0022REST API\u0022}:{alertname=\u0022Alert if peak is too high\u0022, grafana_folder=\u0022Power\u0022}",
|
||||||
|
"truncatedAlerts": 0,
|
||||||
|
"orgId": 1,
|
||||||
|
"title": "[FIRING:1] Alert if peak is too high Power (sensor thienpont_serruys_load_grid)",
|
||||||
|
"state": "alerting",
|
||||||
|
"message": "**Firing**\n\nValue: B=4015.25, C=1\nLabels:\n - alertname = Alert if peak is too high\n - domain = sensor\n - entity_id = thienpont_serruys_load_grid\n - grafana_folder = Power\nAnnotations:\n - description = Disable some devices to lower the peak.\n - summary = Peak is too high!\nSource: http://localhost:3000/alerting/grafana/ef6m5j923ruv4f/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=__alert_rule_uid__%3Def6m5j923ruv4f\u0026matcher=domain%3Dsensor\u0026matcher=entity_id%3Dthienpont_serruys_load_grid\u0026orgId=1\nDashboard: http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984335061\nPanel: http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984335061\u0026viewPanel=3\n"
|
||||||
|
}
|
||||||
53
microservices/power_control/example_body_resolved.json
Normal file
53
microservices/power_control/example_body_resolved.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"receiver": "REST API",
|
||||||
|
"status": "resolved",
|
||||||
|
"alerts": [
|
||||||
|
{
|
||||||
|
"status": "resolved",
|
||||||
|
"labels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"domain": "sensor",
|
||||||
|
"entity_id": "thienpont_serruys_load_grid",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"annotations": {
|
||||||
|
"description": "Disable some devices to lower the peak.",
|
||||||
|
"grafana_state_reason": "Updated",
|
||||||
|
"summary": "Peak is too high!"
|
||||||
|
},
|
||||||
|
"startsAt": "2025-12-29T04:58:50Z",
|
||||||
|
"endsAt": "2025-12-29T04:59:25.018228513Z",
|
||||||
|
"generatorURL": "http://localhost:3000/alerting/grafana/ef6m5j923ruv4f/view?orgId=1",
|
||||||
|
"fingerprint": "2c3f0f9ffb7d8c5d",
|
||||||
|
"silenceURL": "http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=__alert_rule_uid__%3Def6m5j923ruv4f\u0026matcher=domain%3Dsensor\u0026matcher=entity_id%3Dthienpont_serruys_load_grid\u0026orgId=1",
|
||||||
|
"dashboardURL": "http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984365018",
|
||||||
|
"panelURL": "http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984365018\u0026viewPanel=3",
|
||||||
|
"values": null,
|
||||||
|
"valueString": "[ var=\u0027B\u0027 labels={domain=sensor, entity_id=thienpont_serruys_load_grid} value=3826.9 ], [ var=\u0027C\u0027 labels={domain=sensor, entity_id=thienpont_serruys_load_grid} value=1 ]",
|
||||||
|
"orgId": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupLabels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"commonLabels": {
|
||||||
|
"alertname": "Alert if peak is too high",
|
||||||
|
"domain": "sensor",
|
||||||
|
"entity_id": "thienpont_serruys_load_grid",
|
||||||
|
"grafana_folder": "Power"
|
||||||
|
},
|
||||||
|
"commonAnnotations": {
|
||||||
|
"description": "Disable some devices to lower the peak.",
|
||||||
|
"grafana_state_reason": "Updated",
|
||||||
|
"summary": "Peak is too high!"
|
||||||
|
},
|
||||||
|
"externalURL": "http://localhost:3000/",
|
||||||
|
"version": "1",
|
||||||
|
"groupKey": "{}/{__grafana_autogenerated__=\u0022true\u0022}/{__grafana_receiver__=\u0022REST API\u0022}:{alertname=\u0022Alert if peak is too high\u0022, grafana_folder=\u0022Power\u0022}",
|
||||||
|
"truncatedAlerts": 0,
|
||||||
|
"orgId": 1,
|
||||||
|
"title": "[RESOLVED] Alert if peak is too high Power (sensor thienpont_serruys_load_grid)",
|
||||||
|
"state": "ok",
|
||||||
|
"message": "**Resolved**\n\nValue: [no value]\nLabels:\n - alertname = Alert if peak is too high\n - domain = sensor\n - entity_id = thienpont_serruys_load_grid\n - grafana_folder = Power\nAnnotations:\n - description = Disable some devices to lower the peak.\n - grafana_state_reason = Updated\n - summary = Peak is too high!\nSource: http://localhost:3000/alerting/grafana/ef6m5j923ruv4f/view?orgId=1\nSilence: http://localhost:3000/alerting/silence/new?alertmanager=grafana\u0026matcher=__alert_rule_uid__%3Def6m5j923ruv4f\u0026matcher=domain%3Dsensor\u0026matcher=entity_id%3Dthienpont_serruys_load_grid\u0026orgId=1\nDashboard: http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984365018\nPanel: http://localhost:3000/d/2990096a-3ff9-4217-a629-2152a260b6b6?from=1766980730000\u0026orgId=1\u0026to=1766984365018\u0026viewPanel=3\n"
|
||||||
|
}
|
||||||
140
microservices/power_control/example_heat_pump_status.json
Normal file
140
microservices/power_control/example_heat_pump_status.json
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"fw": {
|
||||||
|
"curr": "3.1.7",
|
||||||
|
"last": "3.1.7",
|
||||||
|
"model": "ME_CN105_ATW_WIFI",
|
||||||
|
"upd": true
|
||||||
|
},
|
||||||
|
"heatpump": {
|
||||||
|
"boiler_flow_temperature": 25.0,
|
||||||
|
"boiler_return_temperature": 25.0,
|
||||||
|
"booster_active": false,
|
||||||
|
"compressor_frequency": 0,
|
||||||
|
"compressor_on": false,
|
||||||
|
"controller_datetime": "2025-12-29 07:02:35",
|
||||||
|
"controller_version": 3,
|
||||||
|
"defrost_active": false,
|
||||||
|
"dhw": {
|
||||||
|
"flow_target_temperature": 50.0,
|
||||||
|
"flow_temperature_drop": 10.0,
|
||||||
|
"forced_active": "off",
|
||||||
|
"legionella_prevention_target_temp": 60.0,
|
||||||
|
"mode": "off",
|
||||||
|
"temperature": 51.5,
|
||||||
|
"temperature_secondary": 25.0
|
||||||
|
},
|
||||||
|
"dipswitch": {
|
||||||
|
"sw1": "10110110",
|
||||||
|
"sw2": "10101000",
|
||||||
|
"sw3": "00010000",
|
||||||
|
"sw4": "00000000",
|
||||||
|
"sw5": "00111110",
|
||||||
|
"sw6": "00000000"
|
||||||
|
},
|
||||||
|
"energy": {
|
||||||
|
"consumed_cooling": 0.0,
|
||||||
|
"consumed_dhw": 3.6,
|
||||||
|
"consumed_heating": 72.2,
|
||||||
|
"delivered_cooling": 0.0,
|
||||||
|
"delivered_dhw": 8.6,
|
||||||
|
"delivered_heating": 204.1
|
||||||
|
},
|
||||||
|
"energy_consumed_increasing": 128.1,
|
||||||
|
"fault_code": "No error",
|
||||||
|
"feed_temperature": 38.5,
|
||||||
|
"flowrate": 26,
|
||||||
|
"heat_source": 0,
|
||||||
|
"holiday_mode": "off",
|
||||||
|
"immersion_active": false,
|
||||||
|
"in1_thermostat_request": false,
|
||||||
|
"in5_thermostat_request": false,
|
||||||
|
"in6_thermostat_request": false,
|
||||||
|
"indicative_output_power": 0.0,
|
||||||
|
"max_flow_temperature": 40.0,
|
||||||
|
"min_flow_temperature": 25.0,
|
||||||
|
"mixing_tank_temperature": 25.0,
|
||||||
|
"mixing_valve_status": 4,
|
||||||
|
"mixing_valve_step": 0,
|
||||||
|
"mrc_flag": 7,
|
||||||
|
"multizone_status": 0,
|
||||||
|
"operation": "heat_on",
|
||||||
|
"output_power": 5,
|
||||||
|
"outside_temperature": 0.0,
|
||||||
|
"power": "on",
|
||||||
|
"prohibit": {
|
||||||
|
"cooling_zone1": "off",
|
||||||
|
"cooling_zone2": "off",
|
||||||
|
"dhw": "off",
|
||||||
|
"heating_zone1": "off",
|
||||||
|
"heating_zone2": "off",
|
||||||
|
"server_control": "on"
|
||||||
|
},
|
||||||
|
"rcdischargeTemp": 0.0,
|
||||||
|
"rcdischargesuperheattemp": 0.0,
|
||||||
|
"rcoucompressorsurfacetemp": 0.0,
|
||||||
|
"rcouheatsinktemp": 0.0,
|
||||||
|
"rcouliquidpipetemp": 0.0,
|
||||||
|
"rcousuctionpipetemp": 0.0,
|
||||||
|
"rcoutwoPhasepipetemp": 0.0,
|
||||||
|
"rcsubcooltemp": 0.0,
|
||||||
|
"refrigerant_condensing_temperature": 40.6,
|
||||||
|
"refrigerant_liquid_temperature": 29.5,
|
||||||
|
"return_temperature": 35.5,
|
||||||
|
"runtime": 8847.0,
|
||||||
|
"threeway_valve2_active": true,
|
||||||
|
"threeway_valve_active": false,
|
||||||
|
"waterpump2_active": true,
|
||||||
|
"waterpump3_active": false,
|
||||||
|
"waterpump_active": true,
|
||||||
|
"zone1": {
|
||||||
|
"feed_temperature": 36.0,
|
||||||
|
"flow_target_temperature": 46.5,
|
||||||
|
"mode": "HEAT_COMPENSATION_CURVE",
|
||||||
|
"return_temperature": 32.0,
|
||||||
|
"room_target_temperature": 18.0,
|
||||||
|
"room_temperature": 18.0
|
||||||
|
},
|
||||||
|
"zone2": {
|
||||||
|
"feed_temperature": 25.0,
|
||||||
|
"flow_target_temperature": 35.0,
|
||||||
|
"mode": "HEAT_FLOW_TEMP",
|
||||||
|
"return_temperature": 25.0,
|
||||||
|
"room_target_temperature": 635.4,
|
||||||
|
"room_temperature": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"host": "heatpump"
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"broker": "mqtt://192.168.1.81",
|
||||||
|
"con": true,
|
||||||
|
"ena": true,
|
||||||
|
"ha_auto_disc": true,
|
||||||
|
"port": 1883,
|
||||||
|
"pub_sys_data": true,
|
||||||
|
"sts": "OK",
|
||||||
|
"user": "admin"
|
||||||
|
},
|
||||||
|
"sys": {
|
||||||
|
"bl": "V0.5",
|
||||||
|
"conext": true,
|
||||||
|
"conhp": true,
|
||||||
|
"dbg": false,
|
||||||
|
"frname": "Living room",
|
||||||
|
"hw": "ME_V4.1",
|
||||||
|
"id": "273526330095088",
|
||||||
|
"lang": "ENG",
|
||||||
|
"name": "HVAC_4FC5F8",
|
||||||
|
"scale": "C",
|
||||||
|
"sts": 3,
|
||||||
|
"stsled": true,
|
||||||
|
"up": 1799393
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"ip": "192.168.1.121",
|
||||||
|
"mac": "F0:F5:BD:4F:C5:F8",
|
||||||
|
"rssi": -33,
|
||||||
|
"ssid": "Pladijs124"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
microservices/power_control/power_control.csproj
Normal file
13
microservices/power_control/power_control.csproj
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user