📜 ⬆️ ⬇️

Energy accounting as part of the shopping center SCADA system

Let me tell you how I wrote a console program for taking readings from counters Mercury 230 in a shopping center. Link to the source at the end of the article.

It all began with the SCADA system. We were lucky to get a project for the development and implementation of SCADA for a shopping center. Well, how lucky ... In general, the development of this system is a separate story, which, if it is interesting to the reader, I will tell and share the stuffed cones. Yes, and the system has not yet been fully accepted, therefore, until we shine with details.

Briefly:
')
- temperature control in the premises;
- centralized ventilation control on schedule;
- management of DHW feed;
- accounting of water and electricity;
- sms notifications.

This article is about electricity.

Electricians listened to the choice of model, so I didn’t have any particular problems here. Decided to stay on Mercury. For 3 phases - 234 types (electricians bought 230) for single-phase network 206th model. Further, it turned out that electricians forged only three-phase meters throughout the shopping center. Well, I just have less problems. Although I do not understand why.

Before that, I programmed mainly PLC and small C # scripts. But then I decided that I could make energy accounting myself (I wanted to gain experience in programming). I got excited, of course.

The idea was:

- the interrogator-server keeps the energy account in the database;
- SCADA system is responsible for visualization

Polling method


The survey is implemented simply - in an infinite loop on a single RS485 port. In general, for the operation of the dispatching system, I picked up MOXA UPORT 1650-16. Under the survey gave only one port, but in order not to create a star (for RS-485 this is not desirable), he used a repeater. It is strange that bourgeois repeaters RS-485 with a large number of inputs was not found. However, there was a domestic Tahion PRT-1/8 (12) gizmo at 12 ports. If it works out, I'll post a video of the work of the whole bunch. Works good. Only to MOXA UPORT 1650-16 there are claims, but that’s another story.


Development of the surveyor himself


About design patterns did not even hear. Because I made mistakes right away, because carried away inheritance (and not particularly well). On smart books - it was necessary to apply the composition. In the future, it will be necessary to recycle the entire library.

The chain of inheritance turned out this:

MeterDevice -> Mercury230 -> Mercury230_DatabaseSignals 

MeterDevice is a common class for all counters. It implements the exchange via a COM port with Modbus with a similar protocol;
Mercury230 - a class with a set of polling functions for a specific counter;
Mercury230_DatabaseSignals is a class with specific meter parameters (currents, voltages, etc.) and the function of updating them. Inheritance in it was not a convenient solution. Since Then I had problems with serialization and deserialization of objects. But this class so firmly got into the code that it was impossible to retreat.

I decided to make the RXmes structure key in the polling functions. In it to store result of the answer and an array of bytes of the answer. Any polling function (for example, a serial number request) operates within itself with this structure:

 public enum error_type : int { none = 0, AnswError = -5, //       CRCErr = -4, NoAnsw = -2, //         WrongId = -3, //     NoConnect = -1 //   }; public struct RXmes { public error_type err; public byte[] buff; public byte[] trueCRC; public void testCRC() { err = error_type.CRCErr; if (buff.Length < 4) { err = error_type.CRCErr; return; } byte[] newarr = buff; Array.Resize(ref newarr, newarr.Length - 2); byte[] trueCRC = Modbus.Utility.ModbusUtility.CalculateCrc(newarr); if ((trueCRC[1] == buff.Last()) && (trueCRC[0] == buff[(buff.Length - 2)])) { err = error_type.none; } } public void ReadArr(byte[] b) { buff = b; testCRC(); } } public RXmes SendCmd(byte[] data) { RXmes RXmes_ = new RXmes(); byte[] crc = Modbus.Utility.ModbusUtility.CalculateCrc(data); Array.Resize(ref data, data.Length + 2); data[data.Length - 2] = crc[0]; data[data.Length - 1] = crc[1]; rs_port.Write(data, 0, data.Length); System.Threading.Thread.Sleep(timeout); if (rs_port.BytesToRead > 0) { byte[] answer = new byte[(int)rs_port.BytesToRead]; rs_port.Read(answer, 0, rs_port.BytesToRead); RXmes_.ReadArr(answer); if (RXmes_.err == error_type.none) { DataTime_last_contact = DateTime.Now; } return RXmes_; } RXmes_.err = error_type.NoConnect; return RXmes_; } 

Thus, the function of obtaining the serial number of the Mercury 230 counter turned out to be this:

 public byte[] GiveSerialNumber() { byte[] mes = {address, 0x08 , 0}; RXmes RXmes = SendCmd(mes); if (RXmes.err == error_type.none) { byte[] bytebuf = new byte[7]; Array.Copy(RXmes.buff, 1, bytebuf, 0, 7); return bytebuf; } return null; } 

Who is interested to see other functions - you can see the source.

SCADA communication protocol


Initially, the simple TCP protocol of the north was simple. SCAD's answer over TCP looked for Mercury 230 of that.

"Type = mecc230 * add = 23 * volt = 1: 221-2: 221-3: 221 * cur = 1: 1.2-2: 1.2-3: 1.2"

Scud data parsed and displayed on the icon of the corresponding counter
Everything would be fine, but the customer decided (and rested his horn) that he needed all the data in a tabular form. Yes, and wanted to set the limits of all parameters during operation. And going beyond the limits should be displayed.

Tearing your hair, because SCADA did not know how to display tabular data, sat down to write a separate program for visualization.

Its protocol was already becoming particularly inconvenient, since the number of parameters grew. For example, for the current appeared the upper chapel, the state of the accident, the hysteresis of the inclusion of the accident.
It turned out that a separate class was formed for the parameters:

 public MetersParameter() { minalarm = false; maxalarm = false; } public MetersParameter(float min, float max, float hist, float scalefactor = 1) { MinValue = min; MaxValue = max; Hist = hist; minalarm = false; maxalarm = false; ScalingFactor = scalefactor; } public string alias{set; get;} public float MaxValue { set; get; } public float MinValue { set; get; } public float ScalingFactor { set; get; } //  .       public float Hist { set; get; } private bool minalarm; private bool maxalarm; public bool ComAlarm { get { return MinValueAlarm || MaxValueAlarm ; } } public virtual bool MinValueAlarm { get{ return minalarm; } } public virtual bool MaxValueAlarm { get{ return maxalarm; } } public virtual void RefreshData() { if (null != ParametrUpdated) { ParametrUpdated(); } if ((MinValue == 0) && (MaxValue == 0)) { return; } float calc_par = parametr * ScalingFactor; if (calc_par < (MinValue - Hist)) { minalarm = true; } if (calc_par > (MinValue + Hist)) { minalarm = false; } if (calc_par < (MaxValue - Hist)) { maxalarm = false; } if (calc_par > (MaxValue + Hist)) { maxalarm = true; } } float parametr; public bool UseScaleForInput = false; public virtual float Value { set{ parametr = UseScaleForInput ? value / (ScalingFactor <= 0 ? 1 : ScalingFactor) : value; RefreshData(); } get { return parametr * ScalingFactor; } } public void CopyLimits(MetersParameter ext_par) { this.MinValue = ext_par.MinValue; this.MaxValue = ext_par.MaxValue; this.Hist = ext_par.Hist; } } 

Here I got the serialization of objects. Having tried Byte, XML and JSON serialization, it was decided to stop at JSON (DataContractJsonSerializer). It was easy to read by eye, the data volume was less than XML. In general, DataContractJsonSerializer forgave the absence of a constructor with no arguments. It made life much easier.

Database


Of course, the most important thing was the recording of meter readings. Since Scada system worked with MySql, then the surveyor was decided to tie with her. There were no special problems.

The question was only one - “what data to record?”, Because options counter gives quite a few. Actually codes for the request:

 public enum peroidQuery : byte { afterReset = 0x0, thisYear = 1, lastYear = 2, thisMonth = 3, thisDay = 4, lastDay = 5, thisYear_beginning = 9, lastYear_beginning = 0x0A, thisMonth_beginning = 0x0B, thisDay_beginning = 0x0C, lastDay_beginning = 0x0D } 

Initially, it was decided to record consumption per month and per day. In addition, a simple mechanism for taking readings by months for a year was implemented. And control the availability of this data. If there was not enough data, they were added.

But closer to the end, I get the requirement that the reports always have written indications at the beginning and at the end of the period. People hired for maintenance didn’t like one “Consumed” column. I had to add readings at the beginning of the month and day.

Total


It turned out a little crutch, but still a working program of electricity metering. At the moment, the program interrogates about 70 counters. The console application is spinning on the server, and the client side runs on the user's workplace.

The source of the surveyor is posted on GitHub . I will try to post the link to the client part later.

PS About the similarity of the protocol of Mercury 230 and SET-4tm


If someone did not come across. That is such a plant them. Frunze (in Nizhny Novgorod). And the meters work with a very similar protocol. Run through the manuals of both - one to one. But, I heard that there are some differences in the protocols (I have not yet gone into it). It is a pity that on the hands of no SET.
Legs of similarity grow from the fact that the Mercury was developed by the former workers of Frunze. So it goes. It is strange why Mercury is more popular at the hearing.

Source: https://habr.com/ru/post/330980/


All Articles