pzem016 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '030915a891a9651cde14fcecdca0c8aec9683b3a58fbe36d20913ac5e4603935'
4
+ data.tar.gz: b1d4db0b9982a05e173420f63056272ef298eee1bbf3b01a120800589785c50a
5
+ SHA512:
6
+ metadata.gz: 72d7704b3335514d3c011c2211022b676c4dd5f5764a4a5a581835fdf714c175b0c5bdc7cbe09b9fc11a2410528f77e09487b18d669304f36654df6a279c96a2
7
+ data.tar.gz: 26eeb29d6d1893b83145d791d1c0241af11cacf9679d2878d207732be418713935bbcc15e8489fb9fc0a54f00141b2dbc26d96051a5911bcc26ed6c27cfa995d
data/CHANGELOG ADDED
File without changes
data/PZEM016-bench.jpg ADDED
Binary file
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Pzem016
2
+
3
+ Pzem016 is some Ruby support for some very cheap but decent power monitors you can get from AliExpress
4
+
5
+ These things: https://www.aliexpress.com/w/wholesale-pzem%2525252d016.html?spm=a2g0o.best.search.0
6
+
7
+ This gem provides a library to wrap some of the modbus details, and a cli so you can interact
8
+ with them right away without having to write code.
9
+
10
+ You'll need an RS-485 adapter of some sort - I have had good luck with https://www.amazon.com/dp/B076WVFXN8?ref=ppx_yo2ov_dt_b_product_details&th=1
11
+
12
+ Wiring is very simple. For a single point to point it's just A<->A, B<->B, GND<->GND. You can have more than one node on the bus. If you intend
13
+ to do that you'll need to look at doing some impedance matching with terminating resistors at both ends.
14
+
15
+ ![RS-485 wiring](PZEM016-bench.jpg)
16
+
17
+ ## Installation
18
+
19
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
20
+
21
+ Install the gem and add to the application's Gemfile by executing:
22
+
23
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
24
+
25
+ If bundler is not being used to manage dependencies, install the gem by executing:
26
+
27
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
28
+
29
+ ## Usage
30
+
31
+ Library documentation is here: [TODO]
32
+
33
+ Example of output:
34
+ ![sample usage](sampleoutput.png)
35
+
36
+ The command line tool's cli syntax uses subcommands:
37
+
38
+ ```
39
+ pzem016.rb --help
40
+ PZEM-016 power measurement module utility.
41
+
42
+ Syntax: pzem016.rb [global options] <command> [options]
43
+
44
+ Commands: ["read", "readconfig", "setconfig", "resetenergy", "scanbus"]
45
+
46
+ Global options:
47
+ -p, --port=<s> Serial port (default: /dev/ttyUSB0)
48
+ -s, --speed=<i> Port speed (default: 9600)
49
+ -t, --timeout=<i> Retry timeout (default: 1)
50
+ -r, --retries=<i> Number of times to retry ModBus command (default: 1)
51
+ -n, --name=<s> Device name (default: PZEM-016 Device)
52
+ -d, --debug Extra noise
53
+ -h, --help Show this message
54
+ ```
55
+
56
+ ```
57
+ pzem016.rb read --help
58
+ Read power values from specified device
59
+ -s, --sladdr=<i> Slave address (default: 1)
60
+ -j, --json Output JSON
61
+ -h, --help Show this message
62
+ ```
63
+
64
+ ```
65
+ pzem016.rb readconfig --help
66
+ Read config values from specified device
67
+ -s, --sladdr=<i> Slave address (default: 1)
68
+ -j, --json Output JSON
69
+ -h, --help Show this message
70
+ ```
71
+
72
+ ```
73
+ pzem016.rb setconfig --help
74
+ Set device address and/or alarm threshold: pzem016.rb setconfig <addr|alarm> <value>
75
+ -s, --sladdr=<i> Slave address (default: 1)
76
+ -h, --help Show this message
77
+ ```
78
+
79
+ ```
80
+ pzem016.rb resetenergy --help
81
+ Reset (clear) the energy counter (KWH) on the specified device
82
+ -s, --sladdr=<i> Slave address (default: 1)
83
+ -h, --help Show this message
84
+ ```
85
+
86
+ ```
87
+ pzem016.rb scanbus --help
88
+ Scan ModBus for nodes/addrs
89
+ -r, --read Try to read values when node is found
90
+ -h, --help Show this message
91
+ ```
92
+
93
+
94
+ ## Development
95
+
96
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
99
+
100
+ ## Contributing
101
+
102
+ Bug reports and pull requests are welcome on GitLab at https://gitlab.com/svdasein/pzem016
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: %i[]
data/exe/pzem016.rb ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/pzem016'
5
+ require 'optimist'
6
+ require 'ruby-progressbar'
7
+
8
+ progname = File.basename($PROGRAM_NAME)
9
+ commands = %w[read readconfig setconfig resetenergy scanbus]
10
+
11
+ global_opts = Optimist.options do
12
+ banner "PZEM-016 power measurement module utility.\n\nSyntax: #{progname} [global options] <command> [options]\n\nCommands: #{commands}\n\nGlobal options:"
13
+ opt :port, 'Serial port', type: :string, default: '/dev/ttyUSB0'
14
+ opt :speed, 'Port speed', type: :integer, default: 9600
15
+ opt :timeout, 'Retry timeout', type: :integer, default: 1
16
+ opt :retries, 'Number of times to retry ModBus command', default: 1
17
+ opt :name, 'Device name', type: :string, default: 'PZEM-016 Device'
18
+ opt :debug, 'Extra noise', type: :boolean
19
+ stop_on commands
20
+ end
21
+
22
+ cmd = ARGV.empty? ? '(not given)' : ARGV.shift.downcase.to_sym
23
+
24
+ cmd_opts = case cmd
25
+ when :read, :readconfig # parse delete options
26
+ Optimist.options do
27
+ banner "Read #{cmd == :read ? "power" : "config"} values from specified device"
28
+ opt :sladdr, 'Slave address', type: :integer, default: 1
29
+ opt :json, 'Output JSON', type: :boolean, default: false
30
+ end
31
+ when :setconfig
32
+ opts = Optimist.options do
33
+ banner "Set device address and/or alarm threshold: #{progname} setconfig <addr|alarm> <value>"
34
+ opt :sladdr, 'Slave address', type: :integer, default: 1
35
+ end
36
+ if ARGV.size == 2
37
+ setting = ARGV.shift.downcase.to_sym
38
+ Optimist.die "Specify one of 'addr' or 'alarm'; you gave '#{setting}'" unless %i[addr alarm].include?(setting)
39
+ value = ARGV.shift.to_i
40
+ else
41
+ Optimist.die "syntax #{progname} setconfig <addr|alarm> <value>"
42
+ end
43
+ opts
44
+ when :resetenergy
45
+ Optimist.options do
46
+ banner 'Reset (clear) the energy counter (KWH) on the specified device'
47
+ opt :sladdr, 'Slave address', type: :integer, default: 1
48
+ end
49
+ when :scanbus
50
+ Optimist.options do
51
+ banner 'Scan ModBus for nodes/addrs'
52
+ opt :read, 'Try to read values when node is found', type: :boolean
53
+ end
54
+ else
55
+ puts "Valid commands: #{commands}"
56
+ Optimist.die "unknown command: #{cmd}"
57
+ end
58
+
59
+ if global_opts[:debug]
60
+ puts "Global options: #{global_opts.inspect}"
61
+ puts "Subcommand: #{cmd.inspect}"
62
+ puts "Subcommand options: #{cmd_opts.inspect}"
63
+ puts "Remaining arguments: #{ARGV.inspect}"
64
+ end
65
+
66
+ bus = ModBus::RTUClient.connect(global_opts[:port], global_opts[:speed])
67
+
68
+ begin
69
+ case cmd
70
+ when :read
71
+ bus.with_slave(cmd_opts[:sladdr]).tap do |slave|
72
+ slave.read_retry_timeout = global_opts[:timeout]
73
+ slave.read_retries = global_opts[:retries]
74
+ input = Pzem016::InputRegisters.new(global_opts[:name], slave)
75
+ input.read
76
+ if cmd_opts[:json]
77
+ puts input.to_json
78
+ else
79
+ input.report
80
+ end
81
+ end
82
+ when :readconfig
83
+ bus.with_slave(cmd_opts[:sladdr]).tap do |slave|
84
+ slave.read_retry_timeout = global_opts[:timeout]
85
+ slave.read_retries = global_opts[:retries]
86
+ holding = Pzem016::HoldingRegisters.new(global_opts[:name], slave)
87
+ holding.read
88
+ if cmd_opts[:json]
89
+ puts holding.to_json
90
+ else
91
+ holding.report
92
+ end
93
+ end
94
+ when :setconfig
95
+ slave = bus.with_slave(cmd_opts[:sladdr])
96
+ slave.read_retry_timeout = global_opts[:timeout]
97
+ slave.read_retries = global_opts[:retries]
98
+ device = Pzem016::HoldingRegisters.new(global_opts[:name], slave)
99
+ case setting
100
+ when :addr
101
+ device.set_address(value)
102
+ puts "Address of slave at '#{cmd_opts[:sladdr]}' has been changed to '#{value}'"
103
+ when :alarm
104
+ device.set_alarm(value)
105
+ puts "Alarm threshold value for slave at #{cmd_opts[:sladdr]} has been set to #{value} watts"
106
+ end
107
+ when :resetenergy
108
+ puts "Slave at addr #{cmd_opts[:sladdr]} energy counter reset: #{Pzem016::HoldingRegisters.new(global_opts[:name],
109
+ bus.with_slave(cmd_opts[:sladdr])).reset_energy}"
110
+ when :scanbus
111
+ begin
112
+ (1..247).each do |address|
113
+ bus.with_slave(address) do |slave|
114
+ slave.read_retry_timeout = global_opts[:timeout]
115
+ slave.read_retries = global_opts[:retries]
116
+ begin
117
+ value = slave.read_holding_registers(0, 3)
118
+ puts "\nDevice found at address #{value[2]}\n"
119
+ if cmd_opts[:read]
120
+ input = Pzem016::InputRegisters.new(global_opts[:name], slave)
121
+ input.read
122
+ input.report
123
+ puts
124
+ end
125
+ rescue StandardError
126
+ print '.'
127
+ end
128
+ end
129
+ end
130
+ puts "\n"
131
+ rescue Interrupt
132
+ puts "\nscan interrupted\n"
133
+ end
134
+ else
135
+ puts "Command required, one of #{commands}"
136
+ end
137
+ rescue ModBus::Errors::ModBusTimeout
138
+ puts 'ModBus timed out - are you sure you specified the correct --sladdr?'
139
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # This module is a container for a few classes that wrap a bunch
5
+ # of ModBus related stuff
6
+ #
7
+ module Pzem016
8
+ require 'json'
9
+ require 'awesome_print'
10
+
11
+ ##
12
+ # Some abstract behavior for the register parser classes
13
+ #
14
+ class ModbusRegisters
15
+ attr_reader :raw, :slave
16
+
17
+ ##
18
+ # This takes two params: name and slave. Name is a descriptive string
19
+ # for the slave. Slave is an instance of a ModBus::RTUClient slave
20
+ # that you've previously set up.
21
+ #
22
+ def initialize(name, slave)
23
+ @slave = slave
24
+ @name = name
25
+ @raw = nil
26
+ end
27
+
28
+ ##
29
+ # Stub for read logic
30
+ #
31
+ def read
32
+ @raw = []
33
+ end
34
+
35
+ ##
36
+ # Simple basic behavior for turning the data read into a hash
37
+ #
38
+ def to_hash
39
+ { raw: @raw }
40
+ end
41
+
42
+ ##
43
+ # Returns the sample data as a json string
44
+ #
45
+ def to_json(*_args)
46
+ to_hash.to_json
47
+ end
48
+
49
+ ##
50
+ # Outputs a pretty dump of the readings to STDOUT
51
+ #
52
+ def report
53
+ ap to_hash
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Parsing logic for the "holding registers", also
59
+ # known as read/write. PZEM-016 is configured via these
60
+ #
61
+ class HoldingRegisters < ModbusRegisters
62
+ ##
63
+ # Specific holding register read logic for PZEM-016
64
+ #
65
+ def read
66
+ @raw = @slave.holding_registers[0..6]
67
+ end
68
+
69
+ ##
70
+ # Sets the address of the slave to the new address addr
71
+ #
72
+ def set_address(addr)
73
+ @slave.write_single_register(2, addr)
74
+ end
75
+
76
+ ##
77
+ # Sets the alarm threshold to the given number of watts
78
+ def set_alarm(watts)
79
+ @slave.write_single_register(1, watts)
80
+ end
81
+
82
+ ##
83
+ # Clears / resets the KWH (energy) counter on the PZEM-016
84
+ #
85
+ def reset_energy
86
+ @slave.query("\x42")
87
+ rescue ModBus::Errors::IllegalFunction
88
+ # the units respond with an error but in fact do reset the counter
89
+ true
90
+ end
91
+
92
+ ##
93
+ # Specific logic for generating a hash of the holding registers in a PZEM-016
94
+ #
95
+ def to_hash
96
+ {
97
+ "DeviceName": @name,
98
+ "Address": @raw[2],
99
+ "AlarmWatts": @raw[1]
100
+ }
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Parsing logic for the "input registers", also
106
+ # known as readonly. This is where the sample data
107
+ # shows up
108
+ #
109
+ class InputRegisters < ModbusRegisters
110
+ ##
111
+ # Specific input register read logic for PZEM-016
112
+ #
113
+ def read
114
+ @raw = @slave.input_registers[0..9]
115
+ end
116
+
117
+ ##
118
+ # Extracts measured volts from the sample data
119
+ #
120
+ def volts
121
+ (@raw[0] * 0.1).round(3)
122
+ end
123
+
124
+ ##
125
+ # Extracts measured amps from the sample data
126
+ #
127
+ def amps
128
+ (((@raw[2] << 16) | @raw[1]) * 0.001).round(3)
129
+ end
130
+
131
+ ##
132
+ # Extracts measured watts from the sample data
133
+ #
134
+ def watts
135
+ (((@raw[4] << 16) | @raw[3]) * 0.1).round(3)
136
+ end
137
+
138
+ ##
139
+ # Extracts cumulative watt-hours from the sample data
140
+ #
141
+ def wattHours
142
+ (((@raw[6] << 16) | @raw[5]) * 0.1).round(3)
143
+ end
144
+
145
+ ##
146
+ # Extracts measured frequency from the sample data
147
+ #
148
+ def hz
149
+ (@raw[7] * 0.1).round(3)
150
+ end
151
+
152
+ ##
153
+ # Extracts measured power factor from the sample data
154
+ #
155
+ def powerFactor
156
+ (@raw[8] * 0.01).round(3)
157
+ end
158
+
159
+ ##
160
+ # Returns true of power consumption has reached the alarm number of watts
161
+ #
162
+ def alarm?
163
+ @raw[9] == 0xFFFF
164
+ end
165
+
166
+ ##
167
+ # Specific logic for generating a hash of the input registers in a PZEM-016
168
+ #
169
+ def to_hash
170
+ {
171
+ "DeviceName": @name,
172
+ "Volts": volts,
173
+ "Amps": amps,
174
+ "Watts": watts,
175
+ "WH": wattHours,
176
+ "HZ": hz,
177
+ "PF": powerFactor,
178
+ "Alarm": alarm?
179
+ }
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pzem016
4
+ VERSION = '0.1.0'
5
+ end
data/lib/pzem016.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rmodbus'
4
+ require 'pry' if ENV['RUBY_ENV'] == 'development'
5
+ require_relative 'pzem016/version'
6
+ require_relative 'pzem016/pzem016'
7
+
8
+ module Pzem016
9
+ class Error < StandardError; end
10
+ # Your code goes here...
11
+ end
data/sampleoutput.png ADDED
Binary file
data/sig/pzem016.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Pzem016
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pzem016
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dave Parker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-03-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: awesome_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: optimist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rmodbus
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-progressbar
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: PZEM-016 is a cheap power monitoring thing you can get from aliexpress
70
+ for usually very little money https://www.aliexpress.com/w/wholesale-pzem%2525252d016.html?spm=a2g0o.best.search.0. This
71
+ gem provides a library to talk to them with and some cli utils
72
+ email:
73
+ - dparker@svdasein.org
74
+ executables:
75
+ - pzem016.rb
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - CHANGELOG
80
+ - PZEM016-bench.jpg
81
+ - README.md
82
+ - Rakefile
83
+ - exe/pzem016.rb
84
+ - lib/pzem016.rb
85
+ - lib/pzem016/pzem016.rb
86
+ - lib/pzem016/version.rb
87
+ - sampleoutput.png
88
+ - sig/pzem016.rbs
89
+ homepage: https://gitlab.com/svdasein/pzem016
90
+ licenses: []
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ source_code_uri: https://gitlab.com/svdasein/pzem016
94
+ changelog_uri: https://gitlab.com/svdasein/pzem016/-/blob/master/CHANGELOG
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 3.0.0
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.5.7
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Library and cli utils to interact with PZEM-016 power monitoring units
114
+ test_files: []