diff --git a/.gitignore b/.gitignore index 0f5f168..51e5839 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ configuration-production.yaml node_modules/ config/ + +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Oo]bj/ +.aider* +.env diff --git a/docker-compose.yml b/docker-compose.yml index 6cd6cd7..9c76557 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -135,3 +135,7 @@ services: # Only enable if external services present # ports: # - 4317:4317 # OTLP gRPC receiver + power_control: + build: + context: . + dockerfile: ./dockerfiles/power_control.Dockerfile diff --git a/dockerfiles/power_control.Dockerfile b/dockerfiles/power_control.Dockerfile new file mode 100644 index 0000000..8001f1c --- /dev/null +++ b/dockerfiles/power_control.Dockerfile @@ -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"] diff --git a/microservices/power_control/Program.cs b/microservices/power_control/Program.cs new file mode 100644 index 0000000..53b2c75 --- /dev/null +++ b/microservices/power_control/Program.cs @@ -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("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( + "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( + "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); diff --git a/microservices/power_control/example_body_firing.json b/microservices/power_control/example_body_firing.json new file mode 100644 index 0000000..8d3fe01 --- /dev/null +++ b/microservices/power_control/example_body_firing.json @@ -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" +} diff --git a/microservices/power_control/example_body_resolved.json b/microservices/power_control/example_body_resolved.json new file mode 100644 index 0000000..866b6c5 --- /dev/null +++ b/microservices/power_control/example_body_resolved.json @@ -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" +} diff --git a/microservices/power_control/example_heat_pump_status.json b/microservices/power_control/example_heat_pump_status.json new file mode 100644 index 0000000..2c8eefa --- /dev/null +++ b/microservices/power_control/example_heat_pump_status.json @@ -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" + } +} diff --git a/microservices/power_control/power_control.csproj b/microservices/power_control/power_control.csproj new file mode 100644 index 0000000..63a6d4d --- /dev/null +++ b/microservices/power_control/power_control.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + +