Skip to content

Commit

Permalink
Update doc
Browse files Browse the repository at this point in the history
  • Loading branch information
mathieucarbou committed Jul 6, 2024
1 parent 6d9a245 commit bad1c3a
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 57 deletions.
Binary file modified docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/img/schemas/Solar_Router_Diverter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 34 additions & 11 deletions docs/blog/2024-07-01_shelly_solar_diverter.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ First the easy part: the temperature sensor and the Shelly Add-On, which has to
For example, the Shelly EM can be inside the main electric box, and the Shelly Dimmer + Add-On can be in the water tank electric panel, while the contactor and dimmer can be placed neat the water tank.
They communicate through the network.
- The dimmer will control the voltage regulator through the `COM` and `0-10V` ports
- The dimmer will also control the relay or contactor through the `A2` ports
- The wire from `Dimmer Output` to `Dimmer S1` is to set the switch mode to invert and make the dimmer detect when the contactor is OFF or ON and respectively disable or enable the dimming.
- The Shelly EM will control the relay or contactor through the `A2` ports.
- The B clamp around the wire going from the voltage regulator to the water tank is to measure the current going through the water tank resistance is optional and for information purposes only.
- The A clamp should be put around the main phase entering the house
- The relay / contactor is optional and is used to schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage.
Expand Down Expand Up @@ -122,14 +121,16 @@ Also, this central place allows to control the 1, 2 or more dimmers remotely.

- Make sure the switch (input) are enabled and inverted. S1 should replicate the inverse state of the relay / contactor, so that when you activate the contactor through the Dimmer output remotely, the dimmer will deactivate itself.
- Set static IP addresses
- Use the min/max settings to remap the 0-100% to match the voltage regulator.
I noticed that the LSA allows a wider range but the one from LCTC has to be re-mapped from 10-80%

### Shelly Pro EM 50 Setup

- Set static IP address
- Make sure to place the A clamp around the main phase entering the house in the right direction
- Add the `Shelly Solar Diverter` to the Shelly Pro EM
- Configure the settings in the `CONFIG` object
- Start teh script
- Start the script

## How to use

Expand All @@ -144,18 +145,34 @@ const CONFIG = {
// Grid Power Read Interval (s)
READ_INTERVAL_S: 1,
PID: {
// Reverse
REVERSE: false,
// Proportional Mode:
// - "error" (proportional on error),
// - "input" (proportional on measurement),
// - "both" (proportional on 50% error and 50% measurement)
P_MODE: "input",
// Derivative Mode:
// - "error" (derivative on error),
// - "input" (derivative on measurement)
D_MODE: "error",
// Integral Correction
// - "off" (integral sum not clamped),
// - "clamp" (integral sum not clamped to OUT_MIN and OUT_MAX),
// - "advanced" (advanced anti-windup algorithm)
IC_MODE: "advanced",
// Target Grid Power (W)
SET_POINT: 0,
// Number of Watts allowed to be above or below the set point (W)
SET_POINT_DELTA: 2,
SETPOINT: 0,
// PID Proportional Gain
KP: 0.8,
KP: 0.3,
// PID Integral Gain
KI: 0,
KI: 0.3,
// PID Derivative Gain
KD: 0.8,
// PID Output Minimum Clamp (W)
OUTPUT_MIN: 0,
KD: 0.1,
// Output Minimum (W)
OUT_MIN: -10000,
// Output Maximum (W)
OUT_MAX: 10000,
},
DIMMERS: {
"192.168.125.93": {
Expand Down Expand Up @@ -280,6 +297,12 @@ http://192.168.125.92/script/1/status
}
```
### PID Control and Tuning
The script uses a complex PID controller that can be tuned to really obtain a very good routing precision.
The algorithm used and default parameters are the same as in the YaSolr project.
You will find a lot of information in the [YaSolR manual](/manual#dashboard--pid-controller).
## Demo
Here is the view of the Shelly device websites while the grid power is changing.
Expand Down
160 changes: 115 additions & 45 deletions docs/downloads/solar_diverter_v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* ======================================
*/
const scriptName = "solar_diverter.js";
const scriptName = "solar_diverter";

// Config

Expand All @@ -19,21 +19,37 @@ const CONFIG = {
// Grid Power Read Interval (s)
READ_INTERVAL_S: 1,
PID: {
// Reverse
REVERSE: false,
// Proportional Mode:
// - "error" (proportional on error),
// - "input" (proportional on measurement),
// - "both" (proportional on 50% error and 50% measurement)
P_MODE: "input",
// Derivative Mode:
// - "error" (derivative on error),
// - "input" (derivative on measurement)
D_MODE: "error",
// Integral Correction
// - "off" (integral sum not clamped),
// - "clamp" (integral sum not clamped to OUT_MIN and OUT_MAX),
// - "advanced" (advanced anti-windup algorithm)
IC_MODE: "advanced",
// Target Grid Power (W)
SET_POINT: 0,
// Number of Watts allowed to be above or below the set point (W)
SET_POINT_DELTA: 0,
SETPOINT: 0,
// PID Proportional Gain
KP: 0.8,
KP: 0.3,
// PID Integral Gain
KI: 0.01,
KI: 0.3,
// PID Derivative Gain
KD: 0.8,
// PID Output Minimum Clamp (W)
ITERM_MIN: -10,
KD: 0.1,
// Output Minimum (W)
OUT_MIN: -10000,
// Output Maximum (W)
OUT_MAX: 10000,
},
DIMMERS: {
"192.168.125.93": {
"192.168.125.98": {
// Resistance (in Ohm) of the load connecter to the dimmer + voltage regulator
// 0 will disable the dimmer
RESISTANCE: 24,
Expand Down Expand Up @@ -63,27 +79,26 @@ let PID = {
iTerm: 0,
// Derivative Term
dTerm: 0,
}
// Sum
sum: 0,
};

// Divert Control

let DIVERT = {
lastTime: 0,
voltage: 0,
gridPower: CONFIG.PID.SET_POINT,
divertPower: CONFIG.PID.SET_POINT,
dimmers: {}
}
};

function validateConfig(cb) {
print(scriptName, ":", "Validating Config...")
print(scriptName, ":", "Validating Config...");

if (CONFIG.DIMMERS.length === 0) {
print(scriptName, ":", "ERR: No dimmer configured");
return;
}

for (const ip in CONFIG.DIMMERS) {
for (let ip in CONFIG.DIMMERS) {
if (CONFIG.DIMMERS[ip].RESISTANCE < 0) {
print(scriptName, ":", "ERR: Dimmer resistance should be greater than 0");
return;
Expand All @@ -97,39 +112,98 @@ function validateConfig(cb) {
print(scriptName, ":", "Dimmer", ip, "is enabled");
DIVERT.dimmers[ip] = {
divertPower: 0
}
};
}

cb();
}

function constrain(value, min, max) { return Math.min(Math.max(value, min), max); }

// - https://github.com/Dlloydev/QuickPID
// - https://github.com/br3ttb/Arduino-PID-Library
function calculatePID(input) {
PID.input = input;
const error = CONFIG.PID.SET_POINT - PID.input;
if (Math.abs(error) <= CONFIG.PID.SET_POINT_DELTA) {
PID.pTerm = 0;
PID.iTerm = 0;
PID.dTerm = 0;
PID.output = 0;
} else {
PID.pTerm = error * CONFIG.PID.KP;
PID.iTerm = Math.max(PID.iTerm + error * CONFIG.PID.KI, CONFIG.PID.ITERM_MIN);
PID.dTerm = (error - PID.error) * CONFIG.PID.KD;
const dInput = CONFIG.PID.REVERSE ? PID.input - input : input - PID.input;
const error = CONFIG.PID.REVERSE ? input - CONFIG.PID.SETPOINT : CONFIG.PID.SETPOINT - input;
const dError = error - PID.error;

if (CONFIG.DEBUG > 1) {
print(scriptName, ":", "Input:", input, "W, Error:", error, "W, dError:", dError, "W");
}

let peTerm = CONFIG.PID.KP * error;
let pmTerm = CONFIG.PID.KP * dInput;
switch (CONFIG.PID.P_MODE) {
case "error":
pmTerm = 0;
break;
case "input":
peTerm = 0;
break;
case "both":
peTerm *= 0.5;
pmTerm *= 0.5;
break;
default:
return PID.output;
}

// pTerm
PID.pTerm = peTerm - pmTerm;

// iTerm
PID.iTerm = CONFIG.PID.KI * error;

if (CONFIG.DEBUG > 1) {
print(scriptName, ":", "pTerm:", PID.pTerm, "W, iTerm:", PID.iTerm, "W");
}
PID.output = PID.pTerm + PID.iTerm + PID.dTerm;

// anti-windup
if (CONFIG.PID.IC_MODE == "advanced" && CONFIG.PID.KI) {
const iTermOut = PID.pTerm + CONFIG.PID.KI * (PID.iTerm + error);
if ((iTermOut > CONFIG.PID.OUT_MAX && dError > 0) || (iTermOut < CONFIG.PID.OUT_MIN && dError < 0)) {
_iTerm = constrain(iTermOut, -CONFIG.PID.OUT_MAX, CONFIG.PID.OUT_MAX);
}
}

// integral sum
PID.sum = CONFIG.PID.IC_MODE == "off" ? (PID.sum + PID.iTerm - pmTerm) : constrain(PID.sum + PID.iTerm - pmTerm, CONFIG.PID.OUT_MIN, CONFIG.PID.OUT_MAX);

// dTerm
switch (CONFIG.PID.D_MODE) {
case "error":
PID.dTerm = CONFIG.PID.KD * dError;
break;
case "input":
PID.dTerm = -CONFIG.PID.KD * dInput;
break;
default:
return PID.output;
}

PID.output = constrain(PID.sum + peTerm + PID.dTerm, CONFIG.PID.OUT_MIN, CONFIG.PID.OUT_MAX);

PID.input = input;
PID.error = error;

return PID.output;
}

function callDimmers(cb) {
for (const ip in DIVERT.dimmers) {
for (let ip in DIVERT.dimmers) {
const dimmer = DIVERT.dimmers[ip];

// ignore contacted dimmers
if (dimmer.rpc !== "pending") {
continue;
}

if (isNaN(dimmer.dutyCycle)) {
dimmer.rpc = "failed";
print(scriptName, ":", "ERR: Invalid duty cycle for dimmer", ip);
continue;
}

// build url
const url = "http://" + ip + "/rpc/Light.Set?id=0&on=" + (dimmer.dutyCycle > 0 ? "true" : "false") + "&brightness=" + (dimmer.dutyCycle * 100) + "&transition_duration=0.5";

Expand Down Expand Up @@ -162,29 +236,24 @@ function callDimmers(cb) {
}

function divert(voltage, gridPower) {
DIVERT.voltage = voltage;
DIVERT.gridPower = gridPower;
const correction = calculatePID(DIVERT.gridPower);
DIVERT.divertPower = Math.max(0, DIVERT.divertPower + correction);
let newRoutingPower = calculatePID(gridPower);

if (CONFIG.DEBUG > 0)
print(scriptName, ":", "Grid:", voltage, "V,", gridPower, "W. Correction:", correction, "W. Total Divert Power:", DIVERT.divertPower, "W");

let remaining = DIVERT.divertPower;
print(scriptName, ":", "Grid:", voltage, "V,", gridPower, "W. Power to divert:", newRoutingPower, "W");

for (const ip in DIVERT.dimmers) {
for (let ip in DIVERT.dimmers) {
const dimmer = DIVERT.dimmers[ip];
dimmer.nominalPower = voltage * voltage / CONFIG.DIMMERS[ip].RESISTANCE;
dimmer.divertPower = Math.min(remaining * CONFIG.DIMMERS[ip].RESERVED_EXCESS_PERCENT / 100, dimmer.nominalPower);
dimmer.dutyCycle = dimmer.divertPower / dimmer.nominalPower;
dimmer.maximumPower = voltage * voltage / CONFIG.DIMMERS[ip].RESISTANCE;
dimmer.divertPower = Math.min(newRoutingPower * CONFIG.DIMMERS[ip].RESERVED_EXCESS_PERCENT / 100, dimmer.maximumPower);
dimmer.dutyCycle = dimmer.divertPower / dimmer.maximumPower;
dimmer.powerFactor = Math.sqrt(dimmer.dutyCycle);
dimmer.dimmedVoltage = dimmer.powerFactor * voltage;
dimmer.current = dimmer.dimmedVoltage / CONFIG.DIMMERS[ip].RESISTANCE;
dimmer.apparentPower = dimmer.current * voltage;
dimmer.thdi = dimmer.dutyCycle === 0 ? 0 : Math.sqrt(1 / dimmer.dutyCycle - 1);
dimmer.rpc = "pending";

remaining -= dimmer.divertPower;
newRoutingPower -= dimmer.divertPower;

if (CONFIG.DEBUG > 0)
print(scriptName, ":", "Dimmer", ip, "=>", dimmer.divertPower, "W");
Expand Down Expand Up @@ -222,8 +291,9 @@ function onGetStatus(request, response) {
response.code = 200;
response.headers = {
"Content-Type": "application/json"
}
};
response.body = JSON.stringify({
config: CONFIG,
pid: PID,
divert: DIVERT
});
Expand All @@ -233,7 +303,7 @@ function onGetStatus(request, response) {
// Main

validateConfig(function () {
print(scriptName, ":", "Starting Shelly Solar Diverter...")
print(scriptName, ":", "Starting Shelly Solar Diverter...");
HTTPServer.registerEndpoint("status", onGetStatus);
readPower();
});
6 changes: 5 additions & 1 deletion docs/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ The overview section shows some global information about the router.

The temperature is coming from the sensor installed in the router box.

**A JSY or PZEM is required to see the measurements.**

## Dashboard / Output 1 & 2

The output sections show the state of the outputs and the possibility to control them.
Expand All @@ -184,6 +186,8 @@ The output sections show the state of the outputs and the possibility to control

**Energy:**

**A JSY or PZEM is required to see the measurements.**

- `Power`: Routed power.
- `Apparent Power`: Apparent power in VA circulating on the wires.
- `Power Factor`: Power factor (if lower than 1, mainly composed of harmonic component). Ideal is close to 1.
Expand Down Expand Up @@ -462,7 +466,7 @@ Here are some basic links to start with, which talks about the code used under t

- Proportional Mode: `On Input`
- Derivative Mode: `On Error`
- Integral Correction: `Anti-windup`
- Integral Correction: `Advanced`
- Setpoint: `0`
- Kp: `0.3`
- Ki: `0.3`
Expand Down

0 comments on commit bad1c3a

Please sign in to comment.