Skip to content

Instantly share code, notes, and snippets.

@hfiennes hfiennes/agent.nut Secret
Created Apr 9, 2019

Embed
What would you like to do?
Hugo's PV logger
// 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);
});
}
// 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
You can’t perform that action at this time.