Трошки свіжої та актуальної автоматизаці. До зими докупив до свого інвертора другу літієву батарейку і все добре але є нюанси, основний нюанс в тому що, на час блекауту коли АКБ висаджується в ноль, я викатую невеличкий генератор на 3+квт, від якого йде зарядка та споживання будинку.
Від генератора я можу собі дозволити струм заряду максимум 30А (1500вт) щоб залишилося ще 1500 на будинок в цей час, саме цей струм в мене був виставлений на постійно, і то було досить зручно бо дозволяло коли з'являється мережа за 2-3 години повністю заряджатися.
АЛЕ як акб стало в 2 рази більше - заряджатися постійно струмом 30А стало зовсім не так радісно бо довго, при тому що і інвертор і батарея може значно швидше.
Я вирішив шо хочу мати 3 режими заряджання АКБ
1. дефолтний 30А як раніше, фактично це безпечний режим і для гріда і для інвертора і для генератора і батарейка не перегрівається
2. 70А коли є мережа а АКБ просів нижче 50%, тобто є задача зарядити максимально швидко хоча-б до 50% поки є мережа (пару годин)
3. 50А коли є мережа а заряд став більше 50%
Все начебто логічно, але тут є другий нюанс, 70А при напрузі 55В це вже під 4квт, а в мене інвертор всього 5квт, тобто скоріш за все я не зможу одночасно споживати будинок і заряджати струмом 70А в дефолтному режимі, АЛЕ інвертор можна перевести в режим байпасу, коли він через себе буде тільки заряджати а будинок буде напряму від гріда, при цьому якщо грід пропаде то включиться режим №1
Інвертор в мене підключений до SolarAssistant, який в свою чергу, дає mqtt інтерфейс для отримання данних та конфігурації.
В данному випадку отримую від SolarAssistant струм заряду+встановлення, поточний режим+вствновлення, soc батареї та потужність яка споживається/заливається в батарею.
Bridge mqtt:broker:solar "Solar" @ "MQTT" [ host="192.168.10.60", secure=false, username="***", password="****", retainMessages=false, qos=2, enableDiscovery=false,clientID="openhab34" ]
{
Thing topic SolarAssistant_Inverter_1 "SolarAssistant Inverter 1" @ "SolarAssistant" {
Channels:
Type number : charge-current-max "Max grid charge current" [
stateTopic="solar_assistant/inverter_1/max_grid_charge_current/state",
commandTopic="solar_assistant/inverter_1/max_grid_charge_current/set",
min=10, max=100, step=10,
unit="A"
]
Type string : operation-logic "Operation logic" [
stateTopic="solar_assistant/inverter_1/operation_logic/state",
commandTopic="solar_assistant/inverter_1/operation_logic/set"
]
}
Thing topic SolarAssistant_Total "SolarAssistant Total" @ "SolarAssistant" {
Channels:
Type number : battery-soc "Battery SOC" [
stateTopic="solar_assistant/total/battery_state_of_charge/state",
unit="%"
]
Type number : battery-power "Battery Power" [
stateTopic="solar_assistant/total/battery_power/state",
unit="W"
]
}
}
Ці дані фактично замаплені у відповідні айтеми через проксі:
Number:Dimensionless SA_TOTAL_BATTERYSOC "Battery [%.0f %]" {unit="%", channel="mqtt:topic:solar:SolarAssistant_Total:battery-soc", expire="1m"}
Number:Power SA_TOTAL_BATTERYPOWER "Battery Power [%.0f W]" {channel="mqtt:topic:solar:SolarAssistant_Total:battery-power", expire="1m"}
Number:ElectricCurrent SA_INVERTER1_GRIDMAXCHARGE "Charge Max current [%.0f A]" {channel="mqtt:topic:solar:SolarAssistant_Inverter_1:charge-current-max"}
String SA_INVERTER1_OPERATIONLOGIC "Operation logic [%s]" {channel="mqtt:topic:solar:SolarAssistant_Inverter_1:operation-logic"}
Group ElectricityInput_Inverter "Інвертер" <solar_energy> (ElectricityInput) ["Inverter"]
Switch ElectricityInput_InverterBypassMode "Режим байпасу інвертора [MAP(uk.map):%s]" (ElectricityInput_Inverter) ["Control"]
Group ElectricityInput_InverterBattery "Батарея" <battery> (ElectricityInput_Inverter) ["Battery"]
Number:Power InverterBattery_Power "Споживання від батареї [%.0f %unit%]" <measurement> (ElectricityInput_InverterBattery, gElectricityPower) ["Measurement", "Power"]
Number:Dimensionless InverterBattery_SOC "Стан батареї [%.0f %]" <measurement> (ElectricityInput_InverterBattery) ["Measurement", "Level"] {unit="%", widgetOrder="0"}
Number:Time InverterBattery_SocTime "Залишилося часу [%1$tH:%1$tM:%1$tS]" <measurement> (ElectricityInput_InverterBattery) ["Measurement", "Time"]
Number:ElectricCurrent InverterBattery_MaxChargeCurrent "Максимальний струм заряду батареї [%.1f %unit%]" <measurement> (ElectricityInput_InverterBattery) ["Control", "Currency"] {listWidget="oh-stepper-item"[ step=10, min=10, max=100]}
let proxy = require('openhab-proxy-pattern');
proxy.bind('InverterBattery_SOC', 'SA_TOTAL_BATTERYSOC').update(undefined, 15);
proxy.bind('InverterBattery_Power', 'SA_TOTAL_BATTERYPOWER').update(bw, 15);
proxy.bind('InverterBattery_MaxChargeCurrent', 'SA_INVERTER1_GRIDMAXCHARGE').update().forward();
proxy.bind('ElectricityInput_InverterBypassMode', 'SA_INVERTER1_OPERATIONLOGIC').update(function(value) {
return (value == 'ECO mode') ? 'ON' : 'OFF';
}).forward(function(value) {
return (value == 'ON') ? 'ECO mode' : 'Online mode';
});
Ну і фактично основна "бізнес-логіка" автоматизації дуже лаконічна
rules.JSRule({
name: 'Inverter charger mode',
description: "",
triggers: [
triggers.ItemStateChangeTrigger('InverterBattery_SOC')
],
execute: e => {
let now = time.ZonedDateTime.now();
const soc = parseInt(items.getItem('InverterBattery_SOC').state);
// мінімальна напруга по фазі С останні 5 хвилин
const vc = items.getItem('ElectricityInput_VC').history.minimumSince(now.minusMinutes(5));
if (vc) {
if (vc.numericState > 200) {
// є грід останні 5 хвилин і він живий (мінімальна напруга > 200В)
if (soc < 50) {
// 70А + байпас
items.getItem('ElectricityInput_InverterBypassMode').sendCommand('ON');
items.getItem('InverterBattery_MaxChargeCurrent').sendCommand('70 A');
return;
} else {
items.getItem('ElectricityInput_InverterBypassMode').sendCommand('OFF');
items.getItem('InverterBattery_MaxChargeCurrent').sendCommand('50 A');
return;
}
}
}
// дефолтний режим, в тч для заряду від генератора
items.getItem('ElectricityInput_InverterBypassMode').sendCommand('OFF');
items.getItem('InverterBattery_MaxChargeCurrent').sendCommand('30 A');
}
});
Як бонус, виявилося що дружина не дуже розуміє заряд батарейки в процентах (це скільки в часі?) і я для неї (ну і для себе) додатково зробив розрахунок часу до повного заряду та розряду АКБ. Тут трохи магії бо процент віддається в цілих і не можна просто проценти ділити на час бо буде постійно "пила" на графіку часу, фактично треба було зловити точки часу коли заряд (SOC) переходить в наступний процент:
proxy.bind('InverterBattery_SocTime', 'SA_TOTAL_BATTERYSOC').update(function(value) {
if (typeof value == 'string') {
if (value == '100') return undefined;
let now = time.ZonedDateTime.now();
let before = time.ZonedDateTime.now().minusMinutes(20);
let prev_soc = items.getItem("InverterBattery_SOC").history.previousState(true);
let soc = items.getItem("InverterBattery_SOC").history.previousState(false);
let before_soc = items.getItem("InverterBattery_SOC").history.historicState(before);
if (before_soc && prev_soc.timestamp.isAfter(before_soc.timestamp)) prev_soc = before_soc;
let soc_time = soc.timestamp;
let prev = prev_soc.timestamp;
if (soc.numericState == 100) return undefined;
var t = soc_time;
do {
t = t.minusSeconds(5);
if (items.getItem("InverterBattery_SOC").history.historicState(t).numericState == soc.numericState) {
soc_time = t;
} else {
break;
}
} while (t.isAfter(prev));
t = prev;
var prev_before = time.ZonedDateTime.now();
prev_before = prev_before.minusSeconds((time.ZonedDateTime.now().toEpochSecond() - prev.toEpochSecond()) * 20);
do {
t = t.minusSeconds(5);
if (items.getItem("InverterBattery_SOC").history.historicState(t).numericState == prev_soc.numericState) {
prev = t;
} else {
break;
}
} while (t.isAfter(prev_before));
if (prev_soc.numericState < soc.numericState) {
// charge
t = (soc_time.toEpochSecond() - prev.toEpochSecond()) * (100 - soc.numericState) / (soc.numericState - prev_soc.numericState);
return `${t} s`;
} else if (prev_soc.numericState > soc.numericState) {
// discharge
t = (soc_time.toEpochSecond() - prev.toEpochSecond()) * (soc.numericState - 10) / (prev_soc.numericState - soc.numericState);
return `${t} s`;
}
return undefined;
}
return undefined;
}, 10);