Introduction
Welcome to the EKM METERING API!
We at EKM see easy access to your data, and the scalable systems behind the EKM Push, as crucial to moving our products into the future. To that end, we do what is unheard of in our industry, we give you your data for FREE.
The EKM API is organized around Representational State Transfer, or REST. You can use our Application Programming Interface, or API, to access EKM API endpoints, which can get you information on various EKM Push meter/ioStack data and utilize it in your own application, database, billing system, or building automation system.
We have language bindings in Shell (cURL), Ruby, Python, PHP, Perl, Java, Javascript and Nodejs! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.
Our API is designed to have predictable, resource-oriented URLs and to use HTTP response codes to indicate API errors. We use built-in HTTP features, like HTTP authentication and HTTP verbs, which can be understood by off-the-shelf HTTP clients, and we support cross-origin resource sharing to allow you to interact securely with our API from a client-side web application (though you should remember that you should never expose your secret EKM Push API key in any public website’s client-side code). JSON will be returned in all responses from the API, including errors (though if you’re using API bindings, we will convert the response to the appropriate language-specific object).
Authentication
To authorize, make sure to use your personal EKM Push account key.
The examples in this API documentation use the demo key of MTAxMDoyMDIw. Please make sure you remove this key and place your personal key in the https address if you are trying to access the meters in your account.
With shell, you can just pass the correct address with each request
curl -s "URL Here"
Authorization: "EKM Push Key"
# Ruby Version: 3.1.2
# Load required modules
require 'net/http'
require 'json'
require 'uri'
# Call the call_api method to create a usable
# object named api_object from the API request URI
# Put the API request URI in the call
def call_api(api_path)
uri = URI.parse("URL Here#{api_path}")
response = Net::HTTP.get_response(uri)
puts response.uri
JSON.parse(response.body)
end
# Call the call_api method to create a usable
# object named api_object from the API request URI
# Put the API request URI in the call
# URI only NOT URL - Do not include for example https://summary.ekmpush.com
api_object = call_api('URI Here')
# This just displays the object but you can use what ever
# code you would like to work with the object here
require 'pp'
pp api_object
'''
Python version: 3.9.12
'''
# Required Python Modules
import urllib.request
import urllib.error
import urllib.parse
import json
import pprint
# This function accesses the api_request URL and converts
# the contents to a usable Python object and returns it
def call_api ( api_request ):
'''
Make http request
'''
response = urllib.request.urlopen(api_request)
response = response.read()
json_object = json.loads(response.decode())
return json_object
# Call the call_api function to create a usable
# object named api_object from the API request URL.
# Put the API request URL in the call
api_object = call_api("URL Here")
# This just displays the object but you can use what ever
# code you would like to work with the object here
pprint.pprint(api_object)
<?php
// PHP 8.0.17 (cli)
// Call the callApi function to create a usable
// object named $apiObject from the API request URL.
// Put the API request URL in the call
$apiObject=callApi('URL Here');
// This just displays the object but you can use what ever
// code you would like to work with the object here
var_dump($apiObject);
// This function accesses the apiRequest URL and converts
// the contents to a usable PHP object and returns it
function callApi ($apiRequest='') {
$json=@file_get_contents($apiRequest);
$jsonObject=json_decode($json);
return ($jsonObject);
}
?>
#!/usr/bin/perl
# Perl version: v5.34
# CPAN.pm version 2.2
# Perl Modules Version:
# JSON: 4.05
# LWP::Protocol::https: 6.10
# LWP::Simple: 6.62
#
# OS Prerequisites
# UBUNTU
# apt install cpanminus
#
# Install Perl Modules
# cpan LWP::Simple
# cpan LWP::Protocol::https
# cpan JSON
# Required Perl Modules
use strict;
use warnings;
use LWP::Simple;
use JSON;
use Data::Dumper;
# This function accesses the api_request URL and converts
# the contents to a usable Perl object and returns it
sub call_api {
my $api_request = shift;
my $json_text = get($api_request);
my $json_object = JSON->new->utf8->decode($json_text);
return $json_object;
}
# Call the call_api function to create a usable
# object named api_object from the API request URL.
# Put the API request URL in the call
my $api_object = call_api('URL Here');
# This just displays the object but you can use what ever
# code you would like to work with the object here
print Dumper($api_object); ## no critic (InputOutput::RequireCheckedSyscalls)
/*
openjdk version "17.0.2" 2022-01-18
Download the correct org.json jar version for your
needs from: https://mvnrepository.com/artifact/org.json/json
This example uses version 20220320 accessible here:
https://repo1.maven.org/maven2/org/json/json/20220320/json-20220320.jar
Instructions to run this program
1. Put this code in a file named EKM.java
2. Copy the downloaded org.json jar and EKM.java to the same directory
3. Compile
javac -cp .:./json-20220320.jar ./EKM.java
4. Run
java -cp .:./json-20220320.jar EKM
*/
//Import required classes
import java.net.*;
import java.io.*;
import org.json.*;
@java.lang.SuppressWarnings({"java:S106", "java:S112", "java:S125"})
public class EKM {
public static JSONObject callApi(String apiRequest) throws Exception {
// This code accesses the apiRequest URL and converts
// the contents to a usable JSON object
URL url = new URL(apiRequest);
URLConnection connection = url.openConnection();
BufferedReader in = new BufferedReader(
new InputStreamReader(
connection.getInputStream()));
StringBuilder response = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null)
response.append(inputLine);
in.close();
return new JSONObject(response.toString());
}
public static void main(String[] args) throws Exception {
/*
Call callApi to create a usable
object named apiObject from the API request URL.
Put the API request URL in the call
*/
JSONObject apiObject = EKM.callApi("URL Here");
/*
You can access any part of the apiObject using code like this:
JSONArray readData = apiObject.getJSONObject("readmeter").getJSONArray("ReadSet").
getJSONObject(0).getJSONArray("ReadData");
*/
// This just outputs the whole apiObject
System.out.println(apiObject.toString(4));
}
}
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
// The example function is called from the
// body tag when the page loads
function example(){
// Call the callApi function to create a usable
// object named apiObject from the API request URL.
// Put the API request URL in the call
callApi('URL Here',function(apiObject){
// This just displays the object in the result div
// you can use what ever code you would like to work
// with the object here
document.getElementById("result").innerHTML = "<pre>"+JSON.stringify(apiObject, null, 4)+"</pre>";
});
};
// This code accesses the apiRequest URL and converts
// the contents to a usable JSON object named apiObject
function callApi(apiRequest,callback) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (xhttp.readyState == 4 && xhttp.status == 200) {
var jsonObject = JSON.parse(xhttp.responseText);
callback(jsonObject);
}
};
xhttp.open("GET", apiRequest, true);
xhttp.send();
}
</script>
</head>
<body onload="example()">
<div id="result"/>
</body>
</html>
/*
* Requirements
* NodeJS: v16.14.2
* Axios: 0.27.2
*/
// Load modules
const axios = require('axios');
const apiClient = axios.create({
baseURL: 'URL Here',
timeout: 1000 * 15,
})
// This code accesses the apiRequest query and
// logs the response data or the error
;(async () => {
try {
const res = await apiClient.get('URI Here');
const apiResData = res.data;
console.log(JSON.stringify(apiResData, null, 2));
} catch (error) {
console.error(error.message);
}
})();
Make sure to replace the sample key: MTAxMDoyMDIw, with your API key in the https address.
EKM uses API keys to allow access to the API. You authenticate to the EKM API by providing one of your unique API keys in each request. Each Push account holder is provided with an EKM Push User Key, which provides access to all meters in their account. This key carries lots of privileges so we encourage you to keep it secret. In addition to this master key, additional keys are also provided to give access to each meter/ioStack individually, and keys can be created to provide access to sub groups of meters/ioStacks upon request. These secondary keys can be used to share single meters/ioStacks, or a subset of meters/ioStacks, without sharing access to all meters/ioStacks in an account. For example, if you are a landlord with multiple rentals and meters/ioStacks, you could share specific meter/ioStack keys with each of your tenants, so that they could have access to only the data that pertains to their usage.
Authentication to the API occurs via HTTP Basic Auth. Provide your API key as the basic authorized username. You do not need to provide a password. You must authenticate for all requests.
The EKM Push API expects the API key to be included in all requests to the server. The key is included in the URL in the following way:
Authorization: key=MTAxMDoyMDIw
Realtime API
Click here to go to Realtime Documentation
If you are developing your own app, cloud-to-cloud solution, billing system, or other SAS solution, our Real-Time API allows you to easily access your EKM Push data in any format that you need. Below you will find descriptions regarding how to access the data, and about the filters you can apply so the data comes to you in a format that is easily digested and inserted into your software solution.
The real-time API provides the 1,000 latest meter readings for each of your meters/IOStack devices. If your device is being read once per minute, the data will be made available once per minute, per device. Whether you have one device or 10,000 devices, this is the easiest and most scalable way to access your data.
The EKM Dash, EKM Widget, encompass.io, wattvision.com, pvoutput.org, the other solutions in our Push App Store, as well as other customers that have their own custom solutions, all use this API to access their data. We use the same API as you and do not give ourselves any special permissions, we see what you see, which forces us to make the API as great as possible for everyone. We have even given you code examples that can be copy and pasted into your own software language to make the data access that much easier.
Use the API definition, metered values definition, code snippet suggestion, and guide to get you on your way to developing your next killer app. If you create something great, let us know; we’re open to adding all useful apps into the Push App Store.
We also have a Realtime API Request Builder Tool found here:
Account API
Click here to go to Account API Documentation
This API provides information for accounts and devices owned by the account.
Get information for the specified target, which can be account, gateway, meter or ioStack.
You could make API request with the EKM Push Key that is your own Authorization Key that you received for your Push Account, or instead the EKM Push Key you could use Gateway Key for more restricted access. Be aware that if you use the Gateway Key, you will only receive information regards to that gateway.
Gateway Settings API
Click here to go to Gateway Settings API Documentation
This API is used to send commands and settings to devices connected to a Push3 gateway as well as the gateway device itself.
Trigger API
Click here to go to Trigger API Documentation
A REST API to lookup/create/update/delete triggers.
EKM Push3 gateway triggers are among the most powerful tools that EKM offers. You can automate how and when the EKM Push system reacts to metered values. For example, you can have the Push gateway send you an email, send a webhook to your server, or control a relay to turn on/off a switch or close a valve based on the metered data. Push3 gateways can trigger specific actions based on conditions you set up. These triggers reside on the Push3 gateways, allowing them to function even without an internet connection. Triggers can control the relays on v.4 Omnimeters to turn devices on or off, send webhooks to alert your software system, or email you notifications. You can set up new triggers in the Account Portal or via web APIs.
Please note: v.3 Omnimeters do not have controllable relays, so relay triggers will not work, but email triggers will.
Summary API [LEGACY]
Click here to go to Summary Documentation
Our Summary API takes every Real-Time read, over 15 minute time periods, and summarizes them into single 15 minute summaries. We store this data forever to provide a long term historical dataset for each meter. Our system can then combine these summaries together to summarize hours, days, weeks, and months. This dataset is often the best way to get historical values like kWh, pulse counts, etc. It also provides averages, min. and max. values, difference, and more. We make this data available to you via our Summary API in a very similar way to our Real-Time API.
You can use the Summary API definition to access the data you need, from 15 minutes to years of data. We have gone to great lengths to provide this data for free in order to add value to our metering systems. The Summary API, the Real-Time API, great affordable hardware, and scalable access to your data are all components of the most powerful, and highest value metering system available in the world.
We also have a Summary API Request Builder Tool found here: Summary API Builder
Summary V2 API
Click here to go to Summary V2 Documentation
Summary V2 API is a new set of summary features, such as First/Last to get the first and/or last reported date for the given meter(s)/ioStack(s), GStat for getting the status of one or more gateways, IOStack for getting information from new sensors, and more.
The API V2 summary and documentation are currently in beta, signifying that they are subject to changes and updates. It’s important to note that the API V2 calls and specifications provided in this documentation may undergo modifications as the beta phase progresses. Users should stay informed and regularly check for updates to ensure compatibility and adherence to the latest specifications
MQTT Messaging
Click here to go to MQTT Documentation
MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol designed for low-bandwidth, high-latency, or unreliable networks, often used in Internet of Things (IoT) applications. It follows a publish-subscribe model, where devices (clients) can publish messages to topics or subscribe to topics to receive messages.
MQTT will only work with Push3 Gateways. It is more efficient than making hundreds of API calls because it does not impose rate limits (you can stream 24/7), allowing continuous realtime or summary data streaming. However, it lacks message history, meaning that if you need past data, you will have to make an API request to retrieve realtime or summary information as far back as necessary, and then resume streaming via MQTT. If the connection is lost, you will need to request realtime or summary data again to fill in any gaps before continuing with the MQTT stream. This combination of API and MQTT helps ensure a smooth data flow while compensating for potential disruptions.
RS-485 Communications
This section is for developers, or individuals, that want to communicate with their EKM Meters directly using their own software or embedded solution.
The code examples found in this section are in the simplest possible method to get you started. You are welcome to make them more robust.
First we start you out just connecting to the meter. You will find there is a very simple command line process to help you get started.
After that we cover the CRC method required for most communication to the meter.
Then we put it all together, in a simple test script that will show reads, and also open and close the relays.
Last we cover how to convert the 255 character strings that the meter responds with to a usable array containing field names and their values. It is our hope that after you go through these steps you will have all the information you need, to build whatever tools you like to access the meter.
Our meters use an IEC 62056-21 communication standard that has been optimized for our own needs. We are more than happy to share this with you. With this you can write your own software or embedded solution to access your meter data.
IEC 62056 is a set of standards for Electricity metering data exchange by International Electrotechnical Commission.
To learn more about the IEC 62056 standard click the button below to visit the WikiPedia website.
Additional PDF docs are available:
v.4/v.5 Meter Settings Protocol
v.4/v.5 Meter Alternate Modbus Protocol
If you are coding in Python you can also make use of our ekmmeters.py API.
Serial Settings
Send any of your meters on your RS-485 network this simple request (including the 12 digit meter number) and the targeted meter will respond with most of its data. If you use our EKM Dash software and utilize the “Hex Inspector” functionality under the “Help” menu item, it will simplify your job to be able to “see” the request and response (this is helpful for both the RS-485 parsing as well as parsing the EKM Push .xml data).
Serial Settings for v.3 and v.4 meters: 7 data bits, Even Parity, 1 Stop Bit, No Flow Control, 9600 baud rate.
Always send the “Close String” at the end of the transaction. This tells the meter that the communication session is over.
Close String Parameter
Parameter | Description |
---|---|
01 42 30 03 75 | Close String to end session |
The last 2 bytes of the RS-485 return string are a CRC-16 checksum.
All values are Hexadecimal.
Example: 30 represents a 0, 31 represents a 1, 32 represents a 2, etc.
Hex to ASCII examples
Parameter | Description |
---|---|
30 | represents a 0 |
31 | represents a 1 |
32 | represents a 2 |
33 | represents a 3 |
34 | represents a 4 |
35 | represents a 5 |
etc. | This sequence continues |
For a complete list of ASCII code and the Hex equivalent please click the button below.
Connecting to Meter
Connecting to Meter
# The serial device in this example is /dev/ttyUSB0
# First set up your serial port
stty -F /dev/ttyUSB0 cs7 -parodd -cstopb -ixon 9600
# Now we start hexdump to monitor the serial port
hexdump -v -e '1/1 "%02x "' /dev/ttyUSB0 &
# In this example we are just going to set a Request A call using echo
# The meter number is 000300001184 (in hex 30 30 30 33 30 30 30 30 31 31 38 34)
echo -en "\x2F\x3F\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x30\x30\x21\x0D\x0A" > /dev/ttyUSB0
# Now we can kill hexdump to stop monitoring the serial port
killall -9 hexdump
# Ruby Version: 2.7.0
# Serialport version: 1.3.2
# Requires the SerialPort gem
# (http://rubygems.org/gems/serialport)
# gem install serialport
require 'serialport'
# Params for serial port
# Change /dev/ttyUSB0 to match your syste
port_str = '/dev/ttyUSB0'
baud_rate = 9600
data_bits = 7
stop_bits = 1
parity = SerialPort::EVEN
flow = SerialPort::NONE
# Meter Number
meter = '000300001184'
# Open connection to serial port
sp = SerialPort.new(port_str, baud_rate, data_bits, stop_bits, parity)
sp.flow_control = flow
# Send Request A to v4 Meter
sp.write("\x2F\x3F#{meter}\x30\x30\x21\x0D\x0A")
# Set connection timeout
timeout = 5
# Get meter response
now = Time.now
read = ''
response = ''
count = 0
while count < 255
read = sp.getc
# Checks for correct start of message from meter before storing reads
start = 1 if read == "\x02"
if read && start
now = Time.now
count += 1
response.concat(read)
end
if Time.now > now + timeout
print('Connection timed out!')
break
end
end
# Show response as HEX
print(response.unpack('C*').map { |e| e.to_s 16 }.join(' '))
# Send close to meter
sp.write("\x01\x42\x30\x03\x75")
# Close connection to serial port
sp.close
'''
Python version: 3.8.10
Modules:
Pyserial version: 3.4
Requires pyserial module:
pip3 install pyserial
'''
import serial
# Open connection to serial port
# Change /dev/ttyUSB0 to match your system
sp = serial.Serial(
port='/dev/ttyUSB0',
baudrate=9600,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.SEVENBITS,
xonxoff=0,
timeout=5
)
# Meter Number
METER=b"000300001184"
# Send Request A to v4 Meter
sp.write(b"\x2F\x3F"+METER+b"\x30\x30\x21\x0D\x0A")
# Get meter response
response = sp.read(255)
# Show response
print(" ".join("{0:02x}".format( c ) for c in response))
# Send close to meter
sp.write(b"\x01\x42\x30\x03\x75")
# Close connection to serial port
sp.close()
<?php
/*
* Requirements:
* PHP: 8.1.5
* DIO: 0.2.1
*
* Install php8.1-dev on ubuntu to compile DIO module.
* apt install php8.1-dev
* Compile DIO module from source:
* Download the latest tarball from: https://pecl.php.net/package/dio
* Uncompress: tar xfz dio-0.2.1.tgz
* cd dio-0.2.1/
* phpize
* ./configure
* make
* sudo make install
* You will see a path like: /usr/lib/php/20210902/
* Edit your php.ini
* vi /etc/php/8.1/cli/php.ini
* In the extension section add:
* extension=/usr/lib/php/20210902/dio.so
* Now you can run your php script.
* Ex: php connect.php
*/
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
$sp = dio_open( '/dev/ttyUSB0', O_RDWR | O_NONBLOCK );
// Configure the port
dio_tcsetattr( $sp, array(
'baud' => 9600,
'bits' => 7,
'stop' => 1,
'parity' => 2,
'flow_control' => 0,
'is_canonical' => 0
) );
// Meter Number
$meter="000300001184";
// Send Request A to v4 Meter
dio_write( $sp, "\x2F\x3F".$meter."\x30\x30\x21\x0D\x0A" );
# Set connection timeout
$timeout=10;
// Get meter response
$read = '';
$response = '';
$start=0;
$now=time();
while( strlen($response)<255 && time()<$now+$timeout) {
$read = dio_read($sp,1);
# Checks for correct start of message from meter before storing reads
if ($read=="\x02")
$start=1;
if ($start){
$response .= $read;
}
}
// Show response as HEX
echo strToHex($response);
// Send close to meter
dio_write($sp, "\x01\x42\x30\x03\x75" );
// Close connection to serial port
dio_close( $sp );
function strToHex($string)
{
$hex='';
for ($i=0; $i < strlen($string); $i++)
{
$hex .= sprintf("%02X",ord($string[$i])).' ';
}
return $hex;
}
?>
#!/usr/bin/perl
# Perl version: v5.30.0
# CPAN.pm version v2.34
#
# Requires the Device::SerialPort module
# Device::SerialPort: 1.04
# Readonly: 2.05
#
# Install Perl Modules
# cpan Device::SerialPort
# cpan Readonly
use strict;
use warnings;
use Carp;
use Readonly;
use Device::SerialPort;
use version; our $VERSION = qv(1.0);
# Open connection to serial port
# Change /dev/ttyUSB0 to match your system
my $sp = new Device::SerialPort('/dev/ttyUSB0'); ## no critic (Objects::ProhibitIndirectSyntax)
# Configure the port
Readonly my $BAUDRATE => 9600;
Readonly my $DATABITS => 7;
Readonly my $MSG_ON => 'ON';
$sp->user_msg($MSG_ON);
$sp->baudrate($BAUDRATE);
$sp->parity('even');
$sp->databits($DATABITS);
$sp->stopbits(1);
$sp->handshake('xoff');
$sp->write_settings;
# Meter Number
my $meter = '000300001184';
# Send Request A to v4 Meter
$sp->write("\x2F\x3F$meter\x30\x30\x21\x0D\x0A"); ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Set connection timeout
Readonly my $TIMEOUT => 10;
my $timeout = $TIMEOUT;
# don't wait for each character
$sp->read_char_time(0);
# wait for 1 second per unfulfilled read
Readonly my $CONST_TIME => 1000;
$sp->read_const_time($CONST_TIME);
# Get meter response
Readonly my $MY_BYTE => 255;
my $chars = 0;
my $response = q{};
while ( $timeout > 0 && $chars < $MY_BYTE ) {
my ( $count, $read ) = $sp->read($MY_BYTE);
if ( $count > 0 ) {
$chars += $count;
$response .= $read;
}
else {
$timeout--;
}
}
if ( $timeout == 0 ) {
die "Connection timed out!\n";
}
# Show response as HEX
$response =~ s/(.)/sprintf("%02X",ord($1))." "/seg; ## no critic (RegularExpressions::RequireLineBoundaryMatching RegularExpressions::RequireExtendedFormatting)
print $response; ## no critic (InputOutput::RequireCheckedSyscalls)
# Send close to meter
$sp->write("\x01\x42\x30\x03\x75"); ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Close connection to serial port
$sp->close || croak 'failed to close';
undef $sp;
/*
openjdk version "17.0.2" 2022-01-18
Download the jssc.jar (java-simple-serial-connector)
from: https://code.google.com/archive/p/java-simple-serial-connector/
Version: jSSC-2.8.0
Instructions to run this program
1. Put this code in a file named Connect.java
2. Copy the downloaded jssc.jar and Connect.java to the same directory
3. Compile
javac -cp .:./jssc.jar ./Connect.java
4. Run
java -cp .:./jssc.jar Connect
*/
// Import required classes
import jssc.*;
@java.lang.SuppressWarnings({"java:S106", "java:S112", "java:S125"})
public class Connect {
static byte[] serialReadBytes(SerialPort serialPort) throws Exception {
// Set connection timeout;
int timeout=5;
byte[] buffer = "\r\n".getBytes();
try {
buffer = serialPort.readBytes(255,timeout*1000);
} catch (SerialPortTimeoutException e1)
{
System.out.println("Connection Timed out");
}
return buffer;
}
public static void main(String[] args) throws Exception {
String response = "";
byte[] buffer = "\r\n".getBytes();
SerialPort serialPort = new SerialPort("/dev/ttyUSB0");
try {
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
serialPort.openPort();
// Configure the port
serialPort.setParams(SerialPort.BAUDRATE_9600,
SerialPort.DATABITS_7,
SerialPort.STOPBITS_1,
SerialPort.PARITY_EVEN);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
// Meter Number
String meter="000300001184";
// Send Request A to v4 Meter
String messageString = "\u002F\u003F"+meter+"\u0030\u0030\u0021\r\n";
serialPort.writeBytes(messageString.getBytes());
// Get meter response
buffer = serialReadBytes(serialPort);
response = new String(buffer);
// Show response as HEX
System.out.println(byteArrayToHex(response));
// Send close to meter
serialPort.writeBytes("\u0001\u0042\u0030\u0003\u0075".getBytes());
// Close connection to serial port
serialPort.closePort();
}
catch (SerialPortException ex) {
System.out.println(ex);
}
}
public static String byteArrayToHex(String orgStr) {
byte[] a = orgStr.getBytes();
StringBuilder sb = new StringBuilder(a.length * 2);
for(byte b: a){
sb.append(String.format("%02x", b & 0xff));
sb.append(" ");
}
return sb.toString();
}
}
/*
* Requirements
* NodeJS: v16.15.0
* Serialport: 10.4.0
*/
// Load modules
const {SerialPort} = require('serialport');
// Params for Serial Port
// Change path to match your system
const sp = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 9600,
parity: 'even',
dataBits: 7,
stopBits: 1,
flowControl: 0,
bufferSize: 1,
}, false);
// This should be the meter number you are trying to connect to
const meter = '000300001184';
const timeout = 5;
let response = '';
const strToHex = (str) => {
let hex = '';
for (let i = 0; i < str.length; i++) {
const pad = `${str.charCodeAt(i).toString(16).toUpperCase()}`;
hex += ` ${(`00${pad}`).slice(-2)}`;
}
return hex;
};
const getResponse = (cmd) => new Promise((resolve, reject) => {
sp.write(cmd, (err) => {
// Clear response data
response = '';
if (err) {
console.log(err);
reject(err);
}
setTimeout(() => {
resolve(response);
}, timeout * 1000);
});
});
const sendReq = async (cmd) => {
const resRec = await getResponse(cmd);
if (resRec === '') {
console.log('Did not trigger a response\n');
} else if (resRec.length === 255) {
console.log(`${strToHex(resRec)}\n`);
} else {
console.log('Something failed or connection timed out\n');
}
return resRec;
}
// Start meter communication
;(async () => {
sp.open(() => {
console.log('Connection Opened');
// Get meter response
sp.on('data', (read) => {
if (response.length < 255) {
response += read;
}
});
});
console.log('Sending Connection Request To Meter');
await sendReq(`\x2F\x3F${meter}\x30\x30\x21\x0D\x0A`);
console.log('When you\'re finished you will close the connection');
await sendReq('\x01\x42\x30\x03\x75');
sp.close();
})();
For security reasons browsers have very limited access to the machines resources, writing browser ran Javascript (client-side) is not a viable option
The above example returns the following results:
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
2 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 d a 3 b d
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0d 0a 03 0b 0d
Send any of your meters on your RS-485 network this simple request (including the 12 digit meter number) and the targeted meter will respond with most of its data.
To connect the request would be structured like: 2F 3F (12 Bytes Address) 30 30 21 0D 0A
Example: \x2F\x3F\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x30\x30\x21\x0D\x0A
Request A Parameters for a v.4 Meter
Parameter | Description |
---|---|
2F 3F | |
30 30 30 33 30 30 30 30 31 31 38 34 | 12 byte address = meter number: 000300001184 |
30 30 | type of request. This hex code is for a type A v.4 meter request |
21 0D 0A |
Response from a v.4 Meter
Here is a typical type A response hex code from a v4 meter:
02 10 24 14 30 30 30 33 30 30 30 30 31 31 38 34 31 34 38 39 32 34 30 33 30 31 33 39 39 34 38 39 30 33 33 36 30 33 33 31 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 30 30 30 30 30 30 34 35 30 30 30 30 30 30 30 37 31 32 33 39 31 32 33 39 31 32 33 39 30 30 30 37 30 30 30 30 38 30 30 30 30 36 38 30 30 30 30 38 36 36 30 30 30 30 39 39 34 30 30 30 30 38 36 36 30 30 30 32 37 32 36 30 31 30 30 4c 30 39 39 43 30 30 30 30 30 30 30 30 34 36 30 30 30 30 31 30 38 30 30 30 30 30 34 36 30 30 30 30 32 30 30 36 30 30 35 30 30 30 35 32 33 36 34 30 30 37 34 35 33 32 37 30 30 30 33 36 37 33 34 30 31 31 30 30 30 32 32 30 32 32 31 30 31 32 33 33 37 30 31 30 30 21 0D 0A 03 0B 0D
Parameter | Description |
---|---|
02 | 1 byte address = informs the computer that a packet is coming from meter ( reads ) |
10 24 | 2 byte address = model of meter. Example: EKM-Omnimeter Pulse v.4 |
14 | 1 byte address = firmware version |
30 30 30 33 30 30 30 30 31 31 38 34 | 12 byte address = meter number: 000300001184 |
31 34 38 39 32 34 30 33 | 8 byte address = Kilowatt Hour Total |
30 31 33 39 39 34 38 39 | 8 byte address = Reactive Energy Total |
30 33 33 36 30 33 33 31 | 8 byte address = Rev Kilowatt Hour Total |
30 34 37 39 31 33 30 33 | 8 byte address = Kilowatt Hour L1 |
30 35 30 35 30 35 35 30 | 8 byte address = Kilowatt Hour L2 |
30 35 30 35 30 35 35 30 | 8 byte address = Kilowatt Hour L3 |
30 30 33 38 38 35 36 30 | 8 byte address = Reverse Kilowatt Hour L1 |
30 32 35 38 33 32 31 31 | 8 byte address = Reverse Kilowatt Hour L2 |
30 30 33 38 38 35 36 30 | 8 byte address = Reverse Kilowatt Hour L3 |
30 30 30 30 30 30 34 35 | 8 byte address = Resettable Kilowatt Hour Total |
30 30 30 30 30 30 30 37 | 8 byte address = Resettable Reverse Kilowatt Hour Total |
31 32 33 39 | 4 byte address = RMS Volts L1 |
31 32 33 39 | 4 byte address = RMS Volts L2 |
31 32 33 39 | 4 byte address = RMS Volts L3 |
30 30 30 37 30 | 5 byte address = Amps L1 |
30 30 30 38 30 | 5 byte address = Amps L2 |
30 30 30 36 38 | 5 byte address = Amps L3 |
30 30 30 30 38 36 36 | 7 byte address = RMS Watts L1 |
30 30 30 30 39 39 34 | 7 byte address = RMS Watts L2 |
30 30 30 30 38 36 36 | 7 byte address = RMS Watts L3 |
30 30 30 32 37 32 36 | 7 byte address = RMS Total Watts |
30 31 30 30 | 4 byte address = Cos Theta L1 |
4c 30 39 39 | 4 byte address = Cos Theta L2 |
43 30 30 30 | 4 byte address = Cos Theta L3 |
30 30 30 30 30 34 36 | 7 byte address = Reactive Power L1 |
30 30 30 30 31 30 38 | 7 byte address = Reactive Power L2 |
30 30 30 30 30 34 36 | 7 byte address = Reactive Power L3 |
30 30 30 30 32 30 30 | 7 byte address = Reactive Power Total |
36 30 30 35 | 4 byte address = Line Frequency |
30 30 30 35 32 33 36 34 | 8 byte address = Pulse Count 1 |
30 30 37 34 35 33 32 37 | 8 byte address = Pulse Count 2 |
30 30 30 33 36 37 33 34 | 8 byte address = Pulse Count 3 |
30 | 1 byte address = Indicates Pulse Input State High/Low. Example: 30 = (0) Low/Low/Low, 31 = (1) Low/Low/High, 32 = (2) Low/High/Low, 33 = (3) Low/High/High, 34 = (4) High/Low/Low, 35 = (5) High/Low/High, 36 = (6) High/High/Low, 37 = (7) High/High/High |
31 | 1 byte address = Indicates direction of current for L1, L2, L3. Example: 31 = (1) Forward/Forward/Forward, 32 = (2) Forward/Forward/Reverse, 33 = (3) Forward/Reverse/Forward, 34 = (4) Reverse/Forward/Forward, 35 = (5) Forward/Reverse/Reverse, 36 = (6) Reverse/Forward/Reverse, 37 = (7) Reverse/Reverse/Forward, 38 = (8) Reverse/Reverse/Reverse |
31 | 1 byte address = Indicates outputs. Example: 31 = (1) OFF/OFF, 32 = (2) OFF/ON, 33 = (3) ON/OFF, 34 = (4) ON/ON |
30 | 1 byte address = Indicates Kilowatt Hour decimal places. Example: 30 = 0 decimal places, 31 = 1 decimal place, 32 = 2 decimal places |
30 30 | 2 byte address = Reserved |
32 32 30 32 32 31 30 31 32 33 33 37 30 31 | 14 byte address = Date and Time |
30 30 | 2 byte address = type |
21 0D 0A 03 | 4 byte address = End Session Request |
0B 0D | 2 byte address = CRC-16 checksum |
Here is a PDF of the breakdown of the send and response hex code.
CRC-16 Checksum
CRC-16 Checksum
# Ruby Version: 2.7.0
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
];
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
def ekm_calc_crc16(buf)
crc = 0xffff
i = 0
while i < buf.length do
c = buf[i].chr
index = (crc ^ c.ord) & 0xff
crct = CRCTABLE[index]
crc=(crc>>8)^crct
i +=1
end
crc = (crc << 8) | (crc >> 8);
crc &= 0x7F7F;
return crc
end
# This is an example that will print the CRC value for a message received FROM a V4 meter
crc=ekm_calc_crc16("\x10\x24\x14\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x31\x34\x38\x39\x32\x34\x30\x33\x30\x31\x33\x39\x39\x34\x38\x39\x30\x33\x33\x36\x30\x33\x33\x31\x30\x34\x37\x39\x31\x33\x30\x33\x30\x35\x30\x35\x30\x35\x35\x30\x30\x35\x30\x35\x30\x35\x35\x30\x30\x30\x33\x38\x38\x35\x36\x30\x30\x32\x35\x38\x33\x32\x31\x31\x30\x30\x33\x38\x38\x35\x36\x30\x30\x30\x30\x30\x30\x30\x34\x35\x30\x30\x30\x30\x30\x30\x30\x37\x31\x32\x33\x39\x31\x32\x33\x39\x31\x32\x33\x39\x30\x30\x30\x37\x30\x30\x30\x30\x38\x30\x30\x30\x30\x36\x38\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x30\x39\x39\x34\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x32\x37\x32\x36\x30\x31\x30\x30\x4c\x30\x39\x39\x43\x30\x30\x30\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x31\x30\x38\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x32\x30\x30\x36\x30\x30\x35\x30\x30\x30\x35\x32\x33\x36\x34\x30\x30\x37\x34\x35\x33\x32\x37\x30\x30\x30\x33\x36\x37\x33\x34\x30\x31\x31\x30\x30\x30\x32\x32\x30\x32\x32\x31\x30\x31\x32\x33\x33\x37\x30\x31\x30\x30\x21\x0d\x0a\x03");
require 'pp'
pp crc.to_s(16)
'''
Python version: 3.8.10
'''
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
]
def ekm_calc_crc16(buf):
'''
This function makes use of the CRC table to calulate the CRC
example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF")
'''
crc = 0xffff
for c_value in buf:
index = (crc ^ ord( c_value )) & 0xff
crct = CRCTABLE[index]
crc=(crc>>8)^crct
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
return crc
# This is an example that will print the CRC value for a message received FROM a V4 meter
crc_value=ekm_calc_crc16("\x10\x24\x14\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x31\x34\
\x38\x39\x32\x34\x30\x33\x30\x31\x33\x39\x39\x34\x38\x39\x30\x33\x33\x36\x30\x33\x33\x31\
\x30\x34\x37\x39\x31\x33\x30\x33\x30\x35\x30\x35\x30\x35\x35\x30\x30\x35\x30\x35\x30\x35\
\x35\x30\x30\x30\x33\x38\x38\x35\x36\x30\x30\x32\x35\x38\x33\x32\x31\x31\x30\x30\x33\x38\
\x38\x35\x36\x30\x30\x30\x30\x30\x30\x30\x34\x35\x30\x30\x30\x30\x30\x30\x30\x37\x31\x32\
\x33\x39\x31\x32\x33\x39\x31\x32\x33\x39\x30\x30\x30\x37\x30\x30\x30\x30\x38\x30\x30\x30\
\x30\x36\x38\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x30\x39\x39\x34\x30\x30\x30\x30\x38\
\x36\x36\x30\x30\x30\x32\x37\x32\x36\x30\x31\x30\x30\x4c\x30\x39\x39\x43\x30\x30\x30\x30\
\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x31\x30\x38\x30\x30\x30\x30\x30\x34\x36\x30\x30\
\x30\x30\x32\x30\x30\x36\x30\x30\x35\x30\x30\x30\x35\x32\x33\x36\x34\x30\x30\x37\x34\x35\
\x33\x32\x37\x30\x30\x30\x33\x36\x37\x33\x34\x30\x31\x31\x30\x30\x30\x32\x32\x30\x32\x32\
\x31\x30\x31\x32\x33\x33\x37\x30\x31\x30\x30\x21\x0d\x0a\x03")
print(hex(crc_value))
<?php
/*
* Requirements:
* PHP: 8.1.5
*/
// EKM CRC-16 Table
$CRCTABLE = array(
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
);
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
function ekm_calc_crc16($buf){
$crc = 0xffff;
global $CRCTABLE;
$len = strlen($buf);
for($i=0; $i< $len; $i++){
$c = $buf[$i];
$index = ($crc ^ ord($c)) & 0xff;
$crct = $CRCTABLE[$index];
$crc=($crc>>8) ^ $crct;
}
$crc = ($crc << 8) | ($crc >> 8);
$crc &= 0x7F7F;
return $crc;
}
// This is an example that will print the CRC value for a message received FROM a V4 meter
echo dechex(ekm_calc_crc16("\x10\x24\x14\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x31\x34\x38\x39\x32\x34\x30\x33\x30\x31\x33\x39\x39\x34\x38\x39\x30\x33\x33\x36\x30\x33\x33\x31\x30\x34\x37\x39\x31\x33\x30\x33\x30\x35\x30\x35\x30\x35\x35\x30\x30\x35\x30\x35\x30\x35\x35\x30\x30\x30\x33\x38\x38\x35\x36\x30\x30\x32\x35\x38\x33\x32\x31\x31\x30\x30\x33\x38\x38\x35\x36\x30\x30\x30\x30\x30\x30\x30\x34\x35\x30\x30\x30\x30\x30\x30\x30\x37\x31\x32\x33\x39\x31\x32\x33\x39\x31\x32\x33\x39\x30\x30\x30\x37\x30\x30\x30\x30\x38\x30\x30\x30\x30\x36\x38\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x30\x39\x39\x34\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x32\x37\x32\x36\x30\x31\x30\x30\x4c\x30\x39\x39\x43\x30\x30\x30\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x31\x30\x38\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x32\x30\x30\x36\x30\x30\x35\x30\x30\x30\x35\x32\x33\x36\x34\x30\x30\x37\x34\x35\x33\x32\x37\x30\x30\x30\x33\x36\x37\x33\x34\x30\x31\x31\x30\x30\x30\x32\x32\x30\x32\x32\x31\x30\x31\x32\x33\x33\x37\x30\x31\x30\x30\x21\x0d\x0a\x03"));
?>
# Perl version: v5.30.0
# EKM CRC-16 Table
use constant CRCTABLE => [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
];
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
sub ekm_calc_crc16 {
my $buf = shift(@_);
my $crc=0xffff;
my $s;
my @arr = split("", $buf);
for(my $i=0; $i<length($buf); $i++) {
my $chr = ord($arr[$i]);
$s = ($crc ^ $chr) & 0xff;
my $crct = CRCTABLE->[$s];
$crc = ($crc>>8) ^ $crct;
}
$crc = ($crc << 8) | ($crc >> 8);
$crc &= 0x7F7F;
return $crc;
}
# This is an example call of the ekm_calc_crc16 function
# that will print the CRC value for a message received FROM a V4 meter
printf ("%x",ekm_calc_crc16("\x10\x24\x14\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x31\x34\x38\x39\x32\x34\x30\x33\x30\x31\x33\x39\x39\x34\x38\x39\x30\x33\x33\x36\x30\x33\x33\x31\x30\x34\x37\x39\x31\x33\x30\x33\x30\x35\x30\x35\x30\x35\x35\x30\x30\x35\x30\x35\x30\x35\x35\x30\x30\x30\x33\x38\x38\x35\x36\x30\x30\x32\x35\x38\x33\x32\x31\x31\x30\x30\x33\x38\x38\x35\x36\x30\x30\x30\x30\x30\x30\x30\x34\x35\x30\x30\x30\x30\x30\x30\x30\x37\x31\x32\x33\x39\x31\x32\x33\x39\x31\x32\x33\x39\x30\x30\x30\x37\x30\x30\x30\x30\x38\x30\x30\x30\x30\x36\x38\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x30\x39\x39\x34\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x32\x37\x32\x36\x30\x31\x30\x30\x4c\x30\x39\x39\x43\x30\x30\x30\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x31\x30\x38\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x32\x30\x30\x36\x30\x30\x35\x30\x30\x30\x35\x32\x33\x36\x34\x30\x30\x37\x34\x35\x33\x32\x37\x30\x30\x30\x33\x36\x37\x33\x34\x30\x31\x31\x30\x30\x30\x32\x32\x30\x32\x32\x31\x30\x31\x32\x33\x33\x37\x30\x31\x30\x30\x21\x0d\x0a\x03"));
/*
openjdk version "17.0.2" 2022-01-18
Instructions to run this program:
1. Put this code in a file named: EKMChecksum.java
2. Compile:
javac EKMChecksum.java
3. Run:
java EKMChecksum
*/
@java.lang.SuppressWarnings({"java:S106", "java:S112", "java:S125"})
// EKM CRC-16 Table
public class EKMChecksum {
private static final int[] CRCTABLE = {
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040 };
// This function makes use of the CRC table to calulate the CRC
// example: ekmCalcCrc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
public static int ekmCalcCrc16(String buf) {
int crc = 0xffff;
if (buf != null) {
char[] charArr = buf.toCharArray();
for (int i = 0, len = charArr.length; i < len; i++) {
int c = charArr[i];
int index = (crc ^ c) & 0xff;
int crct = CRCTABLE[index];
crc = ((crc >> 8) ^ crct);
}
crc = (crc << 8) | (crc >> 8);
crc &= 0x7F7F;
}
return crc;
}
public static void main(String[] args) {
// This is an example that will print the CRC value for a message received FROM a V4 meter
// HEX 0D and 0A can not be typed using unicode so use \r for 0D and \n for 0A
System.out.println(Integer.toHexString(ekmCalcCrc16("\u0010\u0024\u0014\u0030\u0030\u0030\u0033\u0030\u0030\u0030\u0030\u0031\u0031\u0038\u0034\u0031\u0034\u0038\u0039\u0032\u0034\u0030\u0033\u0030\u0031\u0033\u0039\u0039\u0034\u0038\u0039\u0030\u0033\u0033\u0036\u0030\u0033\u0033\u0031\u0030\u0034\u0037\u0039\u0031\u0033\u0030\u0033\u0030\u0035\u0030\u0035\u0030\u0035\u0035\u0030\u0030\u0035\u0030\u0035\u0030\u0035\u0035\u0030\u0030\u0030\u0033\u0038\u0038\u0035\u0036\u0030\u0030\u0032\u0035\u0038\u0033\u0032\u0031\u0031\u0030\u0030\u0033\u0038\u0038\u0035\u0036\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0034\u0035\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0037\u0031\u0032\u0033\u0039\u0031\u0032\u0033\u0039\u0031\u0032\u0033\u0039\u0030\u0030\u0030\u0037\u0030\u0030\u0030\u0030\u0038\u0030\u0030\u0030\u0030\u0036\u0038\u0030\u0030\u0030\u0030\u0038\u0036\u0036\u0030\u0030\u0030\u0030\u0039\u0039\u0034\u0030\u0030\u0030\u0030\u0038\u0036\u0036\u0030\u0030\u0030\u0032\u0037\u0032\u0036\u0030\u0031\u0030\u0030\u004c\u0030\u0039\u0039\u0043\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0030\u0034\u0036\u0030\u0030\u0030\u0030\u0031\u0030\u0038\u0030\u0030\u0030\u0030\u0030\u0034\u0036\u0030\u0030\u0030\u0030\u0032\u0030\u0030\u0036\u0030\u0030\u0035\u0030\u0030\u0030\u0035\u0032\u0033\u0036\u0034\u0030\u0030\u0037\u0034\u0035\u0033\u0032\u0037\u0030\u0030\u0030\u0033\u0036\u0037\u0033\u0034\u0030\u0031\u0031\u0030\u0030\u0030\u0032\u0032\u0030\u0032\u0032\u0031\u0030\u0031\u0032\u0033\u0033\u0037\u0030\u0031\u0030\u0030\u0021\r\n\u0003")));
}
}
/*
* Requirements
* NodeJS: v16.15.0
*/
// EKM CRC-16 Table
const CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040,
]
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
function EkmCalcCRC16(buf) {
let crc = 0xffff
for (let i = 0, len = buf.length; i < len; i++) {
const c = buf[i]
const index = (crc ^ c.charCodeAt(0)) & 0xff
const crct = CRCTABLE[index]
crc = (crc >> 8) ^ crct
}
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
return crc
}
// This is an example that will print the CRC value for a massage received FROM a V4 meter
console.log(EkmCalcCRC16('\x10\x24\x14\x30\x30\x30\x33\x30\x30\x30\x30\x31\x31\x38\x34\x31\x34\x38\x39\x32\x34\x30\x33\x30\x31\x33\x39\x39\x34\x38\x39\x30\x33\x33\x36\x30\x33\x33\x31\x30\x34\x37\x39\x31\x33\x30\x33\x30\x35\x30\x35\x30\x35\x35\x30\x30\x35\x30\x35\x30\x35\x35\x30\x30\x30\x33\x38\x38\x35\x36\x30\x30\x32\x35\x38\x33\x32\x31\x31\x30\x30\x33\x38\x38\x35\x36\x30\x30\x30\x30\x30\x30\x30\x34\x35\x30\x30\x30\x30\x30\x30\x30\x37\x31\x32\x33\x39\x31\x32\x33\x39\x31\x32\x33\x39\x30\x30\x30\x37\x30\x30\x30\x30\x38\x30\x30\x30\x30\x36\x38\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x30\x39\x39\x34\x30\x30\x30\x30\x38\x36\x36\x30\x30\x30\x32\x37\x32\x36\x30\x31\x30\x30\x4c\x30\x39\x39\x43\x30\x30\x30\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x31\x30\x38\x30\x30\x30\x30\x30\x34\x36\x30\x30\x30\x30\x32\x30\x30\x36\x30\x30\x35\x30\x30\x30\x35\x32\x33\x36\x34\x30\x30\x37\x34\x35\x33\x32\x37\x30\x30\x30\x33\x36\x37\x33\x34\x30\x31\x31\x30\x30\x30\x32\x32\x30\x32\x32\x31\x30\x31\x32\x33\x33\x37\x30\x31\x30\x30\x21\x0d\x0a\x03').toString(16))
Once you have established a connection to the meter, you will want to start making use of the crc-16 checksum in communication with the meter. You do not need to validate the checksum on messages you receive from the meter, however, it is handy to verify that the message you received was complete. If the checksum fails then you can simply dump the message from the meter and send another request.
The second reasons for the checksum is, in order to send data to the meter you have to add the crc-16 checksum to your message, the meter will verify its correct before applying the command. Checksums are required to send instructions to the meter.
On messages you receive from the meter the checksum is calculated from the 2nd byte to the 3rd from the last byte.
For example when you receive a Type A response from a V4 meter you would get this:
1 Byte - 02 | This first byte ( 02 ) indicates packet coming from meter. Not used to calculate CRC-16 Checksum |
2 Bytes - 10 24 | This is the meter version type. This is where the CRC-16 Checksum starts |
1 Byte - 14 | This is the meter Firmware |
12 Bytes - 30 30 30 33 30 30 30 30 31 31 38 34 | This is the meter Number |
8 Bytes - 31 34 38 39 32 34 30 33 | This is the Total Active Kilowatt Hour |
8 Bytes - 30 31 33 39 39 34 38 39 | This is the Total Kilo Volt Amperes Reactive Hours |
8 Bytes - 30 33 33 36 30 33 33 31 | This is the Total Rev.kWh |
24 Bytes - 30 34 37 39 31 33 30 33 30 35 30 35 30 35 35 30 30 35 30 35 30 35 35 30 | This is 3 phase Kilowatt Hour |
24 Bytes - 30 30 33 38 38 35 36 30 30 32 35 38 33 32 31 31 30 30 33 38 38 35 36 30 | This is 3 phase Rev.kWh |
8 Bytes - 30 30 30 30 30 30 34 35 | This is Resettable Kilowatt Hour |
8 Bytes - 30 30 30 30 30 30 30 37 | This is Resettable Reverse kWh |
4 Bytes - 31 32 33 39 | This is Volts L1 |
4 Bytes - 31 32 33 39 | This is Volts L2 |
4 Bytes - 31 32 33 39 | This is Volts L3 |
5 Bytes - 30 30 30 37 30 | This is Amps L1 |
5 Bytes - 30 30 30 38 30 | This is Amps L2 |
5 Bytes - 30 30 30 36 38 | This is Amps L3 |
7 Bytes - 30 30 30 30 38 36 36 | This is Watts L1 |
7 Bytes - 30 30 30 30 39 39 34 | This is Watts L2 |
7 Bytes - 30 30 30 30 38 36 36 | This is Watts L3 |
7 Bytes - 30 30 30 32 37 32 36 | This is Total Watts |
4 Bytes - 30 31 30 30 | This is Cos Theta L1 |
4 Bytes - 4c 30 39 39 | This is Cos Theta L2 |
4 Bytes - 43 30 30 30 | This is Cos Theta L3 |
7 Bytes - 30 30 30 30 30 34 36 | This is Reactive Power L1 |
7 Bytes - 30 30 30 30 31 30 38 | This is Reactive Power L2 |
7 Bytes - 30 30 30 30 30 34 36 | This is Reactive Power L3 |
7 Bytes - 30 30 30 30 32 30 30 | This is Total Reactive Power |
4 Bytes - 36 30 30 35 | This is Frequency |
8 Bytes - 30 30 30 35 32 33 36 34 | This is Pulse Count 1 |
8 Bytes - 30 30 37 34 35 33 32 37 | This is Pulse Count 2 |
8 Bytes - 30 30 30 33 36 37 33 34 | This is Pulse Count 3 |
1 Byte - 30 | This is Pulse Input Hi/Lo |
1 Byte - 31 | This is Direction of Current |
1 Byte - 31 | This is Outputs On/Off |
1 Byte - 30 | This is Kilowatt Hour Data Decimal Places |
2 Bytes - 30 30 | This is Reserved space |
14 Bytes - 32 32 30 32 32 31 30 31 32 33 33 37 30 31 | This is the Current Time |
2 Bytes - 30 30 | This is Type A response from Meter |
4 Bytes - 21 0D 0A 03 | This is the Close String. This is were the CRC-16 calculation of the checksum ends |
2 Bytes - ?? ?? | This is were the CRC-16 Checksum will be placed |
Where the first byte ( 02 ) is skipped and the last 2 CRC-16 checksum bytes are not used when sending it to the ekm_calc_crc16 function.
For sending instructions to the meter the checksum starts from the 2nd byte and goes to the end of the instruction string, then you append the checksum to the end before sending it to the meter.
For example, the instruction would look like this: 01 52 31 02 30 30 31 32 03.
You would not include the first byte of 01, you would call the ekm_calc_crc16 function only for 52 31 02 30 30 31 32 03.
The CRC-16 function would then return a checksum of: 2e 65.
Now that you have the checksum value you will append it to the end of the instruction string: 52 31 02 30 30 31 32 03 2e 65.
The first byte of 01 will also be included back into the instruction string before it is sent to the meter: 01 52 31 02 30 30 31 32 03 2e 65
The completed instruction string of: 01 52 31 02 30 30 31 32 03 2e 65, is now ready to be sent to the meter.
The example instruction code above represents how a typical CRC-16 checksum will be generated by calling on the ekm_calc_crc16 function. The instruction code can be a response from a message received by a v4 meter.
Sending Requests
Sending Requests to Meter
# Ruby Version: 2.7.0
# Serialport version: 1.3.2
# Requires the SerialPort gem
# (http://rubygems.org/gems/serialport)
# gem install serialport
require 'serialport'
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
].freeze
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
def ekm_calc_crc16(buf) # rubocop:disable Metrics/MethodLength
crc = 0xffff
i = 0
while i < buf.length
c = buf[i].chr
index = (crc ^ c.ord) & 0xff
crct = CRCTABLE[index]
crc = (crc >> 8) ^ crct
i += 1
end
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
crc
end
# In this code example we make a simple function that will take the command
# you want to send to the meter and make the crc for it before sending.
# After sending we get the response from the meter and check its crc to make sure
# the response is correct
def send_rec(send) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# get the the 2nd through the last byte
# from the send string to create crc
crc = ekm_calc_crc16(send[1..])
# Convert CRC from int to byte and add to the send request
send = send + ((crc >> 8) & 0xFF).chr + (crc & 0xFF).chr
# Send request
SP.write(send)
# Set Timeout
timeout = 5
# Get meter response
now = Time.now
read = ''
response = ''
count = 0
while count < 255
read = SP.getc
if read
now = Time.now
count += 1
response.concat(read)
end
break if Time.now > now + timeout
end
if response == ''
print("Meter Instruction: \n")
print(send.unpack('C*').map { |e| format('%02X', e) }.join(' '))
print("\nDid not trigger a response\n\n")
return
end
# The meter returns HEX 06 if ok
print("OK.\n\n") if response[0] == "\x06"
# If the meter returns HEX 02 it's the start of a text read
if response[0] == "\x02" # rubocop:disable Style/GuardClause
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
crc = ekm_calc_crc16(response[1..-3])
# Compare our calculated CRC with the resonse CRC
if ((crc >> 8) & 0xFF).chr + (crc & 0xFF).chr == response[-2..]
# Show response
print(response.unpack('C*').map { |e| format('%02X', e) }.join(' '))
print("\n\n")
else
print("Meter response CRC failed\n\n")
end
end
end
# Params for serial port
# Change /dev/ttyUSB0 to match your system
port_str = '/dev/ttyUSB0'
baud_rate = 9600
data_bits = 7
stop_bits = 1
parity = SerialPort::EVEN
flow = SerialPort::NONE
# Open connection to serial port
SP = SerialPort.new(port_str, baud_rate, data_bits, stop_bits, parity)
SP.flow_control = flow
# Meter Number and Password
# Default password: 00000000
meter = '000300001184'
password = '00000000'
# In this example we have assigned
# some meter request strings to variables
# Get meter data A request and connection request
cmd_request_a = "\x2F\x3F#{meter}\x30\x30\x21\x0D\x0A"
# Get meter data B request
cmd_request_b = "\x2F\x3F#{meter}\x30\x31\x21\x0D\x0A"
# Get last 6 months (total kwh)
cmd_6mo_totkwh = "\x01\x52\x31\x02\x30\x30\x31\x31\x03"
# Get last 6 months (reverse kwh)
cmd_6mo_revkwh = "\x01\x52\x31\x02\x30\x30\x31\x32\x03"
# Send Password String
cmd_send_passwd = "\x01\x50\x31\x02\x28#{password}\x29\x03"
# Relay Open and Close Request Stings
cmd_relay_close_b = "\x01\x57\x31\x02\x30\x30\x38\x32\x28\x30\x30\x30\x30\x30\x29\x03"
cmd_relay_close_a = "\x01\x57\x31\x02\x30\x30\x38\x31\x28\x30\x30\x30\x30\x30\x29\x03"
cmd_relay_open_a = "\x01\x57\x31\x02\x30\x30\x38\x31\x28\x31\x30\x30\x30\x30\x29\x03"
cmd_relay_open_b = "\x01\x57\x31\x02\x30\x30\x38\x32\x28\x31\x30\x30\x30\x30\x29\x03"
# Close Connection String
cmd_close = "\x01\x42\x30\x03\x75"
print("So let's give it a try...\n")
print("First we open the connection to the meter\n")
print("It will respond with the first 255 bytes of meter data (Request A)\n")
send_rec(cmd_request_a)
print("Now we can request other data\n")
print("For example the remaining 255 bytes of meter data (Request B)\n")
send_rec(cmd_request_b)
print("Or request things like the last 6 months of total kwh and reverse kwh\n")
send_rec(cmd_6mo_totkwh)
send_rec(cmd_6mo_revkwh)
print("When requesting information from the meter (reads)\n")
print("you can send as many requests as you like once you sent\n")
print("the first connection request (Request A)\n\n")
print("Now let's try sending a setting to the meter\n")
print("Unlike reading information from the meter\n")
print("You have to send the connection string and password\n")
print("before each setting request\n\n")
print("Now we will loop 3 times opening and closing the relays\n")
[1, 2, 3].each do |n|
print("Loop: #{n}\n")
print("Send Connection Request A\n")
send_rec(cmd_request_a)
print("Send Password\n")
send_rec(cmd_send_passwd)
print("Open Relay A\n")
send_rec(cmd_relay_open_a)
print("Send Connection Request A\n")
send_rec(cmd_request_a)
print("Send Password\n")
send_rec(cmd_send_passwd)
print("Close Relay A\n")
send_rec(cmd_relay_close_a)
print("Send Connection Request A\n")
send_rec(cmd_request_a)
print("Send Password\n")
send_rec(cmd_send_passwd)
print("Open Relay B\n")
send_rec(cmd_relay_open_b)
print("Send Connection Request A\n")
send_rec(cmd_request_a)
print("Send Password\n")
send_rec(cmd_send_passwd)
print("Close Relay B\n")
send_rec(cmd_relay_close_b)
end
print("When you're finished you will close the connection\n")
send_rec(cmd_close)
# Close connection to serial port
SP.close
'''
Python version: 3.8.10
Modules:
Pyserial version: 3.4
Requires pyserial module:
pip3 install pyserial
'''
import serial
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
]
def ekm_calc_crc16(buf):
'''
This function makes use of the CRC table to calulate the CRC
example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
'''
crc = 0xffff
for c_value in buf:
index = (crc ^ ord( c_value )) & 0xff
crct = CRCTABLE[index]
crc=(crc>>8)^crct
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
return crc
def send_rec(send):
'''
In this code example we make a simple function that will take the command
you want to send to the meter and make the crc for it before sending.
After sending we get the response from the meter and check its crc to make sure
the response is correct
'''
# get the the 2nd through the last byte
# from the send string to create crc
crc = ekm_calc_crc16(send[1:])
# Convert CRC from int to byte and add to the send request
send=send+chr((crc >> 8) & 0xFF) + chr(crc & 0xFF)
# Send request
sp.write(send.encode())
# Get meter response
response = sp.read(255)
response=str(response,'ascii')
if response=="":
print("Meter Instruction: ")
print(" ".join("{0:02x}".format(ord( c )) for c in send))
print("Did not trigger a response\n\n")
return
# The meter returns HEX 06 if ok
if response[0]=="\x06":
print("OK.\n\n")
# If the meter returns HEX 02 it's the start of a text read
if response[0]=="\x02":
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
crc = ekm_calc_crc16(response[1:-2])
# Compare our calculated CRC with the response CRC
if chr((crc >> 8) & 0xFF) + chr(crc & 0xFF)==response[-2:]:
# Show response
print(" ".join("{0:02x}".format(ord( c )) for c in response)+"\n\n")
else:
print("Meter response CRC failed\n\n")
# Open connection to serial port
# Change /dev/ttyUSB0 to match your system
sp = serial.Serial(
port='/dev/ttyUSB0',
baudrate=9600,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.SEVENBITS,
xonxoff=0,
timeout=5
)
# Meter Number and Password
# Default password: 00000000
METER="000300001184"
PASSWORD="00000000"
# In this example we have assigned
# some meter request strings to variables
# Get meter data A request and connection request
CMD_REQUEST_A="\x2F\x3F"+METER+"\x30\x30\x21\x0D\x0A"
# Get meter data B request
CMD_REQUEST_B="\x2F\x3F"+METER+"\x30\x31\x21\x0D\x0A"
# Get last 6 months (total kwh)
CMD_6MO_TOTKWH="\x01\x52\x31\x02\x30\x30\x31\x31\x03"
# Get last 6 months (reverse kwh)
CMD_6MO_REVKWH="\x01\x52\x31\x02\x30\x30\x31\x32\x03"
# Send Password String
CMD_SEND_PASSWD="\x01\x50\x31\x02\x28"+PASSWORD+"\x29\x03"
# Relay Open and Close Request Stings
CMD_RELAY_OPEN_B="\x01\x57\x31\x02\x30\x30\x38\x32\x28\x30\x30\x30\x30\x30\x29\x03"
CMD_RELAY_OPEN_A="\x01\x57\x31\x02\x30\x30\x38\x31\x28\x30\x30\x30\x30\x30\x29\x03"
CMD_RELAY_CLOSE_A="\x01\x57\x31\x02\x30\x30\x38\x31\x28\x31\x30\x30\x30\x30\x29\x03"
CMD_RELAY_CLOSE_B="\x01\x57\x31\x02\x30\x30\x38\x32\x28\x31\x30\x30\x30\x30\x29\x03"
# Close Connection String
CMD_CLOSE="\x01\x42\x30\x03\x75"
print("So let's give it a try...")
print("First we open the connection to the meter")
print("It will respond with the first 255 bytes of meter data (Request A)")
send_rec(CMD_REQUEST_A)
print("Now we can request other data")
print("For example the remaining 255 bytes of meter data (Request B)")
send_rec(CMD_REQUEST_B)
print("Or request things like the last 6 months of total kwh and reverse kwh")
send_rec(CMD_6MO_TOTKWH)
send_rec(CMD_6MO_REVKWH)
print("When requesting information from the meter (reads)")
print("you can send as many requests as you like once you sent")
print("the first connection request (Request A)\n\n")
print("Now let's try sending a setting to the meter")
print("Unlike reading information from the meter")
print("You have to send the connection string and password")
print("before each setting request\n\n")
print("Now we will loop 3 times opening and closing the relays")
for x in range(1,3):
print("Send Connection Request A")
send_rec(CMD_REQUEST_A)
print("Send Password")
send_rec(CMD_SEND_PASSWD)
print("Open Relay A")
send_rec(CMD_RELAY_OPEN_A)
print("Send Connection Request A")
send_rec(CMD_REQUEST_A)
print("Send Password")
send_rec(CMD_SEND_PASSWD)
print("Close Relay A")
send_rec(CMD_RELAY_CLOSE_A)
print("Send Connection Request A")
send_rec(CMD_REQUEST_A)
print("Send Password")
send_rec(CMD_SEND_PASSWD)
print("Open Relay B")
send_rec(CMD_RELAY_OPEN_B)
print("Send Connection Request A")
send_rec(CMD_REQUEST_A)
print("Send Password")
send_rec(CMD_SEND_PASSWD)
print("Close Relay B")
send_rec(CMD_RELAY_CLOSE_B)
print("When you're finished you will close the connection")
send_rec(CMD_CLOSE)
# Close connection to serial port
sp.close()
<?php
/*
* Requirements:
* PHP: 8.1.5
* DIO: 0.2.1
*
* Install php8.1-dev on ubuntu to compile DIO module.
* apt install php8.1-dev
* Compile DIO module from source:
* Download the latest tarball from: https://pecl.php.net/package/dio
* Uncompress: tar xfz dio-0.2.1.tgz
* cd dio-0.2.1/
* phpize
* ./configure
* make
* sudo make install
* You will see a path like: /usr/lib/php/20210902/
* Edit your php.ini
* vi /etc/php/8.1/cli/php.ini
* In the extension section add:
* extension=/usr/lib/php/20210902/dio.so
* Now you can run your php script.
* Ex: php connect.php
*/
// EKM CRC-16 Table
$CRCTABLE = array(
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
);
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
$sp = dio_open( '/dev/ttyUSB0', O_RDWR | O_NONBLOCK );
// Configure the port
dio_tcsetattr( $sp, array(
'baud' => 9600,
'bits' => 7,
'stop' => 1,
'parity' => 2,
'flow_control' => 0,
'is_canonical' => 0
) );
// Meter Number and Password
// Default password: 00000000
$meter="000300001184";
$password="00000000";
// In this example we have assigned
// some meter request strings to variables
// Get meter data A request and connection request
$cmd_request_a="\x2F\x3F".$meter."\x30\x30\x21\x0D\x0A";
// Get meter data B request
$cmd_request_b="\x2F\x3F".$meter."\x30\x31\x21\x0D\x0A";
// Get last 6 months (total kwh)
$cmd_6mo_totkwh="\x01\x52\x31\x02\x30\x30\x31\x31\x03";
// Get last 6 months (reverse kwh)
$cmd_6mo_revkwh="\x01\x52\x31\x02\x30\x30\x31\x32\x03";
// Send Password String
$cmd_send_passwd="\x01\x50\x31\x02\x28".$password."\x29\x03";
// Relay Open and Close Request Stings
$cmd_relay_open_b="\x01\x57\x31\x02\x30\x30\x38\x32\x28\x30\x30\x30\x30\x30\x29\x03";
$cmd_relay_open_a="\x01\x57\x31\x02\x30\x30\x38\x31\x28\x30\x30\x30\x30\x30\x29\x03";
$cmd_relay_close_a="\x01\x57\x31\x02\x30\x30\x38\x31\x28\x31\x30\x30\x30\x30\x29\x03";
$cmd_relay_close_b="\x01\x57\x31\x02\x30\x30\x38\x32\x28\x31\x30\x30\x30\x30\x29\x03";
// Close Connection String
$cmd_close="\x01\x42\x30\x03\x75";
echo "So let's give it a try...\n";
echo "First we open the connection to the meter\n";
echo "It will respond with the first 255 bytes of meter data (Request A)\n";
send_rec($cmd_request_a);
echo "Now we can request other data\n";
echo "For example the remaining 255 bytes of meter data (Request B)\n";
send_rec($cmd_request_b);
echo "Or request things like the last 6 months of total kwh and reverse kwh\n";
send_rec($cmd_6mo_totkwh);
send_rec($cmd_6mo_revkwh);
echo "When requesting information from the meter (reads)\n";
echo "you can send as many requests as you like once you sent\n";
echo "the first connection request (Request A)\n\n\n";
echo "Now let's try sending a setting to the meter\n";
echo "Unlike reading information from the meter\n";
echo "You have to send the connection string and password\n";
echo "before each setting request\n\n\n";
echo "Now we will loop 3 times opening and closing the relays\n";
for ($x=0; $x<3; $x++){
echo "Send Connection Request A\n";
send_rec($cmd_request_a);
echo "Send Password\n";
send_rec($cmd_send_passwd);
echo "Open Relay A\n";
send_rec($cmd_relay_open_a);
echo "Send Connection Request A\n";
send_rec($cmd_request_a);
echo "Send Password\n";
send_rec($cmd_send_passwd);
echo "Close Relay A\n";
send_rec($cmd_relay_close_a);
echo "Send Connection Request A\n";
send_rec($cmd_request_a);
echo "Send Password\n";
send_rec($cmd_send_passwd);
echo "Open Relay B\n";
send_rec($cmd_relay_open_b);
echo "Send Connection Request A\n";
send_rec($cmd_request_a);
echo "Send Password\n";
send_rec($cmd_send_passwd);
echo "Close Relay B\n";
send_rec($cmd_relay_close_b);
}
echo "When you're finished you will close the connection\n";
send_rec($cmd_close);
// Close connection to serial port
dio_close( $sp );
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter and check its crc to make sure
// the response is correct
function send_rec($send){
global $sp;
// get the the 2nd through the last byte
// from the send string to create crc
$crc = ekm_calc_crc16(substr($send,1));
// Convert CRC from int to byte and add to the send request
$send=$send.chr(($crc >> 8) & 0xFF).chr($crc & 0xFF);
// Send request
dio_write($sp,$send);
// Set Timeout
$timeout = 5;
// Get meter response
$read = '';
$response = '';
$now=time();
while( strlen($response)<255 && time()<$now+$timeout) {
$read = dio_read($sp,1);
$response .= $read;
}
if ($response==""){
echo "Meter Instruction: \n";
echo strToHex($send)."\n";
echo "Did not trigger a response\n\n";
return;
}
// The meter returns HEX 06 if ok
if (substr($response,0,1)=="\x06")
echo "OK.\n\n";
// If the meter returns HEX 02 it's the start of a text read
if (substr($response,0,1)=="\x02"){
// Get check CRC in response (byte 2 through the 3rd from the last)
// to make sure it's valid
$crc = ekm_calc_crc16(substr($response,1,-2));
// Compare our calculated CRC with the resonse CRC
if (chr(($crc >> 8) & 0xFF).chr($crc & 0xFF)==substr($response,-2))
// Show response
echo strToHex($response)."\n\n";
else
echo "Meter response CRC failed\n\n";
}
}
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
function ekm_calc_crc16($buf){
$crc = 0xffff;
global $CRCTABLE;
$len = strlen($buf);
for($i=0; $i< $len; $i++){
$c = $buf[$i];
$index = ($crc ^ ord($c)) & 0xff;
$crct = $CRCTABLE[$index];
$crc=($crc>>8) ^ $crct;
}
$crc = ($crc << 8) | ($crc >> 8);
$crc &= 0x7F7F;
return $crc;
}
function strToHex($buf)
{
$hex='';
for ($i=0; $i < strlen($buf); $i++)
{
$hex .= sprintf("%02X",ord($buf[$i])).' ';
}
return $hex;
}
?>
#!/usr/bin/perl
# Perl version: v5.30.0
# CPAN.pm version v2.34
#
# Requires the Device::SerialPort module
# Device::SerialPort: 1.04
# Readonly: 2.05
#
# Install Perl Modules
# cpan Device::SerialPort
# cpan Readonly
use Carp;
use Readonly;
use Device::SerialPort;
use version; our $VERSION = qv(1.0);
# EKM CRC-16 Table
## no critic (ValuesAndExpressions::RequireNumberSeparators)
Readonly my $CRCTABLE => [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
];
## critic
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
sub ekm_calc_crc16
{ ## no critic (Subroutines::RequireFinalReturn Subroutines::RequireArgUnpacking)
my $buf = shift @_;
Readonly my $CRC => 0xffff;
Readonly my $CONST_FF => 0xff;
Readonly my $NUMBER_EIGHT => 8;
Readonly my $CONST_7F7F => 0x7F7F;
my $crc = $CRC;
my $s;
my @arr = split //ms, $buf;
for ( my $i = 0 ; $i < length $buf ; $i++ ) {
my $chr = ord $arr[$i];
$s = ( $crc ^ $chr ) & $CONST_FF;
my $crct = $CRCTABLE->[$s];
$crc = ( $crc >> $NUMBER_EIGHT ) ^ $crct;
}
$crc = ( $crc << $NUMBER_EIGHT ) | ( $crc >> $NUMBER_EIGHT );
$crc &= $CONST_7F7F;
return $crc;
}
# In this code example we make a simple function that will take the command
# you want to send to the meter and make the crc for it before sending.
# After sending we get the response from the meter and check its crc to make sure
# the response is correct
sub send_rec
{ ## no critic (Subroutines::RequireFinalReturn Subroutines::RequireArgUnpacking)
Readonly my $CONST_FF => 0xFF;
Readonly my $CONST_BYTE => 255;
Readonly my $NUMBER_EIGHT => 8;
Readonly my $NUMBER_MIN_TWO => -2;
#Set Timeout
Readonly my $TIMEOUT => 5;
my $timeout = $TIMEOUT;
my $send = shift @_;
# get the the 2nd through the last byte
# from the send string to create crc
my $crc = ekm_calc_crc16( substr $send, 1 );
# Convert CRC from int to byte and add to the send request
$send =
$send
. chr( ( $crc >> $NUMBER_EIGHT ) & $CONST_FF )
. chr( $crc & $CONST_FF ); ## no critic (CodeLayout::ProhibitParensWithBuiltins)
# Send request
$sp->write($send);
# Get meter response
my $count = 0;
my $response = q{};
while ( $timeout > 0 && $count < $CONST_BYTE ) {
my ( $chars, $read ) = $sp->read($CONST_BYTE);
if ( $chars > 0 ) {
$count += $chars;
$response .= $read;
}
else {
$timeout--;
}
}
## no critic (InputOutput::RequireCheckedSyscalls)
if ( length($response) == 0 ) {
print "Meter Instruction\n";
# Show response as HEX
$send =~ s/(.)/sprintf("%02X",ord($1))." "/seg; ## no critic (RegularExpressions::RequireLineBoundaryMatching RegularExpressions::RequireExtendedFormatting)
print $send;
print "\nDid not trigger a response\n\n";
return;
}
# The meter returns HEX 06 if ok
if ( substr( $response, 0, 1 ) eq "\x06" )
{ ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
print "OK.\n\n";
}
# If the meter returns HEX 02 it's the start of a text read
if ( substr( $response, 0, 1 ) eq "\x02"
&& length($response) == $CONST_BYTE )
{ ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
$crc = ekm_calc_crc16( substr $response, 1, $NUMBER_MIN_TWO );
# Compare our calculated CRC with the resonse CRC
if ((chr( ( $crc >> $NUMBER_EIGHT ) & $CONST_FF ). chr( $crc & $CONST_FF )) == substr $response, $NUMBER_MIN_TWO) ## no critic (CodeLayout::ProhibitParensWithBuiltins)
{
# Show response
$response =~ s/(.)/sprintf("%02X",ord($1))." "/seg; ## no critic (RegularExpressions::RequireLineBoundaryMatching RegularExpressions::RequireExtendedFormatting)
print $response. "\n\n";
}
else {
print "Meter response CRC failed\n\n";
}
}
## critic
}
# Params for serial port
# Change /dev/ttyUSB0 to match your system
# Open connection to serial port
$sp = new Device::SerialPort('/dev/ttyUSB0'); ## no critic (Objects::ProhibitIndirectSyntax)
Readonly my $BAUDRATE => 9600;
Readonly my $DATABITS => 7;
Readonly my $NUM_THOUSAND => 1000;
# Configure the port
$sp->user_msg(ON);
$sp->baudrate($BAUDRATE);
$sp->parity('even');
$sp->databits($DATABITS);
$sp->stopbits(1);
$sp->handshake('xoff');
$sp->write_settings;
# don't wait for each character
$sp->read_char_time(0);
# wait for 1 second per unfulfilled read
$sp->read_const_time($NUM_THOUSAND);
# Meter Number and Password
# Default password: 00000000
my $meter = '000300001184';
my $password = '00000000';
# In this example we have assigned
# some meter request strings to variables
## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Get meter data A request and connection request
my $cmd_request_a = "\x2F\x3F$meter\x30\x30\x21\x0D\x0A";
# Get meter data B request
my $cmd_request_b = "\x2F\x3F$meter\x30\x31\x21\x0D\x0A";
# Get last 6 months (total kwh)
my $cmd_6mo_totkwh = "\x01\x52\x31\x02\x30\x30\x31\x31\x03";
# Get last 6 months (reverse kwh)
my $cmd_6mo_revkwh = "\x01\x52\x31\x02\x30\x30\x31\x32\x03";
# Send Password String
my $cmd_send_passwd = "\x01\x50\x31\x02\x28$password\x29\x03";
# Relay Open and Close Request Stings
my $cmd_relay_open_b =
"\x01\x57\x31\x02\x30\x30\x38\x32\x28\x30\x30\x30\x30\x30\x29\x03";
my $cmd_relay_open_a =
"\x01\x57\x31\x02\x30\x30\x38\x31\x28\x30\x30\x30\x30\x30\x29\x03";
my $cmd_relay_close_a =
"\x01\x57\x31\x02\x30\x30\x38\x31\x28\x31\x30\x30\x30\x30\x29\x03";
my $cmd_relay_close_b =
"\x01\x57\x31\x02\x30\x30\x38\x32\x28\x31\x30\x30\x30\x30\x29\x03";
# Close Connection String
my $cmd_close = "\x01\x42\x30\x03\x75";
## critic
## no critic (InputOutput::RequireCheckedSyscalls)
print "So let's give it a try...\n";
print "First we open the connection to the meter\n";
print "It will respond with the first 255 bytes of meter data (Request A)\n";
send_rec($cmd_request_a);
print "Now we can request other data\n";
print "For example the remaining 255 bytes of meter data (Request B)\n";
send_rec($cmd_request_b);
print "Or request things like the last 6 months of total kwh and reverse kwh\n";
send_rec($cmd_6mo_totkwh);
send_rec($cmd_6mo_revkwh);
print "When requesting information from the meter (reads)\n";
print "you can send as many requests as you like once you sent\n";
print "the first connection request (Request A)\n\n";
print "Now let's try sending a setting to the meter\n";
print "Unlike reading information from the meter\n";
print "You have to send the connection string and password\n";
print "before each setting request\n\n";
print "Now we will loop 3 times opening and closing the relays\n";
Readonly my $NUM_THREE => 3;
for my $x ( 1 .. $NUM_THREE ) {
print "Send Connection Request A\n";
send_rec($cmd_request_a);
print "Send Password\n";
send_rec($cmd_send_passwd);
print "Open Relay A\n";
send_rec($cmd_relay_open_a);
print "Send Connection Request A\n";
send_rec($cmd_request_a);
print "Send Password\n";
send_rec($cmd_send_passwd);
print "Close Relay A\n";
send_rec($cmd_relay_close_a);
print "Send Connection Request A\n";
send_rec($cmd_request_a);
print "Send Password\n";
send_rec($cmd_send_passwd);
print "Open Relay B\n";
send_rec($cmd_relay_open_b);
print "Send Connection Request A\n";
send_rec($cmd_request_a);
print "Send Password\n";
send_rec($cmd_send_passwd);
print "Close Relay B\n";
send_rec($cmd_relay_close_b);
}
print "When you're finished you will close the connection\n";
send_rec($cmd_close);
## critic
# Close connection to serial port
$sp->close || croak 'failed to close';
undef $sp;
/*
openjdk version "17.0.2" 2022-01-18
Download the jssc.jar (java-simple-serial-connector)
from: https://code.google.com/archive/p/java-simple-serial-connector/
Version: jSSC-2.8.0
Instructions to run this program
1. Put this code in a file named Connect.java
2. Copy the downloaded jssc.jar and Connect.java to the same directory
3. Compile
javac -cp .:./jssc.jar ./Connect.java
4. Run
java -cp .:./jssc.jar Connect
*/
// Import required classes
import jssc.*;
@java.lang.SuppressWarnings({"java:S106", "java:S112", "java:S125"})
public class Connect {
static SerialPort serialPort;
// EKM CRC-16 Table
private static final int[] CRCTABLE = {
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040 };
// This function makes use of the CRC table to calulate the CRC
// example: ekmCalcCrc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
public static int ekmCalcCrc16(String buf) {
int crc = 0xffff;
if (buf != null) {
char[] charArr = buf.toCharArray();
for (int i = 0, len = charArr.length; i < len; i++) {
int c = charArr[i];
int index = (crc ^ c) & 0xff;
int crct = CRCTABLE[index];
crc = ((crc >> 8) ^ crct);
}
crc = (crc << 8) | (crc >> 8);
crc &= 0x7F7F;
}
return crc;
}
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter and check its crc to make sure
// the response is correct
public static void sendRec(String send) throws SerialPortException{
String firstByte;
String response;
byte[] buffer;
// get the the 2nd through the last byte
// from the send string to create crc
int crc = ekmCalcCrc16(send.substring(1));
send=send+(char)((crc >> 8) & 0xFF)+(char)(crc & 0xFF);
// Send request
serialPort.writeBytes(send.getBytes());
// Set connection timeout;
int timeout=5;
// Get byte 1 of meter response
try {
firstByte = serialPort.readString(1,timeout*1000);
} catch (SerialPortTimeoutException e1)
{
System.out.println("Meter Instruction: ");
System.out.println(byteArrayToHex(send)+"");
System.out.println("Did not trigger a response\n");
return;
}
// The meter returns HEX 06 if ok
if (firstByte.charAt(0)==(char)(6 & 0xFF)){
System.out.println("OK.\n");
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
return;
}
// Get rest of meter response
try {
buffer = serialPort.readBytes(254,timeout*1000);
} catch (SerialPortTimeoutException e1)
{
System.out.println("Did receive complete response\n");
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
return;
}
response = new String(buffer);
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
// If the meter returns HEX 02 it's the start of a text read
if (firstByte.charAt(0) == (char)(2 & 0xFF)){
// Put 02 back in the response beings it
// was pulled out when testing the first byte
response=""+(char)(2)+response;
// Get check CRC in response (byte 2 through the 3rd from the last)
// to make sure it's valid
crc = ekmCalcCrc16(response.substring(1, 253));
// Compare our calculated CRC with the resonse CRC
if ((char)((crc >> 8) & 0xFF)==response.charAt(253) && (char)(crc & 0xFF)==response.charAt(254)){
// Show response
System.out.println(byteArrayToHex(response)+"\n");
}
else{
System.out.println("Meter response CRC failed\n");
}
}
}
public static void main(String[] args) {
serialPort = new SerialPort("/dev/ttyUSB0");
try {
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
serialPort.openPort();
// Configure the port
serialPort.setParams(SerialPort.BAUDRATE_9600,
SerialPort.DATABITS_7,
SerialPort.STOPBITS_1,
SerialPort.PARITY_EVEN);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
// Meter Number and Password
// Default password: 00000000
String meter="000300001184";
String password="00000000";
// In this example we have assigned
// some meter request strings to variables
// Get meter data A request and connection request
String cmdRequestA="\u002F\u003F"+meter+"\u0030\u0030\u0021\r\n";
// Get meter data B request
String cmdRequestB="\u002F\u003F"+meter+"\u0030\u0031\u0021\r\n";
// Get last 6 months (total kwh)
String cmd6moTotkwh="\u0001\u0052\u0031\u0002\u0030\u0030\u0031\u0031\u0003";
// Get last 6 months (reverse kwh)
String cmd6moRevkwh="\u0001\u0052\u0031\u0002\u0030\u0030\u0031\u0032\u0003";
// Send Password String
String cmdSendPasswd="\u0001\u0050\u0031\u0002\u0028"+password+"\u0029\u0003";
// Relay Open and Close Request Stings
String cmdRelayOpenB="\u0001\u0057\u0031\u0002\u0030\u0030\u0038\u0032\u0028\u0030\u0030\u0030\u0030\u0030\u0029\u0003";
String cmdRelayOpenA="\u0001\u0057\u0031\u0002\u0030\u0030\u0038\u0031\u0028\u0030\u0030\u0030\u0030\u0030\u0029\u0003";
String cmdRelayCloseA="\u0001\u0057\u0031\u0002\u0030\u0030\u0038\u0031\u0028\u0031\u0030\u0030\u0030\u0030\u0029\u0003";
String cmdRelayCloseB="\u0001\u0057\u0031\u0002\u0030\u0030\u0038\u0032\u0028\u0031\u0030\u0030\u0030\u0030\u0029\u0003";
// Close Connection String
String cmdClose="\u0001\u0042\u0030\u0003\u0075";
System.out.println("So let's give it a try...");
System.out.println("First we open the connection to the meter");
System.out.println("It will respond with the first 255 bytes of meter data (Request A)");
sendRec(cmdRequestA);
System.out.println("Now we can request other data");
System.out.println("For example the remaining 255 bytes of meter data (Request B)");
sendRec(cmdRequestB);
System.out.println("Or request things like the last 6 months of total kwh and reverse kwh");
sendRec(cmd6moTotkwh);
sendRec(cmd6moRevkwh);
System.out.println("When requesting information from the meter (reads)");
System.out.println("you can send as many requests as you like once you sent");
System.out.println("the first connection request (Request A)\n\n");
System.out.println("Now let’s try sending a setting to the meter");
System.out.println("Unlike reading information from the meter");
System.out.println("You have to send the connection string and password");
System.out.println("before each setting request\n\n");
System.out.println("Now we will loop 3 times opening and closing the relays");
String sendConnA = "Send Connection Request A";
String sendPass = "Send Password";
for (int x=0; x<3; x++){
System.out.println(sendConnA);
sendRec(cmdRequestA);
System.out.println(sendPass);
sendRec(cmdSendPasswd);
System.out.println("Open Relay A");
sendRec(cmdRelayOpenA);
pause(5000);
System.out.println(sendConnA);
sendRec(cmdRequestA);
System.out.println(sendPass);
sendRec(cmdSendPasswd);
System.out.println("Close Relay A");
sendRec(cmdRelayCloseA);
pause(5000);
System.out.println(sendConnA);
sendRec(cmdRequestA);
System.out.println(sendPass);
sendRec(cmdSendPasswd);
System.out.println("Open Relay B");
sendRec(cmdRelayOpenB);
pause(5000);
System.out.println(sendConnA);
sendRec(cmdRequestA);
System.out.println(sendPass);
sendRec(cmdSendPasswd);
System.out.println("Close Relay B");
sendRec(cmdRelayCloseB);
pause(5000);
}
System.out.println("When you're finished you will close the connection");
sendRec(cmdClose);
// Close connection to serial port
serialPort.closePort();
} catch (SerialPortException ex) {
System.out.println(ex);
}
}
public static void pause(int pause) {
try {
Thread.sleep(pause);
}
catch(InterruptedException ie){
System.out.println("Thread interrupted !" + ie);
Thread.currentThread().interrupt();
}
}
public static String byteArrayToHex(String orgStr) {
byte[] a = orgStr.getBytes();
StringBuilder sb = new StringBuilder(a.length * 2);
for(byte b: a){
sb.append(String.format("%02x", b & 0xff));
sb.append(" ");
}
return sb.toString();
}
}
/*
* Requirements
* NodeJS: v16.15.0
* Serialport: 10.4.0
*/
// Load modules
const {SerialPort} = require('serialport');
// Params for Serial Port
// Change path to match your system
const sp = new SerialPort({
path: '/dev/ttyUSB0',
baudRate: 9600,
parity: 'even',
dataBits: 7,
stopBits: 1,
flowControl: 0,
bufferSize: 1,
}, false);
// This should be the meter number you are trying to connect to
const meter = '000300001184';
// The default password is '00000000' or update if needed
const password = '00000000';
const timeout = 5;
let response = '';
// EKM CRC-16 Table
const CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040,
];
// Get meter data A request and connection request
const cmdRequestA = `\x2F\x3F${meter}\x30\x30\x21\x0D\x0A`;
// Get meter data B request
const cmdRequestB = `\x2F\x3F${meter}\x30\x31\x21\x0D\x0A`;
// Get last 6 months (total kwh)
const cmd6MonthTotalKwh = '\x01\x52\x31\x02\x30\x30\x31\x31\x03';
// Get last 6 months (reverse kwh)
const cmd6MonthRevKwh = '\x01\x52\x31\x02\x30\x30\x31\x32\x03';
// Send Password String
const cmdSendPasswd = `\x01\x50\x31\x02\x28${password}\x29\x03`;
// Relay Open and Close Request Stings
const cmdRelayOpenA = '\x01\x57\x31\x02\x30\x30\x38\x31' +
'\x28\x30\x30\x30\x30\x30\x29\x03';
const cmdRelayOpenB = '\x01\x57\x31\x02\x30\x30\x38\x32' +
'\x28\x30\x30\x30\x30\x30\x29\x03';
const cmdRelayCloseA = '\x01\x57\x31\x02\x30\x30\x38\x31' +
'\x28\x31\x30\x30\x30\x30\x29\x03';
const cmdRelayCloseB = '\x01\x57\x31\x02\x30\x30\x38\x32' +
'\x28\x31\x30\x30\x30\x30\x29\x03';
// Close Connection String
const cmdClose = '\x01\x42\x30\x03\x75';
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
const ekmCalcCrc16 = (buf) => {
let crc = 0xffff;
for (let i = 0, len = buf.length; i < len; i++) {
const c = buf[i];
const index = (crc ^ c.charCodeAt(0)) & 0xff;
const crct = CRCTABLE[index];
crc = (crc >> 8) ^ crct;
}
crc = (crc << 8) | (crc >> 8);
crc &= 0x7F7F;
return crc;
};
const strToHex = (str) => {
let hex = '';
for (let i = 0; i < str.length; i++) {
const pad = `${str.charCodeAt(i).toString(16).toUpperCase()}`;
hex += ` ${(`00${pad}`).slice(-2)}`;
}
return hex;
};
const getResponse = (cmd) => new Promise((resolve, reject) => {
sp.write(cmd, (err) => {
// Clear response data
response = '';
if (err) {
console.log(err);
reject(err);
}
setTimeout(() => {
resolve(response);
}, timeout * 1000);
});
});
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter
// and check its crc to make sure the response is correct
const sendReq = async (cmd) => {
// Get the the 2nd through the last byte
// from the cmd string to create crc
let crc = ekmCalcCrc16(cmd.substr(1));
// Convert CRC from int to byte and add to the cmd request
cmd = cmd + String.fromCharCode((crc >> 8) & 0xFF) +
String.fromCharCode(crc & 0xFF);
const resRec = await getResponse(cmd);
if (resRec === '') {
console.log('Meter Instruction: ');
console.log(strToHex(resRec));
console.log('Did not trigger a resRec\n');
} else {
// The meter returns HEX 06 if OK
if (resRec.substr(0, 1) === '\x06') {
console.log('OK.\n');
}
// If the meter returns HEX 02 it's the start of a text read
if (resRec.substr(0, 1) === '\x02') {
// Get check CRC in resRec (byte 2 through the 3rd from the last)
// to make sure it's valid
crc = ekmCalcCrc16(resRec.substr(1, resRec.length - 3));
// Compare our calculated CRC with the resonse CRC
if (String.fromCharCode((crc >> 8) & 0xFF) +
String.fromCharCode(crc & 0xFF) === resRec.substr(-2)) {
// Show resRec
console.log(`${strToHex(resRec)}\n`);
} else {
console.log('Meter resRec CRC failed\n');
}
}
}
return resRec;
}
// Start meter communication
;(async () => {
sp.open(() => {
console.log('Connection Opened');
// Get meter response
sp.on('data', (read) => {
if (response.length < 255) {
response += read;
}
});
});
console.log(
'So lets give it a try...',
'\nFirst we open the connection to the meter',
'\nIt will respond with the first 255 bytes of meter data (Request A)',
);
await sendReq(cmdRequestA);
console.log('Now we can request other data');
console.log('For example the remaining 255 bytes of meter data (Request B)');
await sendReq(cmdRequestB);
console.log('We can also request things like the last 6 months of total kwh');
await sendReq(cmd6MonthTotalKwh);
console.log('We can request things like the last 6 months of reverse kwh');
await sendReq(cmd6MonthRevKwh);
console.log(
'When requesting information from the meter (reads)',
'\nyou can send as many requests as you like once you sent',
'\nthe first connection request (Request A)',
'\nNow lets try sending a setting to the meter',
'\nUnlike reading information from the meter',
'\nYou have to send the connection string and password',
'\nbefore each setting request',
);
console.log('So now we will open and close both Relay A and B');
// Open Relay A
console.log('Send Connection Request A');
await sendReq(cmdRequestA);
console.log('Send Password"');
await sendReq(cmdSendPasswd);
console.log('Open Relay A');
await sendReq(cmdRelayOpenA);
// Close Relay A
console.log('Send Connection Request A');
await sendReq(cmdRequestA);
console.log('Send Password');
await sendReq(cmdSendPasswd);
console.log('Close Relay A');
await sendReq(cmdRelayCloseA);
// Open Relay B
console.log('Send Connection Request B');
// Always do a cmdRequestA to initiate communciation with meter
await sendReq(cmdRequestA);
console.log('Send Password');
await sendReq(cmdSendPasswd);
console.log('Open Relay B');
await sendReq(cmdRelayOpenB);
// Close Relay B
console.log('Send Connection Request B');
await sendReq(cmdRequestA);
console.log('Send Password');
await sendReq(cmdSendPasswd);
console.log('Close Relay B');
await sendReq(cmdRelayCloseB);
console.log('When you\'re finished you will close the connection');
await sendReq(cmdClose);
sp.close();
})();
Communication Sequence for Reading Meter
If you would like to retrieve, or read, just the meter data then you would pass the following read request string on a v4 meter.
It is not necessary at this point to send a password request string if you are only reading the meter data.
Data A and Connection Request Format:
2F 3F ( 12 Byte Meter Number ) 30 30 21 0D 0A
Once communication with the meter has beed established it is not necessary to continue to send the Meter Data A and Connection Request string.
Example of a read request string for Data A and a meter number of 000300001184:
2F 3F 30 30 30 33 30 30 30 30 31 31 38 34 30 30 21 0D 0A
For example, you can now send the read request string for Meter Data B.
Data B Request Format:
2F 3F ( 12 Byte Meter Number ) 30 31 21 0D 0A
If you look at the request string, the 2 bytes after the meter number, is where the request for Data A or Data B is made. Requesting Data A you will use 30 30. If you want to request Data B you would use 30 31 in your request string to the meter.
At this point you can continue to send requests to the meter to get the Meter Data A or B readings, otherwise you can send the termination string to the meter to terminate the communication connection.
To end communication with the meter pass the following string:
01 42 30 03 75
If more meter readings are needed you can continue to send read requests to the meter as long as the termination string has not been sent.
Lets say that you need specific data returned. You would just pass that specific data request string to the meter.
For example, you need to get the data from your meter for the last 6 months of the Total kWh’s.
Then you would send this request string to the meter:
Total kWh Request String:
01 52 31 02 30 30 31 31 03 ?? ??
Or if you need the meter data for the last 6 months of Reverse kWh:
Reverse kWh Request String:
01 52 31 02 30 30 31 32 03 ?? ??
The two ( 2 ) sets of “Question Marks” ( ?? ?? ) equal the 2 bytes that the CRC-16 Checksum will be.
There is no limit to how many read requests you can send to the meter, as long as you start with the Meter Data A and Connection string first. Once that initial connection has been made with the meter you don’t have to repeat that connection string. You can continue to call the same read request, a different one, or a combination of meter requests.
Remember, once you have finished requesting all the meter reads you want, the close string must be sent. This tells the meter that you are finished and not to keep expecting more data requests to be sent. This also closes the communication connection from your computer to the meter.
Close String
01 42 30 03 75
Communication Sequence for Setting Requests
To send Setting, or write, Requests to the meter, you begin with the same procedure as you would for the reading meter data requests.
You start the communication with the meter by passing the Meter Data A and Connection Request string: 2F 3F ( 12 Byte Meter Number ) 30 30 21 0D 0A.
Once you have made the connection to the meter you will now need to pass the password string along with the Meter Data A and Connection string. The Password string must be included if you want to set, or write, instructions to the meter.
Password String Format:
01 50 31 02 28 ( password ) 29 03 ?? ??
The two ( 2 ) sets of “Question Marks” ( ?? ?? ) equal the 2 bytes that the CRC-16 Checksum will be.
Once the meter has verified that the password is correct it will return a 06, or an O.K., indicating that the password has been accepted.
06 | Indicates an OK or that the meter has accepted and verified the password. |
Now that the password string has been sent and verified, the meter is ready to accept setting, or write, instructions.
Sending setting instruction to the meter can be anything from setting a new meter number, a new password or setting the time.
For our examples we will show setting strings on how to open and close relays.
To open or close a relay is similar to the way you would send a request string to retrieve Data A from the meter.
Example below is of a request to open Relay A:
01 57 31 02 30 30 38 31 28 31 30 30 30 30 29 03 ?? ??
Lets say you need to close the relay instead of opening it. Then you would pass the following request string to the meter.
Request string to close Relay A:
01 57 31 02 30 30 38 31 28 30 30 30 30 30 29 03 ?? ??
The two ( 2 ) sets of “Question Marks” ( ?? ?? ) equal the 2 bytes that the CRC-16 Checksum will be.
Now that you have sent the setting request to the meter, the meter will send back a 06 indicating that the setting request was successful.
From here there are two ( 2 ) options you can choose from. You can either end the communication with the meter, with the Close String, if this is the only request that was needed to be sent to the meter, or you can continue with another setting request.
If another setting request is needed to be sent to the meter then the Meter Data A and Connection Request string will need to be sent to the meter. Starting the sequence over again.
Complete Setting Request sequence to the meter:
2F 3F ( 12 Byte Meter Number ) 30 30 21 0D 0A | Data A and Connection String Request. This is always sent to the meter first to establish the connection. |
255 Byte return from the meter. | This will be the Meter Data A return |
01 50 31 02 28 ( password ) 29 03 ?? ?? | Password String |
06 | This is what the meter will return once your password is verified. |
01 57 31 02 30 30 38 31 28 31 30 30 30 30 29 03 ?? ?? | This is the write command string that is sent to the meter. Example here is the request to open Relay A. |
06 | This is what the meter will return indicating that the setting was successful. |
2F 3F ( 12 Byte Meter Number ) 30 30 21 0D 0A | Data A and Connection String Request. This is always sent to the meter first to establish the connection. |
255 Byte return from the meter. | This will be the Meter Data A return |
01 50 31 02 28 ( password ) 29 03 ?? ?? | Password String |
06 | This is what the meter will return once your password is verified. |
01 57 31 02 30 30 38 32 28 30 30 30 30 30 29 03 ?? ?? | This is the write command string that is sent to the meter. Example here is the request to close Relay B. |
06 | This is what the meter will return indicating that the setting was successful. |
01 42 30 03 75 | Close String to terminate the connection with the meter. |
Parsing Responses
Parsing Responses from meter
# Ruby Version: 3.3.0
# Serialport version: 1.3.2
# Requires the SerialPort gem
# (http://rubygems.org/gems/serialport)
# gem install serialport
require 'serialport'
require 'json'
require 'pp'
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
].freeze
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF")
def ekm_calc_crc16(buf) # rubocop:disable Metrics/MethodLength
crc = 0xffff
i = 0
while i < buf.length
c = buf[i].chr
index = (crc ^ c.ord) & 0xff
crct = CRCTABLE[index]
crc = (crc >> 8) ^ crct
i += 1
end
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
crc
end
# In this code example we make a simple function that will take the command
# you want to send to the meter and make the crc for it before sending.
# After sending we get the response from the meter and check its crc to make sure
# the response is correct
def send_rec(send) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# get the the 2nd through the last byte
# from the send string to create crc
crc = ekm_calc_crc16(send[1..])
# Convert CRC from int to byte and add to the send request
send = send + ((crc >> 8) & 0xFF).chr + (crc & 0xFF).chr
# Send request
SP.write(send)
# Set Timeout
timeout = 5
# Get meter response
now = Time.now
read = ''
response = ''
count = 0
while count < 255
read = SP.getc
if read
now = Time.now
count += 1
response.concat(read)
end
break if Time.now > now + timeout
end
if response == ''
print("Meter Instruction: \n")
print(send.unpack('C*').map { |e| format('%02X', e) }.join(' '))
print("\nDid not trigger a response\n\n")
return
end
# The meter returns HEX 06 if ok
print("OK.\n\n") if response[0] == "\x06"
# If the meter returns HEX 02 it's the start of a text read
if response[0] == "\x02" # rubocop:disable Style/GuardClause
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
crc = ekm_calc_crc16(response[1..-3])
# Compare our calculated CRC with the resonse CRC
if ((crc >> 8) & 0xFF).chr + (crc & 0xFF).chr == response[-2..]
# Show response
response
else
print("Meter response CRC failed\n\n")
end
end
end
# This function takes the response from the meter
# and parses its output into an array using the defined table
def parse_response(response, table, kwh_scale) # rubocop:disable Metrics/MethodLength
table_array = JSON.parse(table)
response_array = {}
pointer = 0
table_array.each do |obj|
key = obj[0]
val = obj[1][0]
val_format = obj[1][1]
next_end = val + pointer
case val_format
when "byte"
hex_values = ''
(pointer...next_end).each { |i| hex_values += response[i].ord.to_s(16) }
response_array[key] = hex_values
when "integer"
response_array[key] = response[pointer...next_end].to_i
when "float"
response_array[key] = response[pointer...next_end].to_f
when "string"
response_array[key] = response[pointer...next_end]
end
pointer += val
end
response_array = scale_type(response_array, table_array, kwh_scale)
response_array
end
def scale_type(response_array, table_array, kwh_scale)
# Scaling values
# If kwh_scale is an empty string, set it to response_array["kWh_Scale"]
kwh_scale = response_array["kWh_Scale"].to_i if kwh_scale == ""
table_array.each do |obj|
key = obj[0]
scale_t = obj[1][2]
case scale_t
when "kWh_Scale"
response_array[key] /= 10**kwh_scale
when "/10"
response_array[key] /= 10
when "/100"
response_array[key] /= 100
end
end
response_array
end
# Params for serial port
# Change /dev/ttyUSB0 to match your system
# For Windows, use 'COM*' and replace '*' with the corresponding number.
port_str = '/dev/ttyUSB0'
baud_rate = 9600
data_bits = 7
stop_bits = 1
parity = SerialPort::EVEN
flow = SerialPort::NONE
# Open connection to serial port
SP = SerialPort.new(port_str, baud_rate, data_bits, stop_bits, parity)
SP.flow_control = flow
# Meter Number and Password
meter = '000300001184'
# In this example we have assigned
# some meter request strings to variables
# Get meter data A request and connection request
cmd_request_a = "\x2F\x3F#{meter}\x30\x30\x21\x0D\x0A"
# Get meter data B request
cmd_request_b = "\x2F\x3F#{meter}\x30\x31\x21\x0D\x0A"
# Close Connection String
cmd_close = "\x01\x42\x30\x03\x75"
# Now we set up parsing tables for the data A and B requests
tbl_request_a = '{
"reserved_start_byte": [1, "byte", "none"],
"Model": [2, "byte", "none"],
"Firmware": [1, "byte", "none"],
"Meter_Address": [12, "string", "none"],
"kWh_Tot": [8, "float", "kWh_Scale"],
"Reactive_Energy_Tot": [8, "float", "kWh_Scale"],
"Rev_kWh_Tot": [8, "float", "kWh_Scale"],
"kWh_Ln_1": [8, "float", "kWh_Scale"],
"kWh_Ln_2": [8, "float", "kWh_Scale"],
"kWh_Ln_3": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_1": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_2": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_3": [8, "float", "kWh_Scale"],
"Resettable_kWh_Tot": [8, "float", "kWh_Scale"],
"Resettable_Rev_kWh_Tot": [8, "float", "kWh_Scale"],
"RMS_Volts_Ln_1": [4, "float", "/10"],
"RMS_Volts_Ln_2": [4, "float", "/10"],
"RMS_Volts_Ln_3": [4, "float", "/10"],
"Amps_Ln_1": [5, "float", "/10"],
"Amps_Ln_2": [5, "float", "/10"],
"Amps_Ln_3": [5, "float", "/10"],
"RMS_Watts_Ln_1": [7, "integer", "none"],
"RMS_Watts_Ln_2": [7, "integer", "none"],
"RMS_Watts_Ln_3": [7, "integer", "none"],
"RMS_Watts_Tot": [7, "integer", "none"],
"Cos_Theta_Ln_1": [4, "string", "none"],
"Cos_Theta_Ln_2": [4, "string", "none"],
"Cos_Theta_Ln_3": [4, "string", "none"],
"Reactive_Pwr_Ln_1": [7, "integer", "none"],
"Reactive_Pwr_Ln_2": [7, "integer", "none"],
"Reactive_Pwr_Ln_3": [7, "integer", "none"],
"Reactive_Pwr_Tot": [7, "integer", "none"],
"Line_Freq": [4, "float", "/100"],
"Pulse_Cnt_1": [8, "integer", "none"],
"Pulse_Cnt_2": [8, "integer", "none"],
"Pulse_Cnt_3": [8, "integer", "none"],
"State_Inputs": [1, "integer", "none"],
"State_Watts_Dir": [1, "integer", "none"],
"State_Out": [1, "integer", "none"],
"kWh_Scale": [1, "integer", "none"],
"reserved_end_data": [2, "byte", "none"],
"Meter_Time": [14, "integer", "none"],
"reserved_type": [2, "byte", "none"],
"reserved_end": [4, "byte", "none"],
"crc16": [2, "byte", "none"]}'
tbl_request_b = '{
"reserved_start_byte": [1, "byte", "none"],
"Model": [2, "byte", "none"],
"Firmware": [1, "byte", "none"],
"Meter_Address": [12, "string", "none"],
"kWh_Tariff_1": [8, "float", "kWh_Scale"],
"kWh_Tariff_2": [8, "float", "kWh_Scale"],
"kWh_Tariff_3": [8, "float", "kWh_Scale"],
"kWh_Tariff_4": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_1": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_2": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_3": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_4": [8, "float", "kWh_Scale"],
"RMS_Volts_Ln_1": [4, "float", "/10"],
"RMS_Volts_Ln_2": [4, "float", "/10"],
"RMS_Volts_Ln_3": [4, "float", "/10"],
"Amps_Ln_1": [5, "float", "/10"],
"Amps_Ln_2": [5, "float", "/10"],
"Amps_Ln_3": [5, "float", "/10"],
"RMS_Watts_Ln_1": [7, "integer", "none"],
"RMS_Watts_Ln_2": [7, "integer", "none"],
"RMS_Watts_Ln_3": [7, "integer", "none"],
"RMS_Watts_Tot": [7, "integer", "none"],
"Cos_Theta_Adj_Ln_1": [4, "string", "none"],
"Cos_Theta_Adj_Ln_2": [4, "string", "none"],
"Cos_Theta_Adj_Ln_3": [4, "string", "none"],
"RMS_Watts_Max_Demand": [8, "float", "/10"],
"Max_Demand_Period": [1, "integer", "none"],
"Pulse_Ratio_1": [4, "integer", "none"],
"Pulse_Ratio_2": [4, "integer", "none"],
"Pulse_Ratio_3": [4, "integer", "none"],
"CT_Ratio": [4, "integer", "none"],
"reserved_b2": [1, "byte", "none"],
"Pulse_Output_Ratio": [4, "integer", "none"],
"reserved_b3": [56, "byte", "none"],
"Meter_Time": [14, "integer", "none"],
"reserved_type": [2, "byte", "none"],
"reserved_end": [4, "byte", "none"],
"crc16": [2, "byte", "none"]}'
print("So let's give it a try...\n")
print("First we open the connection to the meter\n")
print("It will respond with the first 255 bytes of meter data (Request A)\n")
request_a = parse_response(send_rec(cmd_request_a), tbl_request_a, "")
pp request_a
print("Now we can request other data\n")
print("For example the remaining 255 bytes of meter data (Request B)\n")
request_b = parse_response(send_rec(cmd_request_b), tbl_request_b, request_a["kWh_Scale"])
pp request_b
print("When you're finished you will close the connection\n")
send_rec(cmd_close)
# Close connection to serial port
SP.close
'''
Python version: 3.10.11
Modules:
Pyserial version: 3.4
Requires pyserial module:
pip3 install pyserial
'''
import pprint
import sys
import json
import serial
pp = pprint.PrettyPrinter(sort_dicts=False)
# EKM CRC-16 Table
CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
]
def ekm_calc_crc16(buf):
'''
This function makes use of the CRC table to calulate the CRC
example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
'''
crc = 0xffff
for c_value in buf:
index = (crc ^ ord( c_value )) & 0xff
crct = CRCTABLE[index]
crc=(crc>>8)^crct
crc = (crc << 8) | (crc >> 8)
crc &= 0x7F7F
return crc
def send_rec(send):
'''
In this code example we make a simple function that will take the command
you want to send to the meter and make the crc for it before sending.
After sending we get the response from the meter and check its crc to make sure
the response is correct
'''
# get the the 2nd through the last byte
# from the send string to create crc
crc = ekm_calc_crc16(send[1:])
# Convert CRC from int to byte and add to the send request
send=send+chr((crc >> 8) & 0xFF) + chr(crc & 0xFF)
# Send request
sp.write(send.encode())
# Get meter response
response = sp.read(255)
response=str(response,'ascii')
if response=="":
print("Meter Instruction: ")
print(" ".join("{0:02x}".format(ord( c )) for c in send))
print("Did not trigger a response\n\n")
return
# The meter returns HEX 06 if ok
if response[0]=="\x06":
print("OK.\n\n")
# If the meter returns HEX 02 it's the start of a text read
if response[0]=="\x02":
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
crc = ekm_calc_crc16(response[1:-2])
# Compare our calculated CRC with the resonse CRC
if chr((crc >> 8) & 0xFF) + chr(crc & 0xFF)==response[-2:]:
# Return response
return response
else:
print("Meter response CRC failed\n\n")
def parse_response(response,table, kwh_scale=""):
'''
This function takes the response from the meter
and parses its output into an array using the defined table
'''
table_array= json.loads(table, object_pairs_hook=dict)
response_array={}
pointer = 0
for key in table_array:
val = table_array[key]
end = val[0]+pointer
if val[1] == "byte":
hex_values = ""
for char in response[pointer:end]:
hex_values += hex(ord(char))[2:]
response_array[key] = hex_values
elif val[1] == "integer":
response_array[key] = int(response[pointer:end])
elif val[1] == "float":
response_array[key] = float(response[pointer:end])
elif val[1] == "string":
response_array[key] = str(response[pointer:end])
pointer+=val[0]
response_array = scale_type(response_array, table_array, kwh_scale)
return response_array
def scale_type(response_array, table_array, kwh_scale):
'''
Scaling values
'''
if kwh_scale == "":
kwh_scale = response_array["kWh_Scale"]
for key in table_array:
scale_t = table_array[key][2]
if scale_t == "kWh_Scale":
response_array[key] = response_array[key] / (10 ** kwh_scale)
elif scale_t == "/10":
response_array[key] = response_array[key] / 10
elif scale_t == "/100":
response_array[key] = response_array[key] / 100
return response_array
# Open connection to serial port
# Change /dev/ttyUSB0 to match your system
# For Windows, use 'COM*' and replace '*' with the corresponding number.
try:
sp = serial.Serial(
port='/dev/ttyUSB0',
baudrate=9600,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.SEVENBITS,
xonxoff=0,
timeout=5
)
print("Serial port successfully opened.")
except serial.SerialException as e:
print(f"Error: {e}")
sys.exit()
# Meter Number and Password - Ex.: 000300001184
METER="000300001184"
# In this example we have assigned
# some meter request strings to variables
# Get meter data A request and connection request
CMD_REQUEST_A="\x2F\x3F"+METER+"\x30\x30\x21\x0D\x0A"
# Get meter data B request
CMD_REQUEST_B="\x2F\x3F"+METER+"\x30\x31\x21\x0D\x0A"
# Close Connection String
CMD_CLOSE="\x01\x42\x30\x03\x75"
# Now we set up parsing tables for the data A and B requests
TBL_REQUEST_A='\
{ "reserved_start_byte": [1, "byte", "none"], \
"Model": [2, "byte", "none"], \
"Firmware": [1, "byte", "none"], \
"Meter_Address": [12, "string", "none"], \
"kWh_Tot": [8, "float", "kWh_Scale"], \
"Reactive_Energy_Tot": [8, "float", "kWh_Scale"], \
"Rev_kWh_Tot": [8, "float", "kWh_Scale"], \
"kWh_Ln_1": [8, "float", "kWh_Scale"], \
"kWh_Ln_2": [8, "float", "kWh_Scale"], \
"kWh_Ln_3": [8, "float", "kWh_Scale"], \
"Rev_kWh_Ln_1": [8, "float", "kWh_Scale"], \
"Rev_kWh_Ln_2": [8, "float", "kWh_Scale"], \
"Rev_kWh_Ln_3": [8, "float", "kWh_Scale"], \
"Resettable_kWh_Tot": [8, "float", "kWh_Scale"], \
"Resettable_Rev_kWh_Tot": [8, "float", "kWh_Scale"], \
"RMS_Volts_Ln_1": [4, "float", "/10"], \
"RMS_Volts_Ln_2": [4, "float", "/10"], \
"RMS_Volts_Ln_3": [4, "float", "/10"], \
"Amps_Ln_1": [5, "float", "/10"], \
"Amps_Ln_2": [5, "float", "/10"], \
"Amps_Ln_3": [5, "float", "/10"], \
"RMS_Watts_Ln_1": [7, "integer", "none"], \
"RMS_Watts_Ln_2": [7, "integer", "none"], \
"RMS_Watts_Ln_3": [7, "integer", "none"], \
"RMS_Watts_Tot": [7, "integer", "none"], \
"Cos_Theta_Ln_1": [4, "string", "none"], \
"Cos_Theta_Ln_2": [4, "string", "none"], \
"Cos_Theta_Ln_3": [4, "string", "none"], \
"Reactive_Pwr_Ln_1": [7, "integer", "none"], \
"Reactive_Pwr_Ln_2": [7, "integer", "none"], \
"Reactive_Pwr_Ln_3": [7, "integer", "none"], \
"Reactive_Pwr_Tot": [7, "integer", "none"], \
"Line_Freq": [4, "float", "/100"], \
"Pulse_Cnt_1": [8, "integer", "none"], \
"Pulse_Cnt_2": [8, "integer", "none"], \
"Pulse_Cnt_3": [8, "integer", "none"], \
"State_Inputs": [1, "integer", "none"], \
"State_Watts_Dir": [1, "integer", "none"], \
"State_Out": [1, "integer", "none"], \
"kWh_Scale": [1, "integer", "none"], \
"reserved_end_data": [2, "byte", "none"], \
"Meter_Time": [14, "integer", "none"], \
"reserved_type": [2, "byte", "none"], \
"reserved_end": [4, "byte", "none"], \
"crc16": [2, "byte", "none"]}'
TBL_REQUEST_B='\
{"reserved_start_byte": [1, "byte", "none"], \
"Model": [2, "byte", "none"], \
"Firmware": [1, "byte", "none"], \
"Meter_Address": [12, "string", "none"], \
"kWh_Tariff_1": [8, "float", "kWh_Scale"], \
"kWh_Tariff_2": [8, "float", "kWh_Scale"], \
"kWh_Tariff_3": [8, "float", "kWh_Scale"], \
"kWh_Tariff_4": [8, "float", "kWh_Scale"], \
"Rev_kWh_Tariff_1": [8, "float", "kWh_Scale"], \
"Rev_kWh_Tariff_2": [8, "float", "kWh_Scale"], \
"Rev_kWh_Tariff_3": [8, "float", "kWh_Scale"], \
"Rev_kWh_Tariff_4": [8, "float", "kWh_Scale"], \
"RMS_Volts_Ln_1": [4, "float", "/10"], \
"RMS_Volts_Ln_2": [4, "float", "/10"], \
"RMS_Volts_Ln_3": [4, "float", "/10"], \
"Amps_Ln_1": [5, "float", "/10"], \
"Amps_Ln_2": [5, "float", "/10"], \
"Amps_Ln_3": [5, "float", "/10"], \
"RMS_Watts_Ln_1": [7, "integer", "none"], \
"RMS_Watts_Ln_2": [7, "integer", "none"], \
"RMS_Watts_Ln_3": [7, "integer", "none"], \
"RMS_Watts_Tot": [7, "integer", "none"], \
"Cos_Theta_Adj_Ln_1": [4, "string", "none"], \
"Cos_Theta_Adj_Ln_2": [4, "string", "none"], \
"Cos_Theta_Adj_Ln_3": [4, "string", "none"], \
"RMS_Watts_Max_Demand": [8, "float", "/10"], \
"Max_Demand_Period": [1, "integer", "none"], \
"Pulse_Ratio_1": [4, "integer", "none"], \
"Pulse_Ratio_2": [4, "integer", "none"], \
"Pulse_Ratio_3": [4, "integer", "none"], \
"CT_Ratio": [4, "integer", "none"], \
"reserved_b2": [1, "byte", "none"], \
"Pulse_Output_Ratio": [4, "integer", "none"], \
"reserved_b3": [56, "byte", "none"], \
"Meter_Time": [14, "integer", "none"], \
"reserved_type": [2, "byte", "none"], \
"reserved_end": [4, "byte", "none"], \
"crc16": [2, "byte", "none"]}'
print("So let's give it a try...")
print("First we open the connection to the meter")
print("It will respond with the first 255 bytes of meter data (Request A)")
send_a = send_rec(CMD_REQUEST_A)
if send_a is not None:
REQUEST_A=parse_response(send_a, TBL_REQUEST_A, "")
pp.pprint(REQUEST_A)
else:
print ("No response data A")
print("Now we can request other data")
print("For example the remaining 255 bytes of meter data (Request B)")
send_b = send_rec(CMD_REQUEST_B)
if send_b is not None:
REQUEST_B=parse_response(send_rec(CMD_REQUEST_B),TBL_REQUEST_B, REQUEST_A["kWh_Scale"])
pp.pprint(REQUEST_B)
else:
print ("No response data B")
print("When you're finished you will close the connection")
send_rec(CMD_CLOSE)
# Close connection to serial port
sp.close()
<?php
/*
* Requirements:
* PHP: 8.2.19
* DIO: 0.2.1
* Install php8.2-dev on ubuntu to compile DIO module.
* apt install php8.2-dev
* Compile DIO module from source:
* Download the latest tarball from: https://pecl.php.net/package/dio
* Uncompress: tar xfz dio-0.2.1.tgz
* cd dio-0.2.1/
* phpize
* ./configure
* make
* sudo make install
* You will see a path like: /usr/lib/php/20210902/
* Edit your php.ini
* vi /etc/php/8.2/cli/php.ini
* In the extension section add:
* extension=/usr/lib/php/20210902/dio.so
* Now you can run your php script.
* Ex: php connect.php
*/
// EKM CRC-16 Table
$CRCTABLE = array(
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
);
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
// For Windows, use 'COM*' and replace '*' with the corresponding number.
$sp = dio_open('/dev/ttyUSB0', O_RDWR | O_NONBLOCK);
// Configure the port
dio_tcsetattr($sp, array(
'baud' => 9600,
'bits' => 7,
'stop' => 1,
'parity' => 2,
'flow_control' => 0,
'is_canonical' => 0
));
// Meter Number and Password
$meter = "000300001184";
// In this example we have assigned
// some meter request strings to variables
// Get meter data A request and connection request
$cmd_request_a = "\x2F\x3F" . $meter . "\x30\x30\x21\x0D\x0A";
// Get meter data B request
$cmd_request_b = "\x2F\x3F" . $meter . "\x30\x31\x21\x0D\x0A";
// Close Connection String
$cmd_close = "\x01\x42\x30\x03\x75";
// Now we set up parsing tables for the data A and B requests
$tbl_request_a = '{
"reserved_start_byte": [1, "byte", "none"],
"Model": [2, "byte", "none"],
"Firmware": [1, "byte", "none"],
"Meter_Address": [12, "string", "none"],
"kWh_Tot": [8, "float", "kWh_Scale"],
"Reactive_Energy_Tot": [8, "float", "kWh_Scale"],
"Rev_kWh_Tot": [8, "float", "kWh_Scale"],
"kWh_Ln_1": [8, "float", "kWh_Scale"],
"kWh_Ln_2": [8, "float", "kWh_Scale"],
"kWh_Ln_3": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_1": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_2": [8, "float", "kWh_Scale"],
"Rev_kWh_Ln_3": [8, "float", "kWh_Scale"],
"Resettable_kWh_Tot": [8, "float", "kWh_Scale"],
"Resettable_Rev_kWh_Tot": [8, "float", "kWh_Scale"],
"RMS_Volts_Ln_1": [4, "float", "/10"],
"RMS_Volts_Ln_2": [4, "float", "/10"],
"RMS_Volts_Ln_3": [4, "float", "/10"],
"Amps_Ln_1": [5, "float", "/10"],
"Amps_Ln_2": [5, "float", "/10"],
"Amps_Ln_3": [5, "float", "/10"],
"RMS_Watts_Ln_1": [7, "integer", "none"],
"RMS_Watts_Ln_2": [7, "integer", "none"],
"RMS_Watts_Ln_3": [7, "integer", "none"],
"RMS_Watts_Tot": [7, "integer", "none"],
"Cos_Theta_Ln_1": [4, "string", "none"],
"Cos_Theta_Ln_2": [4, "string", "none"],
"Cos_Theta_Ln_3": [4, "string", "none"],
"Reactive_Pwr_Ln_1": [7, "integer", "none"],
"Reactive_Pwr_Ln_2": [7, "integer", "none"],
"Reactive_Pwr_Ln_3": [7, "integer", "none"],
"Reactive_Pwr_Tot": [7, "integer", "none"],
"Line_Freq": [4, "float", "/100"],
"Pulse_Cnt_1": [8, "integer", "none"],
"Pulse_Cnt_2": [8, "integer", "none"],
"Pulse_Cnt_3": [8, "integer", "none"],
"State_Inputs": [1, "integer", "none"],
"State_Watts_Dir": [1, "integer", "none"],
"State_Out": [1, "integer", "none"],
"kWh_Scale": [1, "integer", "none"],
"reserved_end_data": [2, "byte", "none"],
"Meter_Time": [14, "integer", "none"],
"reserved_type": [2, "byte", "none"],
"reserved_end": [4, "byte", "none"],
"crc16": [2, "byte", "none"]
}';
$tbl_request_b = '{
"reserved_start_byte": [1, "byte", "none"],
"Model": [2, "byte", "none"],
"Firmware": [1, "byte", "none"],
"Meter_Address": [12, "string", "none"],
"kWh_Tariff_1": [8, "float", "kWh_Scale"],
"kWh_Tariff_2": [8, "float", "kWh_Scale"],
"kWh_Tariff_3": [8, "float", "kWh_Scale"],
"kWh_Tariff_4": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_1": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_2": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_3": [8, "float", "kWh_Scale"],
"Rev_kWh_Tariff_4": [8, "float", "kWh_Scale"],
"RMS_Volts_Ln_1": [4, "float", "/10"],
"RMS_Volts_Ln_2": [4, "float", "/10"],
"RMS_Volts_Ln_3": [4, "float", "/10"],
"Amps_Ln_1": [5, "float", "/10"],
"Amps_Ln_2": [5, "float", "/10"],
"Amps_Ln_3": [5, "float", "/10"],
"RMS_Watts_Ln_1": [7, "integer", "none"],
"RMS_Watts_Ln_2": [7, "integer", "none"],
"RMS_Watts_Ln_3": [7, "integer", "none"],
"RMS_Watts_Tot": [7, "integer", "none"],
"Cos_Theta_Adj_Ln_1": [4, "string", "none"],
"Cos_Theta_Adj_Ln_2": [4, "string", "none"],
"Cos_Theta_Adj_Ln_3": [4, "string", "none"],
"RMS_Watts_Max_Demand": [8, "float", "/10"],
"Max_Demand_Period": [1, "integer", "none"],
"Pulse_Ratio_1": [4, "integer", "none"],
"Pulse_Ratio_2": [4, "integer", "none"],
"Pulse_Ratio_3": [4, "integer", "none"],
"CT_Ratio": [4, "integer", "none"],
"reserved_b2": [1, "byte", "none"],
"Pulse_Output_Ratio": [4, "integer", "none"],
"reserved_b3": [56, "byte", "none"],
"Meter_Time": [14, "integer", "none"],
"reserved_type": [2, "byte", "none"],
"reserved_end": [4, "byte", "none"],
"crc16": [2, "byte", "none"]}';
echo "So let's give it a try...\n";
echo "First we open the connection to the meter\n";
echo "It will respond with the first 255 bytes of meter data (Request A)\n";
$request_a = parse_response(send_rec($cmd_request_a), $tbl_request_a, "");
var_dump($request_a);
echo "Now we can request other data\n";
echo "For example the remaining 255 bytes of meter data (Request B)\n";
$request_b = parse_response(send_rec($cmd_request_b), $tbl_request_b, $request_a["kWh_Scale"]);
var_dump($request_b);
echo "When you're finished you will close the connection\n";
send_rec($cmd_close);
// Close connection to serial port
dio_close($sp);
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter and check its crc to make sure
// the response is correct
function send_rec($send)
{
global $sp;
// get the the 2nd through the last byte
// from the send string to create crc
$crc = ekm_calc_crc16(substr($send, 1));
// Convert CRC from int to byte and add to the send request
$send = $send . chr(($crc >> 8) & 0xFF) . chr($crc & 0xFF);
// Send request
dio_write($sp, $send);
// Set Timeout
$timeout = 5;
// Get meter response
$read = '';
$response = '';
$now = time();
while (strlen($response) < 255 && time() < $now + $timeout) {
$read = dio_read($sp, 1);
$response .= $read;
}
if ($response == "") {
echo "Meter Instruction: \n";
echo strToHex($send) . "\n";
echo "Did not trigger a response\n\n";
return;
}
// The meter returns HEX 06 if ok
if (substr($response, 0, 1) == "\x06")
echo "OK.\n\n";
// If the meter returns HEX 02 it's the start of a text read
if (substr($response, 0, 1) == "\x02") {
// Get check CRC in response (byte 2 through the 3rd from the last)
// to make sure it's valid
$crc = ekm_calc_crc16(substr($response, 1, -2));
// Compare our calculated CRC with the resonse CRC
if (chr(($crc >> 8) & 0xFF) . chr($crc & 0xFF) == substr($response, -2)) {
return $response;
} else {
echo "Meter response CRC failed\n\n";
}
}
}
// This function takes the response from the meter
// and parses its output into an array using the defined table
function parse_response($response, $table, $kwh_scale = "")
{
/*
This function takes the response from the meter
and parses its output into an array using the defined table
*/
$table_array = json_decode($table, true);
$response_array = [];
$pointer = 0;
foreach ($table_array as $key => $val) {
$end = $val[0] + $pointer;
if ($val[1] == "byte") {
$hex_values = "";
for ($i = $pointer; $i < $end; $i++) {
$hex_values .= dechex(ord($response[$i]));
}
$response_array[$key] = $hex_values;
} elseif ($val[1] == "integer") {
$response_array[$key] = intval(substr($response, $pointer, $val[0]));
} elseif ($val[1] == "float") {
$response_array[$key] = floatval(substr($response, $pointer, $val[0]));
} elseif ($val[1] == "string") {
$response_array[$key] = substr($response, $pointer, $val[0]);
}
$pointer += $val[0];
}
$response_array = scale_type($response_array, $table_array, $kwh_scale);
return $response_array;
}
function scale_type(&$response_array, $table_array, $kwh_scale)
{
// Scaling values
// If kwh_scale is an empty string, set it to $response_array["kWh_Scale"]
if ($kwh_scale == "") {
$kwh_scale = $response_array["kWh_Scale"];
}
foreach ($table_array as $key => $value) {
$scale_t = $value[2];
switch ($scale_t) {
case "kWh_Scale":
$response_array[$key] /= pow(10, $kwh_scale);
break;
case "/10":
$response_array[$key] /= 10;
break;
case "/100":
$response_array[$key] /= 100;
break;
}
}
return ($response_array);
}
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
function ekm_calc_crc16($buf)
{
$crc = 0xffff;
global $CRCTABLE;
$len = strlen($buf);
for ($i = 0; $i < $len; $i++) {
$c = $buf[$i];
$index = ($crc ^ ord($c)) & 0xff;
$crct = $CRCTABLE[$index];
$crc = ($crc >> 8) ^ $crct;
}
$crc = ($crc << 8) | ($crc >> 8);
$crc &= 0x7F7F;
return $crc;
}
function strToHex($buf)
{
$hex = '';
for ($i = 0; $i < strlen($buf); $i++) {
$hex .= sprintf("%02X", ord($buf[$i])) . ' ';
}
return $hex;
}
#!/usr/bin/perl
# Perl version: v5.30.0
# CPAN.pm version v2.34
#
# Requires the Device::SerialPort module
# Device::SerialPort: 1.04
# Readonly: 2.05
#
# Install Perl Modules
# cpan Device::SerialPort
# cpan Readonly
# cpan JSON::XS;
use Carp;
use Readonly;
use Device::SerialPort;
use Data::Dumper;
use JSON::XS;
use version; our $VERSION = qv(1.0);
# EKM CRC-16 Table
## no critic (ValuesAndExpressions::RequireNumberSeparators)
Readonly my $CRCTABLE => [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040
];
## critic
# This function makes use of the CRC table to calulate the CRC
# example: ekm_calc_crc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
sub ekm_calc_crc16
{ ## no critic (Subroutines::RequireFinalReturn Subroutines::RequireArgUnpacking)
Readonly my $C_CRC => 0xffff;
Readonly my $CONST_FF => 0xff;
Readonly my $NUMBER_EIGHT => 8;
Readonly my $CONST_7F7F => 0x7F7F;
my $buf = shift @_;
my $crc = $C_CRC;
my $s;
my @arr = split //ms, $buf;
for ( my $i = 0 ; $i < length $buf ; $i++ ) {
my $chr = ord $arr[$i];
$s = ( $crc ^ $chr ) & $CONST_FF;
my $crct = $CRCTABLE->[$s];
$crc = ( $crc >> $NUMBER_EIGHT ) ^ $crct;
}
$crc = ( $crc << $NUMBER_EIGHT ) | ( $crc >> $NUMBER_EIGHT );
$crc &= $CONST_7F7F;
return $crc;
}
# In this code example we make a simple function that will take the command
# you want to send to the meter and make the crc for it before sending.
# After sending we get the response from the meter and check its crc to make sure
# the response is correct
sub send_rec
{ ## no critic (Subroutines::RequireFinalReturn Subroutines::RequireArgUnpacking)
Readonly my $CONST_FF => 0xFF;
Readonly my $CONST_BYTE => 255;
Readonly my $NUMBER_EIGHT => 8;
Readonly my $NUMBER_MIN_TWO => -2;
#Set Timeout
Readonly my $TIMEOUT => 5;
my $timeout = $TIMEOUT;
my $send = shift @_;
# get the the 2nd through the last byte
# from the send string to create crc
my $crc = ekm_calc_crc16( substr $send, 1 );
# Convert CRC from int to byte and add to the send request
$send =
$send
. chr( ( $crc >> $NUMBER_EIGHT ) & $CONST_FF )
. chr( $crc & $CONST_FF ); ## no critic (CodeLayout::ProhibitParensWithBuiltins)
# Send request
$sp->write($send);
# Get meter response
my $count = 0;
my $response = q{};
while ( $timeout > 0 && $count < $CONST_BYTE ) {
my ( $chars, $read ) = $sp->read($CONST_BYTE);
if ( $chars > 0 ) {
$count += $chars;
$response .= $read;
}
else {
$timeout--;
}
}
## no critic (InputOutput::RequireCheckedSyscalls)
if ( length($response) == 0 ) {
print "Meter Instruction\n";
# Show response as HEX
$send =~ s/(.)/sprintf("%02X",ord($1))." "/seg; ## no critic (RegularExpressions::RequireLineBoundaryMatching RegularExpressions::RequireExtendedFormatting)
print $send;
print "\nDid not trigger a response\n\n";
return;
}
# The meter returns HEX 06 if ok
if ( substr( $response, 0, 1 ) eq "\x06" )
{ ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
print "OK.\n\n";
}
# If the meter returns HEX 02 it's the start of a text read
if ( substr( $response, 0, 1 ) eq "\x02"
&& length($response) == $CONST_BYTE )
{ ## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Get check CRC in response (byte 2 through the 3rd from the last)
# to make sure it's valid
$crc = ekm_calc_crc16(substr( $response, 1, $NUMBER_MIN_TWO )); ## no critic (CodeLayout::ProhibitParensWithBuiltins)
# Compare our calculated CRC with the resonse CRC
if (
(
chr( ( $crc >> $NUMBER_EIGHT ) & $CONST_FF )
. chr( $crc & $CONST_FF ) ## no critic (CodeLayout::ProhibitParensWithBuiltins)
) == substr $response,
$NUMBER_MIN_TWO
)
{
# Return response
return ($response);
}
else {
print "Meter response CRC failed\n\n";
}
}
## critic
}
# This function takes the response from the meter
# and parses its output into an array using the defined table
sub parse_response {
my ($response, $table, $kwh_scale) = @_;
my %response_array = ();
my $pointer_field = 0;
# Accessing elements using array indices
for my $i (0 .. $#{$table}) {
my $key = $table->[$i][0];
my $val = $table->[$i][1];
my $field_type = $table->[$i][2];
my $substring = substr($response, $pointer_field, $val, '');
if ($field_type eq "byte") {
my $hex_values = "";
for my $char (split //, $substring) {
$hex_values .= sprintf("%x", ord($char));
}
$response_array{$key} = $hex_values;
} elsif ($field_type eq "integer") {
$response_array{$key} = int($substring);
} elsif ($field_type eq "float") {
$response_array{$key} = sprintf("%.2f", $substring);
} elsif ($field_type eq "string") {
$response_array{$key} = $substring;
}
$pointer = $val + $pointer;
}
my $converted_array = scale_type(\%response_array, $table, $kwh_scale);
return $converted_array;
}
sub scale_type {
my ($response_array_ref, $table_array, $kwh_scale) = @_;
if ($kwh_scale eq ""){
$kwh_scale = $response_array_ref->{kWh_Scale};
}
for my $i (0 .. $#{$table_array}) {
$key = $table_array->[$i][0];
if ($table_array->[$i][3] eq "kWh_Scale") {
$response_array_ref->{$key} = sprintf("%.2f", $response_array_ref->{$key} / (10 ** $kwh_scale));
} elsif ($table_array->[$i][3] eq "/10") {
$response_array_ref->{$key} = sprintf("%.2f", $response_array_ref->{$key} / 10);
} elsif ($table_array->[$i][3] eq "/100") {
$response_array_ref->{$key} = sprintf("%.2f", $response_array_ref->{$key} / 100);
}
}
return $response_array_ref;
}
# In Perl, hashes do not maintain order because they are inherently unordered collections of key-value pairs.
# For consistency, the following function will print the hash in the expected order.
sub print_in_order {
my ($array_ref, $table) = @_;
for my $i (0 .. $#{$table}) {
my $key = $table->[$i][0];
print "$key: $array_ref->{$key}\n";
}
}
# Params for serial port
# Change /dev/ttyUSB0 to match your system
# Open connection to serial port
$sp = new Device::SerialPort('/dev/ttyUSB0'); ## no critic (Objects::ProhibitIndirectSyntax)
Readonly my $BAUDRATE => 9600;
Readonly my $DATABITS => 7;
Readonly my $NUM_THOUSAND => 1000;
# Configure the port
$sp->user_msg(ON);
$sp->baudrate($BAUDRATE);
$sp->parity('even');
$sp->databits($DATABITS);
$sp->stopbits(1);
$sp->handshake('xoff');
$sp->write_settings;
# don't wait for each character
$sp->read_char_time(0);
# wait for 1 second per unfulfilled read
$sp->read_const_time($NUM_THOUSAND);
# Meter Number and Password, Ex. 000300001184
my $meter = '000300001184';
# In this example we have assigned
# some meter request strings to variables
## no critic (ValuesAndExpressions::ProhibitEscapedCharacters)
# Get meter data A request and connection request
my $cmd_request_a = "\x2F\x3F$meter\x30\x30\x21\x0D\x0A";
# Get meter data B request
my $cmd_request_b = "\x2F\x3F$meter\x30\x31\x21\x0D\x0A";
# Close Connection String
my $cmd_close = "\x01\x42\x30\x03\x75";
## critic
# Now we set up parsing tables for the data A and B requests
my $tbl_request_a = [
["reserved_start_byte", 1, "byte", "none"],
["Model", 2, "byte", "none"],
["Firmware", 1, "byte", "none"],
["Meter_Address", 12, "string", "none"],
["kWh_Tot", 8, "float", "kWh_Scale"],
["Reactive_Energy_Tot", 8, "float", "kWh_Scale"],
["Rev_kWh_Tot", 8, "float", "kWh_Scale"],
["kWh_Ln_1", 8, "float", "kWh_Scale"],
["kWh_Ln_2", 8, "float", "kWh_Scale"],
["kWh_Ln_3", 8, "float", "kWh_Scale"],
["Rev_kWh_Ln_1", 8, "float", "kWh_Scale"],
["Rev_kWh_Ln_2", 8, "float", "kWh_Scale"],
["Rev_kWh_Ln_3", 8, "float", "kWh_Scale"],
["Resettable_kWh_Tot", 8, "float", "kWh_Scale"],
["Resettable_Rev_kWh_Tot", 8, "float", "kWh_Scale"],
["RMS_Volts_Ln_1", 4, "float", "/10"],
["RMS_Volts_Ln_2", 4, "float", "/10"],
["RMS_Volts_Ln_3", 4, "float", "/10"],
["Amps_Ln_1", 5, "float", "/10"],
["Amps_Ln_2", 5, "float", "/10"],
["Amps_Ln_3", 5, "float", "/10"],
["RMS_Watts_Ln_1", 7, "integer", "none"],
["RMS_Watts_Ln_2", 7, "integer", "none"],
["RMS_Watts_Ln_3", 7, "integer", "none"],
["RMS_Watts_Tot", 7, "integer", "none"],
["Cos_Theta_Ln_1", 4, "string", "none"],
["Cos_Theta_Ln_2", 4, "string", "none"],
["Cos_Theta_Ln_3", 4, "string", "none"],
["Reactive_Pwr_Ln_1", 7, "integer", "none"],
["Reactive_Pwr_Ln_2", 7, "integer", "none"],
["Reactive_Pwr_Ln_3", 7, "integer", "none"],
["Reactive_Pwr_Tot", 7, "integer", "none"],
["Line_Freq", 4, "float", "/100"],
["Pulse_Cnt_1", 8, "integer", "none"],
["Pulse_Cnt_2", 8, "integer", "none"],
["Pulse_Cnt_3", 8, "integer", "none"],
["State_Inputs", 1, "integer", "none"],
["State_Watts_Dir", 1, "integer", "none"],
["State_Out", 1, "integer", "none"],
["kWh_Scale", 1, "integer", "none"],
["reserved_end_data", 2, "byte", "none"],
["Meter_Time", 14, "integer", "none"],
["reserved_type", 2, "byte", "none"],
["reserved_end", 4, "byte", "none"],
["crc16", 2, "byte", "none"]
];
my $tbl_request_b = [
["reserved_start_byte", 1, "byte", "none"],
["Model", 2, "byte", "none"],
["Firmware", 1, "byte", "none"],
["Meter_Address", 12, "string", "none"],
["kWh_Tariff_1", 8, "float", "kWh_Scale"],
["kWh_Tariff_2", 8, "float", "kWh_Scale"],
["kWh_Tariff_3", 8, "float", "kWh_Scale"],
["kWh_Tariff_4", 8, "float", "kWh_Scale"],
["Rev_kWh_Tariff_1", 8, "float", "kWh_Scale"],
["Rev_kWh_Tariff_2", 8, "float", "kWh_Scale"],
["Rev_kWh_Tariff_3", 8, "float", "kWh_Scale"],
["Rev_kWh_Tariff_4", 8, "float", "kWh_Scale"],
["RMS_Volts_Ln_1", 4, "float", "/10"],
["RMS_Volts_Ln_2", 4, "float", "/10"],
["RMS_Volts_Ln_3", 4, "float", "/10"],
["Amps_Ln_1", 5, "float", "/10"],
["Amps_Ln_2", 5, "float", "/10"],
["Amps_Ln_3", 5, "float", "/10"],
["RMS_Watts_Ln_1", 7, "integer", "none"],
["RMS_Watts_Ln_2", 7, "integer", "none"],
["RMS_Watts_Ln_3", 7, "integer", "none"],
["RMS_Watts_Tot", 7, "integer", "none"],
["Cos_Theta_Adj_Ln_1", 4, "string", "none"],
["Cos_Theta_Adj_Ln_2", 4, "string", "none"],
["Cos_Theta_Adj_Ln_3", 4, "string", "none"],
["RMS_Watts_Max_Demand", 8, "float", "/10"],
["Max_Demand_Period", 1, "integer", "none"],
["Pulse_Ratio_1", 4, "integer", "none"],
["Pulse_Ratio_2", 4, "integer", "none"],
["Pulse_Ratio_3", 4, "integer", "none"],
["CT_Ratio", 4, "integer", "none"],
["reserved_b2", 1, "byte", "none"],
["Pulse_Output_Ratio", 4, "integer", "none"],
["reserved_b3", 56, "byte", "none"],
["Meter_Time", 14, "integer", "none"],
["reserved_type", 2, "byte", "none"],
["reserved_end", 4, "byte", "none"],
["crc16", 2, "byte", "none"]
];
## no critic (InputOutput::RequireCheckedSyscalls)
print "So let's give it a try...\n";
print "First we open the connection to the meter\n";
print "It will respond with the first 255 bytes of meter data (Request A)\n";
$request_a = parse_response( send_rec($cmd_request_a), $tbl_request_a, "");
print_in_order($request_a, $tbl_request_a);
print "\n\nNow we can request other data\n";
print "For example the remaining 255 bytes of meter data (Request B)\n";
$request_b = parse_response( send_rec($cmd_request_b), $tbl_request_b, $request_a->{kWh_Scale});
print_in_order($request_b, $tbl_request_b);
print "When you're finished you will close the connection\n";
send_rec($cmd_close);
## critic
# Close connection to serial port
$sp->close || croak 'failed to close';
undef $sp;
/*
openjdk version "17.0.2" 2022-01-18
Download the jssc.jar (java-simple-serial-connector)
from: https://code.google.com/archive/p/java-simple-serial-connector/
Version: jSSC-2.8.0
Download the correct org.json jar version for your
needs from: https://mvnrepository.com/artifact/org.json/json
This example uses version 20220320 accessible here:
https://repo1.maven.org/maven2/org/json/json/20220320/json-20220320.jar
Instructions to run this program
1. Put this code in a file named Connect.java
2. Copy the downloaded jssc.jar and Connect.java to the same directory
3. Compile
javac -cp .:./jssc.jar:./json-20220320.jar ./Connect.java
4. Run
java -cp .:./jssc.jar:./json-20220320.jar Connect
Use sudo for linux users
*/
// Import required classes
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.*;
import jssc.*;
import netscape.javascript.JSObject;
import org.json.*;
import org.json.JSONObject;
@java.lang.SuppressWarnings({ "java:S106", "java:S112", "java:S125" })
public class Connect {
static SerialPort serialPort;
// EKM CRC-16 Table
private static final int[] CRCTABLE = {
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040 };
// This function makes use of the CRC table to calulate the CRC
// example: ekmCalcCrc16("PUT_STRING_HERE_YOU_WANT_CRC_OF");
public static int ekmCalcCrc16(String buf) {
int crc = 0xffff;
if (buf != null) {
char[] charArr = buf.toCharArray();
for (int i = 0, len = charArr.length; i < len; i++) {
int c = charArr[i];
int index = (crc ^ c) & 0xff;
int crct = CRCTABLE[index];
crc = ((crc >> 8) ^ crct);
}
crc = (crc << 8) | (crc >> 8);
crc &= 0x7F7F;
}
return crc;
}
// This function takes the response from the meter
// and parses its output into an array using the defined table
public static JSONObject printInOrder(JSONObject dataObject, List<String> orderedKeys) {
for (String key : orderedKeys) {
Object value = dataObject.get(key);
System.out.println(key + ": " + value);
}
return null;
}
public static JSONObject scaleType(JSONObject dataObject, JSONObject table, Integer kWhScale) {
if (kWhScale == null) {
kWhScale = dataObject.optInt("kWh_Scale");
}
// Iterate over the keys
Iterator<String> keys = table.keys();
while (keys.hasNext()) {
String key = keys.next();
Object value = table.get(key);
JSONArray dataScale = (JSONArray) value;
Object dataScaleObject = dataScale.get(2);
String dataScaleString = dataScaleObject.toString();
Double result = null;
if ("kWh_Scale".equals(dataScaleString)) {
double dataValue = dataObject.getDouble(key);
result = dataValue / (float) Math.pow(10, kWhScale);
} else if ("/10".equals(dataScaleString)) {
double dataValue = dataObject.getDouble(key);
result = dataValue / 10.0;
} else if ("/100".equals(dataScaleString)) {
double dataValue = dataObject.getDouble(key);
result = dataValue / 100.0;
}
if (result != null) {
// Use BigDecimal to format result to 2 decimal places
BigDecimal bd = BigDecimal.valueOf(result);
bd = bd.setScale(2, RoundingMode.DOWN); // Truncate to 2 decimal places
dataObject.put(key, bd.doubleValue());
}
}
return dataObject;
}
public static JSONObject parseResponse(String response, JSONObject table, Integer kWhScale,
List<String> orderedKeys) {
JSONObject responseArray = new JSONObject();
Integer pointer = 0;
for (String key : orderedKeys) {
Object value = table.get(key);
JSONArray valueArray = (JSONArray) value;
Integer pointerValue = (Integer) valueArray.get(0);
Object dataTypeObject = valueArray.get(1);
String typeString = dataTypeObject.toString();
Integer end = pointerValue + pointer;
// Extract substring between pointer and end positions
String extractedValue = response.substring(pointer, end);
if ("byte".equals(typeString)) {
StringBuilder hexValues = new StringBuilder();
for (char c : extractedValue.toCharArray()) {
hexValues.append(Integer.toHexString((int) c));
}
String hexString = hexValues.toString();
responseArray.put(key, hexString);
} else if ("integer".equals(typeString)) {
if ("Meter_Time".equals(key)) {
BigInteger integerValue = new BigInteger(extractedValue);
responseArray.put(key, integerValue);
} else {
int integerValue = Integer.parseInt(extractedValue);
responseArray.put(key, integerValue);
}
} else if ("float".equals(typeString)) {
float floatValue = Float.parseFloat(extractedValue);
responseArray.put(key, floatValue);
} else if ("string".equals(typeString)) {
responseArray.put(key, extractedValue);
}
pointer += pointerValue;
}
responseArray = scaleType(responseArray, table, kWhScale);
printInOrder(responseArray, orderedKeys);
return responseArray;
}
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter and check its crc to make
// sure
// the response is correct
public static String sendRec(String send) throws SerialPortException {
String firstByte;
String response;
byte[] buffer;
// get the the 2nd through the last byte
// from the send string to create crc
int crc = ekmCalcCrc16(send.substring(1));
send = send + (char) ((crc >> 8) & 0xFF) + (char) (crc & 0xFF);
// Send request
serialPort.writeBytes(send.getBytes());
// Set connection timeout;
int timeout = 5;
// Get byte 1 of meter response
try {
firstByte = serialPort.readString(1, timeout * 1000);
} catch (SerialPortTimeoutException e1) {
System.out.println("Meter Instruction: ");
System.out.println(byteArrayToHex(send));
System.out.println("Did not trigger a response\n");
return "";
}
// The meter returns HEX 06 if ok
if (firstByte.charAt(0) == (char) (6 & 0xFF)) {
System.out.println("OK.\n");
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
return "";
}
// Get rest of meter response
try {
buffer = serialPort.readBytes(254, timeout * 1000);
} catch (SerialPortTimeoutException e1) {
System.out.println("Did receive complete response\n");
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
return "";
}
response = new String(buffer);
// Clear response buffer
serialPort.purgePort(SerialPort.PURGE_RXCLEAR | SerialPort.PURGE_TXCLEAR);
// If the meter returns HEX 02 it's the start of a text read
if (firstByte.charAt(0) == (char) (2 & 0xFF)) {
// Put 02 back in the response beings it
// was pulled out when testing the first byte
response = "" + (char) (2) + response;
// Get check CRC in response (byte 2 through the 3rd from the last)
// to make sure it's valid
crc = ekmCalcCrc16(response.substring(1, 253));
// Compare our calculated CRC with the resonse CRC
if ((char) ((crc >> 8) & 0xFF) == response.charAt(253) && (char) (crc & 0xFF) == response.charAt(254)) {
// Return response
return response;
} else {
System.out.println("Meter response CRC failed\n");
return "";
}
}
return "";
}
public static void main(String[] args) {
serialPort = new SerialPort("/dev/ttyUSB0");
try {
// Open connection to serial port
// Change /dev/ttyUSB0 to match your system
serialPort.openPort();
// Configure the port
serialPort.setParams(SerialPort.BAUDRATE_9600,
SerialPort.DATABITS_7,
SerialPort.STOPBITS_1,
SerialPort.PARITY_EVEN);
serialPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
// Meter Number, for example: 000300001184
String meter = "000300001184";
// In this example we have assigned
// some meter request strings to variables
// Get meter data A request and connection request
String cmdRequestA = "\u002F\u003F" + meter + "\u0030\u0030\u0021\r\n";
// Get meter data B request
String cmdRequestB = "\u002F\u003F" + meter + "\u0030\u0031\u0021\r\n";
// Close Connection String
String cmdClose = "\u0001\u0042\u0030\u0003\u0075";
// Now we set up parsing tables for the data A and B requests
String tblStringA = "{\"reserved_start_byte\": [1, \"byte\", \"none\"], \"Model\": [2, \"byte\", \"none\"], \"Firmware\": [1, \"byte\", \"none\"], \"Meter_Address\": [12, \"string\", \"none\"], \"kWh_Tot\": [8, \"float\", \"kWh_Scale\"], \"Reactive_Energy_Tot\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Tot\": [8, \"float\", \"kWh_Scale\"], \"kWh_Ln_1\": [8, \"float\", \"kWh_Scale\"], \"kWh_Ln_2\": [8, \"float\", \"kWh_Scale\"], \"kWh_Ln_3\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Ln_1\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Ln_2\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Ln_3\": [8, \"float\", \"kWh_Scale\"], \"Resettable_kWh_Tot\": [8, \"float\", \"kWh_Scale\"], \"Resettable_Rev_kWh_Tot\": [8, \"float\", \"kWh_Scale\"], \"RMS_Volts_Ln_1\": [4, \"float\", \"/10\"], \"RMS_Volts_Ln_2\": [4, \"float\", \"/10\"], \"RMS_Volts_Ln_3\": [4, \"float\", \"/10\"], \"Amps_Ln_1\": [5, \"float\", \"/10\"], \"Amps_Ln_2\": [5, \"float\", \"/10\"], \"Amps_Ln_3\": [5, \"float\", \"/10\"], \"RMS_Watts_Ln_1\": [7, \"integer\", \"none\"], \"RMS_Watts_Ln_2\": [7, \"integer\", \"none\"], \"RMS_Watts_Ln_3\": [7, \"integer\", \"none\"], \"RMS_Watts_Tot\": [7, \"integer\", \"none\"], \"Cos_Theta_Ln_1\": [4, \"string\", \"none\"], \"Cos_Theta_Ln_2\": [4, \"string\", \"none\"], \"Cos_Theta_Ln_3\": [4, \"string\", \"none\"], \"Reactive_Pwr_Ln_1\": [7, \"integer\", \"none\"], \"Reactive_Pwr_Ln_2\": [7, \"integer\", \"none\"], \"Reactive_Pwr_Ln_3\": [7, \"integer\", \"none\"], \"Reactive_Pwr_Tot\": [7, \"integer\", \"none\"], \"Line_Freq\": [4, \"float\", \"/100\"], \"Pulse_Cnt_1\": [8, \"integer\", \"none\"], \"Pulse_Cnt_2\": [8, \"integer\", \"none\"], \"Pulse_Cnt_3\": [8, \"integer\", \"none\"], \"State_Inputs\": [1, \"integer\", \"none\"], \"State_Watts_Dir\": [1, \"integer\", \"none\"], \"State_Out\": [1, \"integer\", \"none\"], \"kWh_Scale\": [1, \"integer\", \"none\"], \"reserved_end_data\": [2, \"byte\", \"none\"], \"Meter_Time\": [14, \"integer\", \"none\"], \"reserved_type\": [2, \"byte\", \"none\"], \"reserved_end\": [4, \"byte\", \"none\"], \"crc16\": [2, \"byte\", \"none\"]}";
// Parse the JSON string into a JSONObject
JSONObject tblRequestA = new JSONObject(tblStringA);
List<String> orderedKeysA = new ArrayList<>();
orderedKeysA.addAll(Arrays.asList("reserved_start_byte", "Model", "Firmware", "Meter_Address", "kWh_Tot", "Reactive_Energy_Tot", "Rev_kWh_Tot", "kWh_Ln_1", "kWh_Ln_2", "kWh_Ln_3", "Rev_kWh_Ln_1", "Rev_kWh_Ln_2", "Rev_kWh_Ln_3", "Resettable_kWh_Tot", "Resettable_Rev_kWh_Tot", "RMS_Volts_Ln_1", "RMS_Volts_Ln_2", "RMS_Volts_Ln_3", "Amps_Ln_1", "Amps_Ln_2", "Amps_Ln_3", "RMS_Watts_Ln_1", "RMS_Watts_Ln_2", "RMS_Watts_Ln_3", "RMS_Watts_Tot", "Cos_Theta_Ln_1", "Cos_Theta_Ln_2", "Cos_Theta_Ln_3", "Reactive_Pwr_Ln_1", "Reactive_Pwr_Ln_2", "Reactive_Pwr_Ln_3", "Reactive_Pwr_Tot", "Line_Freq", "Pulse_Cnt_1", "Pulse_Cnt_2", "Pulse_Cnt_3", "State_Inputs", "State_Watts_Dir", "State_Out", "kWh_Scale", "reserved_end_data", "Meter_Time", "reserved_type", "reserved_end", "crc16"));
String tblStringB = "{\"reserved_start_byte\": [1, \"byte\", \"none\"], \"Model\": [2, \"byte\", \"none\"], \"Firmware\": [1, \"byte\", \"none\"], \"Meter_Address\": [12, \"string\", \"none\"], \"kWh_Tariff_1\": [8, \"float\", \"kWh_Scale\"], \"kWh_Tariff_2\": [8, \"float\", \"kWh_Scale\"], \"kWh_Tariff_3\": [8, \"float\", \"kWh_Scale\"], \"kWh_Tariff_4\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Tariff_1\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Tariff_2\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Tariff_3\": [8, \"float\", \"kWh_Scale\"], \"Rev_kWh_Tariff_4\": [8, \"float\", \"kWh_Scale\"], \"RMS_Volts_Ln_1\": [4, \"float\", \"/10\"], \"RMS_Volts_Ln_2\": [4, \"float\", \"/10\"], \"RMS_Volts_Ln_3\": [4, \"float\", \"/10\"], \"Amps_Ln_1\": [5, \"float\", \"/10\"], \"Amps_Ln_2\": [5, \"float\", \"/10\"], \"Amps_Ln_3\": [5, \"float\", \"/10\"], \"RMS_Watts_Ln_1\": [7, \"integer\", \"none\"], \"RMS_Watts_Ln_2\": [7, \"integer\", \"none\"], \"RMS_Watts_Ln_3\": [7, \"integer\", \"none\"], \"RMS_Watts_Tot\": [7, \"integer\", \"none\"], \"Cos_Theta_Adj_Ln_1\": [4, \"string\", \"none\"], \"Cos_Theta_Adj_Ln_2\": [4, \"string\", \"none\"], \"Cos_Theta_Adj_Ln_3\": [4, \"string\", \"none\"], \"RMS_Watts_Max_Demand\": [8, \"float\", \"/10\"], \"Max_Demand_Period\": [1, \"integer\", \"none\"], \"Pulse_Ratio_1\": [4, \"integer\", \"none\"], \"Pulse_Ratio_2\": [4, \"integer\", \"none\"], \"Pulse_Ratio_3\": [4, \"integer\", \"none\"], \"CT_Ratio\": [4, \"integer\", \"none\"], \"reserved_b2\": [1, \"byte\", \"none\"], \"Pulse_Output_Ratio\": [4, \"integer\", \"none\"], \"reserved_b3\": [56, \"byte\", \"none\"], \"Meter_Time\": [14, \"integer\", \"none\"], \"reserved_type\": [2, \"byte\", \"none\"], \"reserved_end\": [4, \"byte\", \"none\"], \"crc16\": [2, \"byte\", \"none\"] }";
JSONObject tblRequestB = new JSONObject(tblStringB);
List<String> orderedKeysB = new ArrayList<>();
orderedKeysB.addAll(Arrays.asList("reserved_start_byte", "Model", "Firmware", "Meter_Address", "kWh_Tariff_1", "kWh_Tariff_2", "kWh_Tariff_3", "kWh_Tariff_4", "Rev_kWh_Tariff_1", "Rev_kWh_Tariff_2", "Rev_kWh_Tariff_3", "Rev_kWh_Tariff_4", "RMS_Volts_Ln_1", "RMS_Volts_Ln_2", "RMS_Volts_Ln_3", "Amps_Ln_1", "Amps_Ln_2", "Amps_Ln_3", "RMS_Watts_Ln_1", "RMS_Watts_Ln_2", "RMS_Watts_Ln_3", "RMS_Watts_Tot", "Cos_Theta_Adj_Ln_1", "Cos_Theta_Adj_Ln_2", "Cos_Theta_Adj_Ln_3", "RMS_Watts_Max_Demand", "Max_Demand_Period", "Pulse_Ratio_1", "Pulse_Ratio_2", "Pulse_Ratio_3", "CT_Ratio", "reserved_b2", "Pulse_Output_Ratio", "reserved_b3", "Meter_Time", "reserved_type", "reserved_end", "crc16"));
System.out.println("So let's give it a try...");
System.out.println("First we open the connection to the meter");
System.out.println("It will respond with the first 255 bytes of meter data (Request A)");
// sendRec(cmdRequestA);
JSONObject requestA = parseResponse(sendRec(cmdRequestA), tblRequestA, null, orderedKeysA);
System.out.println("\nNow we can request other data.");
System.out.println("For example the remaining 255 bytes of meter data (Request B)");
JSONObject requestB = parseResponse(sendRec(cmdRequestB), tblRequestB, requestA.getInt("kWh_Scale"),
orderedKeysB);
System.out.println("When you're finished you will close the connection");
sendRec(cmdClose);
// Close connection to serial port
serialPort.closePort();
} catch (SerialPortException ex) {
System.out.println(ex);
}
}
public static String byteArrayToHex(String orgStr) {
byte[] a = orgStr.getBytes();
StringBuilder sb = new StringBuilder(a.length * 2);
for (byte b : a) {
sb.append(String.format("%02x", b & 0xff));
sb.append(" ");
}
return sb.toString();
}
}
/*
* Requirements
* NodeJS: v20.10.0
* Serialport: 12.0.0
*/
// Load modules
const {SerialPort} = require('serialport');
// Params for Serial Port
// Change path to match your system
const sp = new SerialPort(
{
path: '/dev/ttyUSB0',
baudRate: 9600,
parity: 'even',
dataBits: 7,
stopBits: 1,
flowControl: 0,
bufferSize: 1,
},
false,
);
// This should be the meter number you are trying to connect to
const meter = '000300001184';
const timeout = 5;
let response = '';
// In this example we have assigned
// some meter request strings to variables
// Get meter data A request and connection request
const cmdRequestA = `\x2F\x3F${meter}\x30\x30\x21\x0D\x0A`;
// Get meter data B request
const cmdRequestB = `\x2F\x3F${meter}\x30\x31\x21\x0D\x0A`;
// Close Connection String
const cmdClose = '\x01\x42\x30\x03\x75';
// EKM CRC-16 Table
const CRCTABLE = [
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601,
0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440, 0xcc01, 0x0cc0,
0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40, 0x0a00, 0xcac1, 0xcb81,
0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941,
0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01,
0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0,
0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081,
0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00,
0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0,
0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981,
0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41,
0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700,
0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0,
0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281,
0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1,
0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80,
0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541,
0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101,
0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0,
0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481,
0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801,
0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1,
0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581,
0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341,
0x4100, 0x81c1, 0x8081, 0x4040,
];
// This function makes use of the CRC table to calulate the CRC
// example: ekm_calc_crc16('PUT_STRING_HERE_YOU_WANT_CRC_OF');
const ekmCalcCrc16 = (buf) => {
let crc = 0xffff;
for (let i = 0, len = buf.length; i < len; i++) {
const c = buf[i];
const index = (crc ^ c.charCodeAt(0)) & 0xff;
const crct = CRCTABLE[index];
crc = (crc >> 8) ^ crct;
}
crc = (crc << 8) | (crc >> 8);
crc &= 0x7f7f;
return crc;
};
const strToHex = (str) => {
let hex = '';
for (let i = 0; i < str.length; i++) {
const pad = `${str.charCodeAt(i).toString(16).toUpperCase()}`;
hex += ` ${`00${pad}`.slice(-2)}`;
}
return hex;
};
const getResponse = (cmd) =>
new Promise((resolve, reject) => {
// Clear response data
response = '';
sp.write(cmd, (err) => {
if (err) {
console.log(err);
reject(err);
}
setTimeout(() => {
resolve(response);
}, timeout * 1000);
});
});
// In this code example we make a simple function that will take the command
// you want to send to the meter and make the crc for it before sending.
// After sending we get the response from the meter
// and check its crc to make sure the response is correct
const sendReq = async (cmd) => {
// Get the the 2nd through the last byte
// from the cmd string to create crc
let crc = ekmCalcCrc16(cmd.substr(1));
// Convert CRC from int to byte and add to the cmd request
cmd =
cmd +
String.fromCharCode((crc >> 8) & 0xff) +
String.fromCharCode(crc & 0xff);
const resRec = await getResponse(cmd);
if (resRec === '') {
console.log('Meter Instruction: ');
console.log(strToHex(resRec));
console.log('Did not trigger a response\n');
} else {
// The meter returns HEX 06 if OK
if (resRec.substr(0, 1) === '\x06') {
console.log('OK.\n');
}
// If the meter returns HEX 02 it's the start of a text read
if (resRec.substr(0, 1) === '\x02') {
// Get check CRC in response (byte 2 through the 3rd from the last)
// to make sure it's valid
crc = ekmCalcCrc16(resRec.substr(1, resRec.length - 3));
// Compare our calculated CRC with the response CRC
if (
String.fromCharCode((crc >> 8) & 0xff) +
String.fromCharCode(crc & 0xff) ===
resRec.substr(-2)
) {
// Show resRec
console.log(`${strToHex(resRec)}\n`);
} else {
console.log('Meter response CRC failed\n');
}
}
}
return resRec;
};
// This function takes the response from the meter
// and parses its output into an array using the defined table
const parseResponse = async (res, table, kwhScale = '') => {
let responseObj = {};
let pointer = 0;
for (const key in table) {
if ({}.hasOwnProperty.call(table, key)) {
const val = table[key];
const end = val[0] + pointer;
if (val[1] == 'byte') {
let hexValues = '';
for (const char of res.slice(pointer, end)) {
hexValues += char.charCodeAt(0).toString(16);
}
responseObj[key] = hexValues;
} else if (val[1] == 'integer') {
responseObj[key] = parseInt(res.substring(pointer, end));
} else if (val[1] == 'float') {
responseObj[key] = parseFloat(res.substring(pointer, end));
} else if (val[1] == 'string') {
responseObj[key] = res.substring(pointer, end).toString();
}
pointer += val[0];
}
}
responseObj = scaleType(responseObj, table, kwhScale);
return responseObj;
};
function scaleType(responseArray, tableArray, kwhScale) {
/*
Scaling values
*/
if (kwhScale === '') {
kwhScale = responseArray['kWh_Scale'];
}
for (const key in tableArray) {
const scaleT = tableArray[key][2];
if (scaleT === 'kWh_Scale') {
responseArray[key] = responseArray[key] / Math.pow(10, kwhScale);
} else if (scaleT === '/10') {
responseArray[key] = responseArray[key] / 10;
} else if (scaleT === '/100') {
responseArray[key] = responseArray[key] / 100;
}
}
return responseArray;
}
const tableRequestA = {
reserved_start_byte: [1, 'byte', 'none'],
Model: [2, 'byte', 'none'],
Firmware: [1, 'byte', 'none'],
Meter_Address: [12, 'string', 'none'],
kWh_Tot: [8, 'float', 'kWh_Scale'],
Reactive_Energy_Tot: [8, 'float', 'kWh_Scale'],
Rev_kWh_Tot: [8, 'float', 'kWh_Scale'],
kWh_Ln_1: [8, 'float', 'kWh_Scale'],
kWh_Ln_2: [8, 'float', 'kWh_Scale'],
kWh_Ln_3: [8, 'float', 'kWh_Scale'],
Rev_kWh_Ln_1: [8, 'float', 'kWh_Scale'],
Rev_kWh_Ln_2: [8, 'float', 'kWh_Scale'],
Rev_kWh_Ln_3: [8, 'float', 'kWh_Scale'],
Resettable_kWh_Tot: [8, 'float', 'kWh_Scale'],
Resettable_Rev_kWh_Tot: [8, 'float', 'kWh_Scale'],
RMS_Volts_Ln_1: [4, 'float', '/10'],
RMS_Volts_Ln_2: [4, 'float', '/10'],
RMS_Volts_Ln_3: [4, 'float', '/10'],
Amps_Ln_1: [5, 'float', '/10'],
Amps_Ln_2: [5, 'float', '/10'],
Amps_Ln_3: [5, 'float', '/10'],
RMS_Watts_Ln_1: [7, 'integer', 'none'],
RMS_Watts_Ln_2: [7, 'integer', 'none'],
RMS_Watts_Ln_3: [7, 'integer', 'none'],
RMS_Watts_Tot: [7, 'integer', 'none'],
Cos_Theta_Ln_1: [4, 'string', 'none'],
Cos_Theta_Ln_2: [4, 'string', 'none'],
Cos_Theta_Ln_3: [4, 'string', 'none'],
Reactive_Pwr_Ln_1: [7, 'integer', 'none'],
Reactive_Pwr_Ln_2: [7, 'integer', 'none'],
Reactive_Pwr_Ln_3: [7, 'integer', 'none'],
Reactive_Pwr_Tot: [7, 'integer', 'none'],
Line_Freq: [4, 'float', '/100'],
Pulse_Cnt_1: [8, 'integer', 'none'],
Pulse_Cnt_2: [8, 'integer', 'none'],
Pulse_Cnt_3: [8, 'integer', 'none'],
State_Inputs: [1, 'integer', 'none'],
State_Watts_Dir: [1, 'integer', 'none'],
State_Out: [1, 'integer', 'none'],
kWh_Scale: [1, 'integer', 'none'],
reserved_end_data: [2, 'byte', 'none'],
Meter_Time: [14, 'integer', 'none'],
reserved_type: [2, 'byte', 'none'],
reserved_end: [4, 'byte', 'none'],
crc16: [2, 'byte', 'none'],
};
const tableRequestB = {
reserved_start_byte: [1, 'byte', 'none'],
Model: [2, 'byte', 'none'],
Firmware: [1, 'byte', 'none'],
Meter_Address: [12, 'string', 'none'],
kWh_Tariff_1: [8, 'float', 'kWh_Scale'],
kWh_Tariff_2: [8, 'float', 'kWh_Scale'],
kWh_Tariff_3: [8, 'float', 'kWh_Scale'],
kWh_Tariff_4: [8, 'float', 'kWh_Scale'],
Rev_kWh_Tariff_1: [8, 'float', 'kWh_Scale'],
Rev_kWh_Tariff_2: [8, 'float', 'kWh_Scale'],
Rev_kWh_Tariff_3: [8, 'float', 'kWh_Scale'],
Rev_kWh_Tariff_4: [8, 'float', 'kWh_Scale'],
RMS_Volts_Ln_1: [4, 'float', '/10'],
RMS_Volts_Ln_2: [4, 'float', '/10'],
RMS_Volts_Ln_3: [4, 'float', '/10'],
Amps_Ln_1: [5, 'float', '/10'],
Amps_Ln_2: [5, 'float', '/10'],
Amps_Ln_3: [5, 'float', '/10'],
RMS_Watts_Ln_1: [7, 'integer', 'none'],
RMS_Watts_Ln_2: [7, 'integer', 'none'],
RMS_Watts_Ln_3: [7, 'integer', 'none'],
RMS_Watts_Tot: [7, 'integer', 'none'],
Cos_Theta_Adj_Ln_1: [4, 'string', 'none'],
Cos_Theta_Adj_Ln_2: [4, 'string', 'none'],
Cos_Theta_Adj_Ln_3: [4, 'string', 'none'],
RMS_Watts_Max_Demand: [8, 'float', '/10'],
Max_Demand_Period: [1, 'integer', 'none'],
Pulse_Ratio_1: [4, 'integer', 'none'],
Pulse_Ratio_2: [4, 'integer', 'none'],
Pulse_Ratio_3: [4, 'integer', 'none'],
CT_Ratio: [4, 'integer', 'none'],
reserved_b2: [1, 'byte', 'none'],
Pulse_Output_Ratio: [4, 'integer', 'none'],
reserved_b3: [56, 'byte', 'none'],
Meter_Time: [14, 'integer', 'none'],
reserved_type: [2, 'byte', 'none'],
reserved_end: [4, 'byte', 'none'],
crc16: [2, 'byte', 'none'],
};
(async () => {
sp.open(() => {
console.log('Connection Opened');
// Get meter response
sp.on('data', (read) => {
if (response.length < 255) {
response += read;
}
});
});
console.log(
'So lets give it a try...',
'\nFirst we open the connection to the meter',
'\nIt will respond with the first 255 bytes of meter data (Request A)',
);
const cmdResponseA = await sendReq(cmdRequestA);
const responseA = await parseResponse(cmdResponseA, tableRequestA, '');
console.log(JSON.stringify(responseA, null, 4));
console.log(
'Now we can request the remaining 255' +
' bytes of meter data (Request B)\n',
);
const cmdResponseB = await sendReq(cmdRequestB);
const responseB = await parseResponse(
cmdResponseB,
tableRequestB,
responseA['kWh_Scale'],
);
console.log(JSON.stringify(responseB, null, 4));
console.log('When you are finished you will close the connection');
await sendReq(cmdClose);
sp.close();
})();
The final step. Parsing the meters response
As the final step you can create an array that contains the field names and size of each field and then walk that array to parse the response from the meter to a usable array.
This example, like the other code examples is the simplest way to do this, in order to show you the steps required to get the job done. You can of course build a more robust code base.
In this example we store the table as JSON and use that to parse the response by calling a function named parse_response.
You can find a complete break down of all meter requests and responses in the RS-485 V3 and V4 Requests sections.
V3 Only Requests
The requests and responses in this section apply ONLY to v3 Meters
We also have a straight forward C header file that includes definitions for all the responses from the meters and valid values for request parameters. The more advanced programmer may find this useful. The C header can be found here: RS-485 V3, V4 and V5 Requests.
V3 Read Meter Data/Connect
Required v3 request to connect and standard method to read meter
Request Example:
2f 3f (12 byte Meter_Number) 21 0d 0a
Parameter | Description |
---|---|
Meter_Number | 12 byte Meter_Number. 30 30 30 31 30 30 30 30 31 31 38 84 = 000100001184 |
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_start_ |
1 | Byte | None |
Model | 2 | Byte | None |
Firmware | 1 | Byte | None |
Meter_Address | 12 | String | None |
kWh_Tot | 8 | Float | None |
kWh_Tariff_1 | 8 | Float | None |
kWh_Tariff_2 | 8 | Float | None |
kWh_Tariff_3 | 8 | Float | None |
kWh_Tariff_4 | 8 | Float | None |
Rev_kWh_Tot | 8 | Float | None |
Rev_kWh_ |
8 | Float | None |
Rev_kWh_ |
8 | Float | None |
Rev_kWh_ |
8 | Float | None |
Rev_kWh_ |
8 | Float | None |
RMS_Volts_Ln_1 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_2 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_3 | 4 | Float | Divide By 10 |
Amps_Ln_1 | 5 | Float | Divide By 10 |
Amps_Ln_2 | 5 | Float | Divide By 10 |
Amps_Ln_3 | 5 | Float | Divide By 10 |
RMS_Watts_Ln_1 | 7 | Integer | None |
RMS_Watts_Ln_2 | 7 | Integer | None |
RMS_Watts_Ln_3 | 7 | Integer | None |
RMS_Watts_Tot | 7 | Integer | None |
Cos_Theta_Ln_1 | 4 | String | None |
Cos_Theta_Ln_2 | 4 | String | None |
Cos_Theta_Ln_3 | 4 | String | None |
Max_Demand | 8 | Float | None |
Max_Demand_ |
1 | Integer | None |
Meter_Time | 14 | Integer | None |
CT_Ratio | 4 | Integer | None |
Reserved_type | 59 | Byte | None |
Reserved_end | 4 | Byte | None |
CRC | 2 | Byte | None |
V4/V5 Only Requests
The requests and responses in this section apply ONLY to v4 and v5 Meters
You will notice in a “Response table” for some requests the scale is listed as “kWh_Scale” This means: The scale is based on the kWh_Scale value in the Data A Request. If the kWh_Scale=0 (30 Hex) then no scaling, if it is 1 (31 Hex) then divide by 10, if it is 2 (32 Hex) then divide by 100
We also have a straight forward C header file that includes definitions for all the responses from the meters and valid values for request parameters. The more advanced programmer may find this useful. The C header can be found here: RS-485 V3, V4 and V5 Requests.
V4/V5 Read Meter Data A/Connect
Required v4 or v5 request to connect and standard method to read meter date A
Request Example:
2f 3f (12 byte Meter_Number) 30 30 21 0d 0a
Parameter | Description |
---|---|
Meter_Number | 12 byte Meter_Number. 30 30 30 31 30 30 30 30 31 31 38 84 = 000100001184 |
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_start_ |
1 | Byte | None |
Model | 2 | Byte | None |
Firmware | 1 | Byte | None |
Meter_Address | 12 | String | None |
kWh_Tot | 8 | Float | kWh_Scale |
Reactive_ |
8 | Float | kWh_Scale |
Rev_kWh_Tot | 8 | Float | kWh_Scale |
kWh_Ln_1 | 8 | Float | kWh_Scale |
kWh_Ln_2 | 8 | Float | kWh_Scale |
kWh_Ln_3 | 8 | Float | kWh_Scale |
Rev_kWh_Ln_1 | 8 | Float | kWh_Scale |
Rev_kWh_Ln_2 | 8 | Float | kWh_Scale |
Rev_kWh_Ln_3 | 8 | Float | kWh_Scale |
Resettable_ |
8 | Float | kWh_Scale |
Resettable_Rev_ |
8 | Float | kWh_Scale |
RMS_Volts_Ln_1 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_2 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_3 | 4 | Float | Divide By 10 |
Amps_Ln_1 | 5 | Float | Divide By 10 |
Amps_Ln_2 | 5 | Float | Divide By 10 |
Amps_Ln_3 | 5 | Float | Divide By 10 |
RMS_Watts_Ln_1 | 7 | Integer | None |
RMS_Watts_Ln_2 | 7 | Integer | None |
RMS_Watts_Ln_3 | 7 | Integer | None |
RMS_Watts_Tot | 7 | Integer | None |
Cos_Theta_Ln_1 | 4 | String | None |
Cos_Theta_Ln_2 | 4 | String | None |
Cos_Theta_Ln_3 | 4 | String | None |
Reactive_Pwr_ |
7 | Integer | None |
Reactive_Pwr_ |
7 | Integer | None |
Reactive_Pwr_ |
7 | Integer | None |
Reactive_Pwr_ |
7 | Integer | None |
Line_Freq | 4 | Float | Divide By 100 |
Pulse_Cnt_1 | 8 | Integer | None |
Pulse_Cnt_2 | 8 | Integer | None |
Pulse_Cnt_3 | 8 | Integer | None |
State_Inputs | 1 | Integer | None |
State_Watts_Dir | 1 | Integer | None |
State_Out | 1 | Integer | None |
kWh_Scale | 1 | Integer | None |
Reserved_ |
2 | Byte | None |
Meter_Time | 14 | Integer | None |
Reserved_type | 2 | Byte | None |
Reserved_end | 4 | Byte | None |
CRC | 2 | Byte | None |
V4/V5 Read Meter Data B
Read meter request for data B
Request Example:
2f 3f (12 byte Meter_Number) 30 31 21 0d 0a
Parameter | Description |
---|---|
Meter_Number | 12 byte Meter_Number. 30 30 30 31 30 30 30 30 31 31 38 84 = 000100001184 |
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_start_ |
1 | Byte | None |
Model | 2 | Byte | None |
Firmware | 1 | Byte | None |
Meter_Address | 12 | String | None |
kWh_Tariff_1 | 8 | Float | kWh_Scale |
kWh_Tariff_2 | 8 | Float | kWh_Scale |
kWh_Tariff_3 | 8 | Float | kWh_Scale |
kWh_Tariff_4 | 8 | Float | kWh_Scale |
Rev_kWh_ |
8 | Float | kWh_Scale |
Rev_kWh_ |
8 | Float | kWh_Scale |
Rev_kWh_ |
8 | Float | kWh_Scale |
Rev_kWh_ |
8 | Float | kWh_Scale |
RMS_Volts_Ln_1 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_2 | 4 | Float | Divide By 10 |
RMS_Volts_Ln_3 | 4 | Float | Divide By 10 |
Amps_Ln_1 | 5 | Float | Divide By 10 |
Amps_Ln_2 | 5 | Float | Divide By 10 |
Amps_Ln_3 | 5 | Float | Divide By 10 |
RMS_Watts_Ln_1 | 7 | Integer | None |
RMS_Watts_Ln_2 | 7 | Integer | None |
RMS_Watts_Ln_3 | 7 | Integer | None |
RMS_Watts_Tot | 7 | Integer | None |
Cos_Theta_Adj_Ln_1 | 4 | String | None |
Cos_Theta_Adj_Ln_2 | 4 | String | None |
Cos_Theta_Adj_Ln_3 | 4 | String | None |
RMS_Watts_ |
8 | Float | Divide By 10 |
Max_Demand_ |
1 | Integer | None |
Pulse_Ratio_1 | 4 | Integer | None |
Pulse_Ratio_2 | 4 | Integer | None |
Pulse_Ratio_3 | 4 | Integer | None |
CT_Ratio | 4 | Integer | None |
Reserved_b2 | 1 | Byte | None |
Pulse_Output_ |
4 | Integer | None |
Reserved_b3 | 56 | Byte | None |
Meter_Time | 14 | Integer | None |
Reserved_type | 2 | Byte | None |
Reserved_end | 4 | Byte | None |
CRC | 2 | Byte | None |
V4/V5 Set Relay
Set relay state on meter via serial call
Request Example:
01 57 31 02 30 30 38 (1 byte Relay) 28 (1 byte State) (4 byte Seconds) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Relay | 11 byte Relay. 31 = Relay 1 1 byte Relay. 32 = Relay 2 |
State | 1 byte State. 30 = Open 1 byte State. 31 = Close |
Seconds | 4 byte Seconds. 31 32 33 34 = 1234 (0000-9999) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
V4/V5 Set Pulse Input Ratio
Set the pulse input constant on meter over serial line
Request Example:
01 57 31 02 30 30 41 (1 byte Line) 28 (4 byte Ratio) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Line | 1 byte Line. 31 = Line 1 1 byte Line. 32 = Line 2 1 byte Line. 33 = Line 3 |
Ratio | 4 byte Ratio. 31 32 33 34 = 1234 (0001-9999) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
V4/V5 Set Pulse Output Ratio
Set pulse output ratio serial call. See current value via read request.
Request Example:
01 57 31 02 30 30 44 34 28 (4 byte Ratio) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Ratio | 4 byte Ratio. 30 30 30 31 = 0001 4 byte Ratio. 30 30 30 32 = 0002 4 byte Ratio. 30 30 30 34 = 0004 4 byte Ratio. 30 30 30 35 = 0005 4 byte Ratio. 30 30 30 38 = 0008 4 byte Ratio. 30 30 31 30 = 0010 4 byte Ratio. 30 30 31 36 = 0016 4 byte Ratio. 30 30 32 30 = 0020 4 byte Ratio. 30 30 32 35 = 0025 4 byte Ratio. 30 30 34 30 = 0040 4 byte Ratio. 30 30 35 30 = 0050 4 byte Ratio. 30 30 38 30 = 0080 4 byte Ratio. 30 31 30 30 = 0100 4 byte Ratio. 30 32 30 30 = 0200 4 byte Ratio. 30 34 30 30 = 0400 |
CRC | 2 byte CRC |
Response: True on completion with 06 return
V4/V5 Reset Resettable kWh Reverse
Clear the resettable kWhReverse value
Request Example:
01 57 31 02 30 30 44 33 03 (2 byte CRC)
Parameter | Description |
---|---|
CRC | 2 byte CRC |
Response: True on completion with 06 return
V4/V5 Auto Reset Max Demand
Serial call to set the frequency that the v4/v5 meter automatically resets its Max Demand to zero(0).
Request Example:
01 57 31 02 30 30 44 35 28 (1 byte Interval) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Interval | 1 byte Interval. 30 = Off 1 byte Interval. 31 = Monthly 1 byte Interval. 32 = Weekly 1 byte Interval. 33 = Daily 1 byte Interval. 34 = Hourly |
CRC | 2 byte CRC |
Response: True on completion with 06 return
V4/V5 Set LCD
Set LCD fields serial call
Request Example:
01 57 31 02 30 30 44 32 28 (40 byte LCD_Setting) 29 03 (2 byte CRC)
You will compose a sting of 80 bytes that contains the LCD items you wish to select (2 bytes per item, 40 items total)
The Hex byte values are listed in the table below for each Item
Item | Hex |
---|---|
kWh_Tot | 2 byte. 30 31 = 01 |
Rev_kWh_Tot | 2 byte. 30 32 = 02 |
RMS_Volts_Ln_1 | 2 byte. 30 33 = 03 |
RMS_Volts_Ln_2 | 2 byte. 30 34 = 04 |
RMS_Volts_Ln_3 | 2 byte. 30 35 = 05 |
Amps_Ln_1 | 2 byte. 30 36 = 06 |
Amps_Ln_2 | 2 byte. 30 37 = 07 |
Amps_Ln_3 | 2 byte. 30 38 = 08 |
RMS_Watts_Ln_1 | 2 byte. 30 39 = 09 |
RMS_Watts_Ln_2 | 2 byte. 31 30 = 10 |
RMS_Watts_Ln_3 | 2 byte. 31 31 = 11 |
RMS_Watts_Tot | 2 byte. 31 32 = 12 |
Cos_Theta_Ln_1 | 2 byte. 31 33 = 13 |
Cos_Theta_Ln_2 | 2 byte. 31 34 = 14 |
Cos_Theta_Ln_3 | 2 byte. 31 35 = 15 |
kWh_Tariff_1 | 2 byte. 31 36 = 16 |
kWh_Tariff_2 | 2 byte. 31 37 = 17 |
kWh_Tariff_3 | 2 byte. 31 38 = 18 |
kWh_Tariff_4 | 2 byte. 31 39 = 19 |
Rev_kWh_Tariff_1 | 2 byte. 32 30 = 20 |
Rev_kWh_Tariff_2 | 2 byte. 32 31 = 21 |
Rev_kWh_Tariff_3 | 2 byte. 32 32 = 22 |
Rev_kWh_Tariff_4 | 2 byte. 32 33 = 23 |
Reactive_Pwr_Ln_1 | 2 byte. 32 34 = 24 |
Reactive_Pwr_Ln_2 | 2 byte. 32 35 = 25 |
Reactive_Pwr_Ln_3 | 2 byte. 32 36 = 26 |
Reactive_Pwr_Tot | 2 byte. 32 37 = 27 |
Line_Freq | 2 byte. 32 38 = 28 |
Pulse_Cnt_1 | 2 byte. 32 39 = 29 |
Pulse_Cnt_2 | 2 byte. 33 30 = 30 |
Pulse_Cnt_3 | 2 byte. 33 31 = 31 |
kWh_Ln_1 | 2 byte. 33 32 = 32 |
Rev_kWh_Ln_1 | 2 byte. 33 33 = 33 |
kWh_Ln_2 | 2 byte. 33 34 = 34 |
Rev_kWh_Ln_2 | 2 byte. 33 35 = 35 |
kWh_Ln_3 | 2 byte. 33 36 = 36 |
Rev_kWh_Ln_3 | 2 byte. 33 37 = 37 |
Reactive_Energy_Tot | 2 byte. 33 38 = 38 |
Max_Demand_Rst | 2 byte. 33 39 = 39 |
Rev_kWh_Rst | 2 byte. 34 30 = 40 |
State_Inputs | 2 byte. 34 31 = 41 |
Max_Demand | 2 byte. 34 32 = 42 |
Parameter | Description |
---|---|
LCD_Setting | 40 byte LCD_Setting. Use the list above to create 40 byte string |
CRC | 2 byte CRC |
Response: True on completion with 06 return
V3, V4 and V5 Requests
The requests and responses in this section apply to both v3, v4 and v5 meters. You will notice in a “Response table” for some requests the scale is listed as “kWh_Scale”. This means: V3 Meters have no scaling, V4 and V5 Meters will scale based on the kWh_Scale value in the Data A Request. If the kWh_Scale=0 (30 Hex) then no scaling, if it is 1 (31 Hex) then divide by 10, if it is 2 (32 Hex) then divide by 100.
v3/v4/v5 End Communication
Tell the meter your done talking to it.
Request Example:
01 42 30 03 75
There are no parameters or responses for this request.
v3/v4/v5 Send Password
Password authorization call for all settings to meter.
Request Example:
01 50 31 02 28 (8 byte Password) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Password | 8 byte Password. Default: 30 30 30 30 30 30 30 30 |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Change Password
Reset the meter password. Do not use this function unless the risk of serial line hacking exceeds the risk of pwd loss. There is no recovery backdoor for your meter.
Request Example:
01 57 31 02 30 30 32 30 28 (8 byte New_Password) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
New_Password | 8 byte New_Password |
Response: True on completion with 06 return
v3/v4/v5 Change Meter Address
Set the meter to new address instead of what is stamped on the side.
Request Example:
01 57 31 02 30 30 31 30 28 (12 Bytes New Address) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
New Address | 12 bytes New Address |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set Max Demand Period
Serial call to set the max demand period for the meter.
Request Example:
01 57 31 02 30 30 35 30 28 (1 byte Period) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Period | 1 byte Period. 31 = 15 Minutes 1 byte Period. 32 = 30 Minutes 1 byte Period. 33 = 60 Minutes |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set Max Demand Reset Value
Serial call to set max demand reset value.
Request Example:
01 57 31 02 30 30 34 30 28 (6 byte Reset_Value) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Reset_Value | 6 byte Reset Value. 30 30 30 30 30 30 = Resets to Zero |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set Time
Set the time on the meter.
Request Example:
01 57 31 02 30 30 36 30 28 (2 byte Year) (2 byte Month) (2 byte Day) (2 byte Day_Of_Week) (2 byte Hours) (2 byte Minutes) (2 bytes Seconds) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Year | 2 byte Year. 31 35 = 15 for 2015 |
Month | 2 byte Month. 31 32 = 12 for December |
Day | 2 byte Day. 32 36 = 26th day of month |
Day_Of_Week | 2 byte Day_Of_Week. 30 32 = Monday (31-37) |
Hour | 2 byte Hour. 32 32 = 22 (00-23) |
Minutes | 2 byte Minute. 35 53 = 53 (00-59) |
Seconds | 2 byte Seconds. 31 30 = 10 (00-59) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set CT Ratio
Set the current rating for attached inductive pickup.
Request Example:
01 57 31 02 30 30 44 30 28 (4 byte CT_Ratio) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
CT_Ratio | 2 byte CT_Ratio. 30 31 30 30 = 100 2 byte CT_Ratio. 30 32 30 30 = 200 2 byte CT_Ratio. 30 34 30 30 = 400 2 byte CT_Ratio. 30 36 30 30 = 600 2 byte CT_Ratio. 30 38 30 30 = 800 2 byte CT_Ratio. 31 30 30 30 = 1000 2 byte CT_Ratio. 31 35 30 30 = 1500 2 byte CT_Ratio. 32 30 30 30 = 2000 2 byte CT_Ratio. 33 30 30 30 = 3000 2 byte CT_Ratio. 34 30 30 30 = 4000 2 byte CT_Ratio. 35 30 30 30 = 5000 |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set Schedule Tariffs
Serial set schedule tariff table.
Request Example:
01 57 31 02 30 30 37 (1 byte Schedule) 28 (2 byte Period_1_Hour) (2 byte Period_1_Minute) (2 byte Period_1_Tariff) (1 byte Schedule) (2 byte Period_2_Hour) (2 byte Period_2_Minute) (2 byte Period_2_Tariff) (1 byte Schedule) (2 byte Period_3_Hour) (2 byte Period_3_Minute) (2 byte Period_3_Tariff) (1 byte Schedule) (2 byte Period_4_Hour) (2 byte Period_4_Minute) (2 byte Period_4_Tariff) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Schedule | 1 byte Schedule. 30 = 0 (0-7) |
Period_?_Hour | 2 byte Hour_(1,2,3,4). 31 37 = 17 (00-23) |
Period_?_Minute | 2 byte Minute_(1,2,3,4). 34 30 = 40 (00-59) |
Period_?_Tariff | 2 byte Rate_(1,2,3,4). 30 32 = 02 (00-03) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Read Schedule Tariffs
Read the schedule tariff and time table (either 1 to 4 or 5 to 8).
Example Request:
01 52 31 02 30 30 37 (1 byte Schedule_Range) 03 (2 byte CRC)
Parameter | Description |
---|---|
Schedule_Range | 1 byte Schedule_Range. 30 = Periods 1 to 4 1 byte Schedule_Range. 31 = Periods 5 to 6 |
CRC | 2 byte CRC |
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_ |
6 | Byte | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Schedule_1_ |
2 | Integer | None |
Reserved_1 | 24 | Byte | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Schedule_2_ |
2 | Integer | None |
Reserved_2 | 24 | Byte | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Schedule_3_ |
2 | Integer | None |
Reserved_3 | 24 | Byte | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Schedule_4_ |
2 | Integer | None |
Reserved_5 | 79 | Byte | None |
CRC | 2 | Byte | None |
v3/v4/v5 Set Seasons
Define the start month and day for each of four seasons and assign a period to each.
Request Example:
01 57 31 02 30 30 38 30 28 (2 byte Season_1_Start_Month) (2 byte Season_1_Start_Day) (2 byte Season_1_Schedule) (2 byte Season_2_Start_Month) (2 byte Season_2_Start_Day) (2 byte Season_2_Schedule) (2 byte Season_3_Start_Month) (2 byte Season_3_Start_Day) (2 byte Season_3_Schedule) (2 byte Season_4_Start_Month) (2 byte Season_5_Start_Day) (2 byte Season_4_Schedule (2 byte CRC)
Parameter | Description |
---|---|
Season_?_ |
2 byte Season_(1,2,3,4)_Start_Month. 31 31 = 11 (01-12) |
Season_?_ |
2 byte Season_(1,2,3,4)_Start_Day. 30 31 = 01 (01-31) |
Season_?_ |
2 byte Season_(1,2,3,4)_Period. 30 37 = 07 (00-07) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Set Holidays
Set holiday table on meter. The holiday dates block contains the month and day for up to 20 holiday dates.
Request Example:
01 57 31 02 30 30 42 30 28 (2 byte Holiday_1_Month) (2 byte Holiday_1_Day) (2 byte Holiday_2_Month) (2 byte Holiday_2_Day) (2 byte Holiday_3_Month) (2 byte Holiday_3_Day) (2 byte Holiday_4_Month) (2 byte Holiday_4_Day) (2 byte Holiday_5_Month) (2 byte Holiday_5_Day) (2 byte Holiday_6_Month) (2 byte Holiday_6_Day) (2 byte Holiday_7_Month) (2 byte Holiday_7_Day) (2 byte Holiday_8_Month) (2 byte Holiday_8_Day) (2 byte Holiday_9_Month) (2 byte Holiday_9_Day) (2 byte Holiday_10_Month) (2 byte Holiday_10_Day) (2 byte Holiday_11_Month) (2 byte Holiday_11_Day) (2 byte Holiday_12_Month) (2 byte Holiday_12_Day) (2 byte Holiday_13_Month) (2 byte Holiday_13_Day) (2 byte Holiday_14_Month) (2 byte Holiday_14_Day) (2 byte Holiday_15_Month) (2 byte Holiday_15_Day) (2 byte Holiday_16_Month) (2 byte Holiday_16_Day) (2 byte Holiday_17_Month) (2 byte Holiday_17_Day) (2 byte Holiday_18_Month) (2 byte Holiday_18_Day) (2 byte Holiday_19_Month) (2 byte Holiday_19_Day) (2 byte Holiday_20_Month) (2 byte Holiday_20_Day) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Holiday_?_ |
2 byte Holiday_(1-20)_Month. 31 31 = 11 (01-12) |
Holiday_?_ |
2 byte Holiday_(1-20)_Day. 30 31 = 01 (01-31) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Read Holiday Dates
Serial call to read holidays block from meter.
Request Example:
01 52 31 02 30 30 42 30 03 (2 byte CRC)
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_ |
6 | Byte | None |
Holiday_1_Mon | 2 | Integer | None |
Holiday_1_Day | 2 | Integer | None |
Holiday_2_Mon | 2 | Integer | None |
Holiday_2_Day | 2 | Integer | None |
Holiday_3_Mon | 2 | Integer | None |
Holiday_3_Day | 2 | Integer | None |
Holiday_4_Mon | 2 | Integer | None |
Holiday_4_Day | 2 | Integer | None |
Holiday_5_Mon | 2 | Integer | None |
Holiday_5_Day | 2 | Integer | None |
Holiday_6_Mon | 2 | Integer | None |
Holiday_6_Day | 2 | Integer | None |
Holiday_7_Mon | 2 | Integer | None |
Holiday_7_Day | 2 | Integer | None |
Holiday_8_Mon | 2 | Integer | None |
Holiday_8_Day | 2 | Integer | None |
Holiday_9_Mon | 2 | Integer | None |
Holiday_9_Day | 2 | Integer | None |
Holiday_10_Mon | 2 | Integer | None |
Holiday_10_Day | 2 | Integer | None |
Holiday_11_Mon | 2 | Integer | None |
Holiday_11_Day | 2 | Integer | None |
Holiday_12_Mon | 2 | Integer | None |
Holiday_12_Day | 2 | Integer | None |
Holiday_13_Mon | 2 | Integer | None |
Holiday_13_Day | 2 | Integer | None |
Holiday_14_Mon | 2 | Integer | None |
Holiday_14_Day | 2 | Integer | None |
Holiday_15_Mon | 2 | Integer | None |
Holiday_15_Day | 2 | Integer | None |
Holiday_16_Mon | 2 | Integer | None |
Holiday_16_Day | 2 | Integer | None |
Holiday_17_Mon | 2 | Integer | None |
Holiday_17_Day | 2 | Integer | None |
Holiday_18_Mon | 2 | Integer | None |
Holiday_18_Day | 2 | Integer | None |
Holiday_19_Mon | 2 | Integer | None |
Holiday_19_Day | 2 | Integer | None |
Holiday_20_Mon | 2 | Integer | None |
Holiday_20_Day | 2 | Integer | None |
Weekend_ |
2 | Integer | None |
Holiday_ |
2 | Integer | None |
Reserved_1 | 163 | Byte | None |
CRC | 2 | Byte | None |
v3/v4/v5 Set Schedule for Weekend and Holiday
Set the schedule for weekend and holiday schedules.
Request Example:
01 57 31 02 30 30 43 30 28 (2 byte Weekend_Schedule) (2 byte Holiday_Schedule) 29 03 (2 byte CRC)
Parameter | Description |
---|---|
Weekend_Schedule | 2 byte Weekend_Schedule. 30 34 = 04 (00-07) |
Holiday_Schedule | 2 byte Holiday_Schedule. 30 32 = 02 (00-07) |
CRC | 2 byte CRC |
Response: True on completion with 06 return
v3/v4/v5 Read 6 Months
Serial call to read months block from meter.
Request Example:
01 52 31 02 30 30 31 (2 byte kWh_Type) 03 (2 byte CRC)
Parameter | Description |
---|---|
kWh_Type | 2 byte kWh_Type. 31 = kWh Total 2 byte kWh_Type. 32 = kWh Reverse |
CRC | 2 byte CRC |
Response Table:
Field Name | Byte Length | Type | Scale Type |
---|---|---|---|
Reserved_ |
6 | Byte | None |
Month_1_Ttl | 8 | Float | kWh_Scale |
Month_1_ |
8 | Float | kWh_Scale |
Month_1_ |
8 | Float | kWh_Scale |
Month_1_ |
8 | Float | kWh_Scale |
Month_1_ |
8 | Float | kWh_Scale |
Month_2_Ttl | 8 | Float | kWh_Scale |
Month_2_ |
8 | Float | kWh_Scale |
Month_2_ |
8 | Float | kWh_Scale |
Month_2_ |
8 | Float | kWh_Scale |
Month_2_ |
8 | Float | kWh_Scale |
Month_3_Ttl | 8 | Float | kWh_Scale |
Month_3_ |
8 | Float | kWh_Scale |
Month_3_ |
8 | Float | kWh_Scale |
Month_3_ |
8 | Float | kWh_Scale |
Month_3_ |
8 | Float | kWh_Scale |
Month_4_Ttl | 8 | Float | kWh_Scale |
Month_4_ |
8 | Float | kWh_Scale |
Month_4_ |
8 | Float | kWh_Scale |
Month_4_ |
8 | Float | kWh_Scale |
Month_4_ |
8 | Float | kWh_Scale |
Month_5_Ttl | 8 | Float | kWh_Scale |
Month_5_ |
8 | Float | kWh_Scale |
Month_5_ |
8 | Float | kWh_Scale |
Month_5_ |
8 | Float | kWh_Scale |
Month_5_ |
8 | Float | kWh_Scale |
Month_6_Ttl | 8 | Float | kWh_Scale |
Month_6_ |
8 | Float | kWh_Scale |
Month_6_ |
8 | Float | kWh_Scale |
Month_6_ |
8 | Float | kWh_Scale |
Month_6_ |
8 | Float | kWh_Scale |
Reserved_1 | 7 | Byte | None |
CRC | 2 | Byte | None |
ekmmeters.py API
Sending and Parsing Requests using ekmmeters.py
'''
Python version: 3.8.10
Modules:
Pyserial version: 3.4
Requires pyserial module:
pip3 install pyserial
'''
# import ekmmeters module
import sys
from ekmmeters import *
# In this code example we make use ekmmeters.py
# with some simple calls to the api you can manage all your meter request
# Open connection to serial port
# Change /dev/ttyUSB0 to match your system
sp = SerialPort("/dev/ttyUSB0", 9600)
if sp.initPort() is False:
print ("Port cannot initialize")
sys.exit()
# Meter Number and Password
# Default password: 00000000
METER="000300001184"
PASSWORD="00000000"
# In the example we are connecting to a v4 meter
# so we use V4Meter(). If you're connecting to a v3
# meter use V3Meter()
meterV4 = V4Meter(METER)
meterV4.attachPort(sp)
print("So let's give it a try...")
print("First we open the connection to the meter")
print("We use request() it will return a json object")
print("that contains both meter data A and B responses")
if meterV4.request():
print(meterV4.jsonRender(meterV4.getReadBuffer())+"\n")
else:
print("Request failed\n")
print("Or request things like the last 6 months of total kwh and reverse kwh")
if meterV4.readMonthTariffs(ReadMonths.kWh):
mon_buf_kwh = meterV4.getMonthsBuffer(ReadMonths.kWh)
print(meterV4.jsonRender(mon_buf_kwh))
else:
print("Request failed\n")
if meterV4.readMonthTariffs(ReadMonths.kWhReverse):
mon_buf_rev_kwh = meterV4.getMonthsBuffer(ReadMonths.kWhReverse)
print(meterV4.jsonRender(mon_buf_rev_kwh))
else:
print("Request failed\n")
print("When requesting information from the meter (reads)")
print("you can send as many requests as you like once you sent")
print("the first connection request (Request A)\n\n")
print("Now let's try sending a setting to the meter")
print("Unlike reading information from the meter")
print("You have to send the connection string and password")
print("The ekmmeters.py API automaticly takes care of this for you")
print("before each setting request\n\n")
print("Now we will loop 3 times opening and closing the relays")
for x in range(1,3):
print("Open Relay A")
if meterV4.setRelay(0, Relay.Relay1, RelayState.RelayOpen, PASSWORD):
print("OK\n")
else:
print("Request failed\n")
print("Close Relay A")
if meterV4.setRelay(0, Relay.Relay1, RelayState.RelayClose, PASSWORD):
print("OK\n")
else:
print("Request failed\n")
print("Open Relay B")
if meterV4.setRelay(0, Relay.Relay2, RelayState.RelayOpen, PASSWORD):
print("OK\n")
else:
print("Request failed\n")
print("Close Relay B")
if meterV4.setRelay(0, Relay.Relay2, RelayState.RelayClose, PASSWORD):
print("OK\n")
else:
print("Request failed\n")
print("When you're finished you will close the connection")
meterV4.serialPostEnd()
# Close connection to serial port
sp.closePort()
If you are coding in Python you can also make use of our ekmmeters.py API
You can find it here: https://github.com/ekmmetering/ekmmeters
Run the following command from your *nix shell in an empty directory:
git clone https://github.com/ekmmetering/ekmmeters .
A required Python module is pyserial, so make sure to run:
pip install pyserial
or pip3 install pyserial
We have created a unittest script named: unittest-ekmmeters.py
To use it, edit the unittest.ini file and update it to match your configuration.
You will also find standard python documentation for the module in two places:
Here: http://ekmmeters.readthedocs.org/en/latest/
Or located in the docs dir.
To build the documentation inside the ekmmeters/docs directory:
cd ekmmeters/docs
pip install sphinx
pip install sphinxcontrib.divparams
pip install bs4
pip install sphinxcontrib-plantuml
pip install sphinxtheme.readability
make html
This will build html docs which will be located in the ekmmeters/docs/_build/html dir.
Just run “make” to see what other document build options are available.
Status Codes
EKM uses conventional HTTP response codes in addition to our API Status Codes to indicate the success or failure of an API request. In general, codes in the 2xx range indicate success, codes in the 4xx range indicate an error that failed given the information provided, and codes in the 5xx range indicate an error with EKM’s servers (these are rare).
The EKM API uses the following API specific Status codes:
API Status Code | Meaning |
---|---|
1000 | There is not data for: {meters} |
1001 | There is not data for: {meters} between ${startDate} and ${endDate} |
1002 | There is not data for: {meters} This meter first reported on: |
1003 | There is not data for: {meters} This meter last reported on: |
1004 | Timezone is not supported for this date range request |
1100 | All invalid query requests |
1101 | Invalid format |
1102 | Invalid value |
1103 | Invalid parameter |
1200 | Found invalid meter: {meter} for key: MTAxMDoyMDIw |
1300 | Oops! It looks like there was an unexpected error. Try checking your query for issues or hold tight while we see what we can do. This error has been reported. |
The EKM API also uses the following HTTP Status codes:
HTTP Status Code | Meaning |
---|---|
200 | OK – The request succeeded. |
400 | Bad Request – The server could not understand the request due to invalid syntax. |
401 | Unauthorized – Although the HTTP standard specifies “unauthorized”, semantically this response means “unauthenticated”. That is, the client must authenticate itself to get the requested response. |
403 | Forbidden – The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give the requested resource. Unlike 401 Unauthorized, the client’s identity is known to the server. |
404 | Not Found – The server can not find the requested resource. In the browser, this means the URL is not recognized. In an API, this can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web. |
429 | Too Many Requests – The user has sent too many requests in a given amount of time (“rate limiting”). |
500 | Internal Server Error – The server has encountered a situation it does not know how to handle. |
501 | Not Implemented – The Vertical Bar otherwise known as “pipe” request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are GET and HEAD. |
502 | Bad Gateway – This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response. |
503 | Service Unavailable – The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the estimated time before the recovery of the service. The webmaster must also take care about the caching-related headers that are sent along with this response, as these temporary condition responses should usually not be cached. |
504 | Gateway Timeout – This error response is given when the server is acting as a gateway and cannot get a response in time. |
505 | HTTP Version Not Supported – The HTTP version used in the request is not supported by the server. |
506 | Variant Also Negotiates – The server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. |
510 | Not Extended – Further extensions to the request are required for the server to fulfill it. |
511 | Network Authentication Required – Indicates that the client needs to authenticate to gain network access. |
Widget Documentation
Click here to go to Widget Documentation
Access free web-based real-time and historical graphs showcasing your EKM Push data, covering all your devices including meters and ioStacks. It’s quick, easy, and doesn’t require a login.
The EKM Widget offers a convenient means to gain visual insights into your systems’ performance. We utilize it to monitor our office’s current consumption and solar power generation in real-time. Additionally, the historical chart helps us track the impact of efficiency upgrades on our usage and monitor the energy consumption of our electric car over time.