-
-
Save hfiennes/60f0f8ff81afc1297f1e99114ff62010 to your computer and use it in GitHub Desktop.
Hugo's PV logger
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Log output to pvoutput.org via HTTP POST | |
// | |
// see https://pvoutput.org/help.html#api-spec | |
// API key | |
const APIKEY = "<apikey here>"; | |
// System ID | |
const SID = "<system id here>"; | |
device.on("reading", function(v) { | |
server.log(http.jsonencode(v)); | |
post(v); | |
}); | |
function post(reading) { | |
local now = date(); | |
// Form readings | |
local body = ""; | |
body += format("d=%04d%02d%02d", now.year, now.month+1, now.day); | |
body += format("&t=%02d:%02d", now.hour, now.min); | |
body += format("&v1=%d", reading.energytoday*1000); | |
body += format("&v2=%d", reading.outputpower); | |
body += format("&v5=%d", reading.temperature); | |
body += format("&v6=%d", reading.voltage); | |
body += format("&c1=0"); | |
body += format("&n=0"); | |
server.log(body); | |
// Make POST request | |
local req = http.post("https://pvoutput.org/service/r2/addstatus.jsp", { "X-Pvoutput-Apikey": APIKEY, "X-Pvoutput-SystemId": SID }, body); | |
req.sendasync(function(res) { | |
server.log(res.statuscode); | |
server.log(res.body); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Code to talk over RS485 to a Samil Power 4400TL inverter and collect current state information | |
// (energy accumulated per day, current power, inverter temperature, voltage etc) | |
// | |
// 20181230 hugo@electricimp.com | |
#require "ConnectionManager.lib.nut:3.0.0" | |
cm <- ConnectionManager({ | |
"blinkupBehavior": CM_BLINK_ALWAYS, | |
"stayConnected": true | |
}); | |
class Application { | |
// UART in use | |
_uart = null; | |
// Receive state machine | |
_rxstate = 0; | |
_rxlen = 0; | |
_rxpacket = null; | |
// The inverter's address | |
_inverter_addr = 0; | |
// Callback for delivering stats | |
_stats_cb = null; | |
// Debug flag | |
_debug = false; | |
constructor(uart, txpin, stats_cb, debug) { | |
_uart = uart; | |
_debug = debug; | |
_stats_cb = stats_cb; | |
// Configure UART | |
_uart.configure(9600, 8, PARITY_NONE, 1, NO_CTSRTS, _rx_byte.bindenv(this)); | |
// Set up RS485 TX enable with 20us window | |
_uart.settxactive(txpin, 1, 20, 20); | |
} | |
// Read 16 bit big-endian value from current position | |
function _p2(pkt) { | |
return (pkt.readn('b')<<8) + pkt.readn('b'); | |
} | |
// Read 32 bit bit-endian value from current position | |
function _p4(pkt) { | |
return (pkt.readn('b')<<24) + (pkt.readn('b')<<16) + (pkt.readn('b')<<8) + pkt.readn('b'); | |
} | |
// Deal with a good packet | |
function _rx_packet(pkt) { | |
// Extract header fields | |
local src = (pkt[2]<<8) + pkt[3]; | |
local dst = (pkt[4]<<8) + pkt[5]; | |
local ctrl = pkt[6]; | |
local func = pkt[7]; | |
local len = pkt[8]; | |
// Log it if we're debugging | |
if (_debug) { | |
server.log(format("RX src=%04x dst=%04x ctrl=%02x func=%02x len=%02x", | |
src, dst, ctrl, func, len)); | |
if (len > 0) { | |
local s = ""; | |
for(local a=0; a<len; a++) { | |
local b = pkt[9+a]; | |
s+=format("%02x ", b); | |
} | |
server.log("RX "+s); | |
} | |
} | |
// Reply to inverter poll? | |
if (ctrl == 0x00 && func == 0x80) { | |
// Register the inverter serial number | |
// First, pull it out into a string (it's ASCII) | |
pkt.seek(9); | |
local s = ""; | |
for(local a=0; a<len; a++) s+=format("%c", pkt.readn('b')); | |
if (_debug) server.log("Registering "+s); | |
// Wait 200ms before registering as we get the strange | |
// packet set ~70ms after the serial number, then the | |
// inverter stays on bus for a while longer | |
// Register it | |
imp.wakeup(0.2, function() { | |
_tx_packet(0,0,0,0x01, s); | |
}.bindenv(this)); | |
return; | |
} | |
// Are we collecting the inverter address? | |
if (ctrl == 0x00 && func == 0x81) { | |
_inverter_addr = src; | |
if (_debug) server.log(format("Inverter online, address %02x", _inverter_addr)); | |
// Get stats and post; wait 200ms for device to get off bus | |
imp.wakeup(0.2, function() { | |
_tx_packet(0, _inverter_addr, 0x01, 0x02, null); | |
}.bindenv(this)); | |
return; | |
} | |
// Solar data response? | |
if (ctrl == 0x01 && func == 0x82) { | |
local reading = {}; | |
// Move pointer to start of packet's data | |
pkt.seek(9); | |
// Pull out the inverter's operational info | |
reading.temperature <- 0.1*_p2(pkt); | |
reading.voltage <- 0.1*_p2(pkt); | |
reading.current <- 0.1*_p2(pkt); | |
reading.poh <- 0.1*_p4(pkt); | |
reading.mode <- _p2(pkt); | |
reading.energytoday <- 0.01*_p2(pkt); | |
// Skip blank fields | |
_p2(pkt); _p2(pkt); _p2(pkt); _p2(pkt); _p2(pkt); | |
reading.gridcurrent <- 0.1*_p2(pkt); | |
reading.gridvoltage <- 0.1*_p2(pkt); | |
reading.gridfrequency <- 0.01*_p2(pkt); | |
reading.outputpower <- _p2(pkt); | |
reading.accumulatedenergy <- 0.1*_p4(pkt); | |
// Deliver to callback | |
_stats_cb(reading); | |
return; | |
} | |
} | |
function _rx_byte() { | |
local d = _uart.read(); | |
// If we have somewhere to store it, do... | |
if (_rxpacket) _rxpacket[_rxstate] = d; | |
switch(_rxstate) { | |
case 0: // Wait for 0x55 | |
if (d == 0x55) { | |
// Allocate a buffer for the packet | |
// ..then store the first byte | |
_rxpacket = blob(256); | |
_rxpacket[0] = 0x55; | |
_rxstate++; | |
} | |
break; | |
case 1: // Wait for 0xAA | |
// If it's 0x55 then could be the start of a new packet | |
// so we'd be waiting for 0xAA next | |
if (d == 0xaa) _rxstate++; | |
else if (d != 0x55) _rxstate = 0; | |
break; | |
case 2: // Source address H | |
case 3: // Source address L | |
case 4: // Dest address H | |
case 5: // Dest address L | |
case 6: // Control code | |
case 7: // Function code | |
_rxstate++; | |
break; | |
case 8: // Data length | |
_rxlen = d; | |
_rxstate ++; | |
break; | |
default: | |
// Have we got the whole packet? | |
// (9 byte header, data, 2 checksum bytes) | |
if (++_rxstate == (9 + _rxlen + 2)) { | |
// Extract received checksum, calculate our own | |
local ck_rx = (_rxpacket[_rxstate-2]<<8)+_rxpacket[_rxstate-1]; | |
local ck_calc = 0; | |
for(local a=0; a<(_rxstate-2); a++) ck_calc += _rxpacket[a]; | |
if (ck_rx == ck_calc) { | |
// Good packet: truncate and pass to handler | |
_rxpacket.resize(9 + _rxlen); | |
// Dump packet | |
if (_debug) { | |
local dump = ""; | |
foreach(b in _rxpacket) dump += format(" %02x", b); | |
server.log("RX"+dump); | |
} | |
// Pass to packet handler | |
_rx_packet(_rxpacket); | |
} else { | |
server.log(format("ERROR bad checksum: rx %04x calc %04x", ck_rx, ck_calc)); | |
} | |
// Reset state machine and free packet | |
_rxstate = _rxlen = 0; | |
_rxpacket = null; | |
} | |
break; | |
} | |
} | |
// Form a packet for sending, calculate checksum, send it | |
function _tx_packet(src, dest, ctrl, func, data) { | |
// Build header | |
local packet = "\x55\xaa"; | |
packet += format("%c%c", src>>8, src&0xff); | |
packet += format("%c%c", dest>>8, dest&0xff); | |
packet += format("%c", ctrl); | |
packet += format("%c", func); | |
if (data != null) { | |
// Length then data bytes | |
packet += format("%c", data.len()); | |
foreach(b in data) packet += format("%c", b); | |
} else { | |
// No data | |
packet += "\x00"; | |
} | |
// Calculate checksum & append | |
local chk = 0; | |
foreach(b in packet) chk += b; | |
packet += format("%c%c", chk>>8, chk&0xff); | |
// Dump packet | |
if (_debug) { | |
local dump = ""; | |
foreach(b in packet) dump += format(" %02x", b); | |
server.log("TX"+dump); | |
} | |
// Send packet | |
_uart.write(packet); | |
} | |
function poll_inverter() { | |
// Tell all devices to re-register | |
_tx_packet(0,0,0,0x04,null); | |
// After a couple of seconds, scan for unregistered inverters | |
imp.wakeup(2, function() { | |
// Send off-line query to find unregistered inverters | |
// The reply from this will kick off the next command | |
_tx_packet(0,0,0,0,null); | |
}.bindenv(this)); | |
} | |
} | |
// Inverter is on isolated 485 UART | |
if (imp.info().type == "impC001") { | |
rs485 <- hardware.uartYABCD; | |
rs485_tx <- hardware.pinYT; | |
hardware.pinYM.configure(DIGITAL_OUT, 1); | |
} else { | |
rs485 <- hardware.uart2; | |
rs485_tx <- hardware.pinL; | |
} | |
function take_readings() { | |
// Kick off reading sequence | |
app.poll_inverter(); | |
// Do this every 5 mins | |
imp.wakeup(5*60, take_readings); | |
} | |
function new_readings(r) { | |
// Send reading to agent | |
agent.send("reading", r); | |
} | |
app <- Application(rs485, rs485_tx, new_readings, false); | |
// Start reading stats every 5 mins | |
take_readings(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment