Compare commits
18 Commits
92ae0eee50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ff22cf6ec | |||
| b4dd203718 | |||
| 2eaaf8d6d8 | |||
|
|
a5aa6de67b | ||
|
|
d5a4a8c791 | ||
|
|
5b7b913588 | ||
|
|
ca5a4a1b5d | ||
| 9f7f8a4d80 | |||
| 9470623915 | |||
|
|
72cd644528 | ||
| 85eaceaad2 | |||
| b40f4d2dc6 | |||
|
|
24dd351249 | ||
|
|
63b2260ba0 | ||
|
|
211b6e7cac | ||
|
|
968a5cea2f | ||
|
|
c165f24632 | ||
|
|
b303331581 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
.config
|
.config
|
||||||
.project
|
.project
|
||||||
*.pid
|
*.pid
|
||||||
|
config/smtp.yml
|
||||||
|
|||||||
18
Dockerfile
18
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:2.7
|
FROM ruby:3.1
|
||||||
|
|
||||||
ENV BUILD_PACKAGES="apt-utils build-essential curl less nodejs sudo wget zsh libmariadb-dev libserialport-dev cron"
|
ENV BUILD_PACKAGES="apt-utils build-essential curl less nodejs sudo wget zsh libmariadb-dev libserialport-dev cron"
|
||||||
|
|
||||||
@@ -14,8 +14,18 @@ COPY Gemfile Gemfile.lock ./
|
|||||||
RUN \
|
RUN \
|
||||||
apt-get update -qq && \
|
apt-get update -qq && \
|
||||||
apt-get install -y $BUILD_PACKAGES && \
|
apt-get install -y $BUILD_PACKAGES && \
|
||||||
bundle install
|
# Fix bundler version BEFORE bundle install
|
||||||
|
gem install bundler -v "$(grep -A 1 'BUNDLED WITH' Gemfile.lock | tail -n 1)" && \
|
||||||
|
bundle install && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY . .
|
# Copy cron file (if you use /etc/cron.d style)
|
||||||
|
COPY reportcron /etc/cron.d/reportcron
|
||||||
|
RUN chmod 0644 /etc/cron.d/reportcron && touch /var/log/cron.log
|
||||||
|
|
||||||
|
# Add entrypoint script
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["/bin/bash -c ruby ./smartmeter.rb"]
|
|
||||||
|
|||||||
1
Gemfile
1
Gemfile
@@ -12,3 +12,4 @@ gem 'numo-narray'
|
|||||||
gem 'i18n'
|
gem 'i18n'
|
||||||
gem 'gruff'
|
gem 'gruff'
|
||||||
gem 'net-http' # to avoid error: uninitialized constant ...
|
gem 'net-http' # to avoid error: uninitialized constant ...
|
||||||
|
gem 'mqtt'
|
||||||
|
|||||||
67
Gemfile.lock
67
Gemfile.lock
@@ -1,13 +1,13 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
activemodel (7.1.5.1)
|
activemodel (7.1.6)
|
||||||
activesupport (= 7.1.5.1)
|
activesupport (= 7.1.6)
|
||||||
activerecord (7.1.5.1)
|
activerecord (7.1.6)
|
||||||
activemodel (= 7.1.5.1)
|
activemodel (= 7.1.6)
|
||||||
activesupport (= 7.1.5.1)
|
activesupport (= 7.1.6)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activesupport (7.1.5.1)
|
activesupport (7.1.6)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
@@ -20,53 +20,55 @@ GEM
|
|||||||
mutex_m
|
mutex_m
|
||||||
securerandom (>= 0.3)
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
base64 (0.2.0)
|
base64 (0.3.0)
|
||||||
benchmark (0.4.0)
|
benchmark (0.5.0)
|
||||||
bigdecimal (3.1.9)
|
bigdecimal (4.1.2)
|
||||||
concurrent-ruby (1.3.4)
|
concurrent-ruby (1.3.6)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.5.5)
|
||||||
daemons (1.4.1)
|
daemons (1.4.1)
|
||||||
date (3.4.1)
|
date (3.5.1)
|
||||||
drb (2.2.1)
|
drb (2.2.3)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.4.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
fugit (1.11.1)
|
fugit (1.12.1)
|
||||||
et-orbi (~> 1, >= 1.2.11)
|
et-orbi (~> 1.4)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
gruff (0.24.0)
|
gruff (0.24.0)
|
||||||
histogram
|
histogram
|
||||||
rmagick (>= 5.3)
|
rmagick (>= 5.3)
|
||||||
histogram (0.2.4.1)
|
histogram (0.2.4.1)
|
||||||
i18n (1.14.6)
|
i18n (1.14.8)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
logger (1.6.4)
|
logger (1.7.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
minitest (5.26.1)
|
||||||
minitest (5.25.4)
|
mqtt (0.7.0)
|
||||||
|
logger
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
mysql2 (0.5.6)
|
mysql2 (0.5.7)
|
||||||
net-http (0.6.0)
|
bigdecimal
|
||||||
uri
|
net-http (0.9.1)
|
||||||
net-imap (0.4.18)
|
uri (>= 0.11.1)
|
||||||
|
net-imap (0.4.24)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nokogiri (1.15.7)
|
nokogiri (1.15.7-aarch64-linux)
|
||||||
mini_portile2 (~> 2.8.2)
|
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
numo-narray (0.9.2.1)
|
||||||
observer (0.1.2)
|
observer (0.1.2)
|
||||||
pkg-config (1.5.8)
|
pkg-config (1.6.5)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rmagick (5.5.0)
|
rmagick (5.5.0)
|
||||||
@@ -75,12 +77,12 @@ GEM
|
|||||||
rufus-scheduler (3.9.2)
|
rufus-scheduler (3.9.2)
|
||||||
fugit (~> 1.1, >= 1.11.1)
|
fugit (~> 1.1, >= 1.11.1)
|
||||||
securerandom (0.3.2)
|
securerandom (0.3.2)
|
||||||
serialport (1.3.2)
|
serialport (1.4.0)
|
||||||
state_pattern (2.0.2)
|
state_pattern (2.0.2)
|
||||||
timeout (0.4.3)
|
timeout (0.6.1)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
uri (1.0.2)
|
uri (1.1.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@@ -91,6 +93,7 @@ DEPENDENCIES
|
|||||||
gruff
|
gruff
|
||||||
i18n
|
i18n
|
||||||
mail
|
mail
|
||||||
|
mqtt
|
||||||
mysql2
|
mysql2
|
||||||
net-http
|
net-http
|
||||||
nokogiri
|
nokogiri
|
||||||
|
|||||||
18
LICENSE
Normal file
18
LICENSE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 halteren
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
2
Rakefile
2
Rakefile
@@ -11,7 +11,7 @@ namespace :db do
|
|||||||
task :migrate do
|
task :migrate do
|
||||||
connection_details = YAML::load(File.open('config/database.yml'))
|
connection_details = YAML::load(File.open('config/database.yml'))
|
||||||
ActiveRecord::Base.establish_connection(connection_details)
|
ActiveRecord::Base.establish_connection(connection_details)
|
||||||
ActiveRecord::Migrator.migrate("db/migrate/")
|
ActiveRecord::MigrationContext.new("db/migrate/").migrate
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Create the db"
|
desc "Create the db"
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class InSyncState < StatePattern::State
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_frame(frame_lines)
|
def handle_frame(frame_lines)
|
||||||
gas_pattern = /^([0-1:24\.2\.1]+)\((\d{12}[SW])\)\((\d{5}\.\d{3})\*m3\)$/
|
|
||||||
|
|
||||||
# prepare DB record
|
# prepare DB record
|
||||||
last_reading = Reading.last
|
last_reading = Reading.last
|
||||||
@@ -80,8 +79,11 @@ class InSyncState < StatePattern::State
|
|||||||
if line.match(/0-0:96.14.0/) # Hoog/laag tarief
|
if line.match(/0-0:96.14.0/) # Hoog/laag tarief
|
||||||
reading.high_tarif = line.split(/0-0:96.14.0\(|\)/).join.eql?("0002")
|
reading.high_tarif = line.split(/0-0:96.14.0\(|\)/).join.eql?("0002")
|
||||||
end
|
end
|
||||||
if match = line.match(/0-1:24.3.0/) # Gas verbruik (1x per uur een nieuwe stand)
|
# example line: "0-1:24.2.1(250717121000S)(00000.474*m3)"
|
||||||
p "Gas reading: #{match[1]} (#{match[2]})"
|
if match = line.match(/^(0-1:24.2.1)\(([^)]+)\)\(([\d.]+)\*m3\)$/) # Gas verbruik (1x per uur een nieuwe stand)
|
||||||
|
#p "Gas reading: #{match[1]} (#{match[2]})"
|
||||||
|
#datetime = DateTime.strptime(match[2][0..11], "%y%m%d%H%M%S")
|
||||||
|
#p "Gas reading at #{datetime}."
|
||||||
reading.total_m3_gas_consumed = match[3].to_f
|
reading.total_m3_gas_consumed = match[3].to_f
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
@@ -92,15 +94,11 @@ class InSyncState < StatePattern::State
|
|||||||
reading.save
|
reading.save
|
||||||
end
|
end
|
||||||
|
|
||||||
# Write to EmonHub
|
# Post current_kw_consumed and current_kw_produced to MQTT
|
||||||
begin
|
MqttClient.publish("home/energy/current_kw_consumed", reading.current_kw_consumed.to_s)
|
||||||
TCPSocket.open("printserver",5050){|s|
|
MqttClient.publish("home/energy/current_kw_produced", reading.current_kw_produced.to_s)
|
||||||
s.write(sprintf("8 %d %d\r\n", reading.current_kw_consumed*1000, reading.current_kw_produced*1000))
|
|
||||||
}
|
# Result
|
||||||
rescue
|
|
||||||
p "Socket problem."
|
|
||||||
end
|
|
||||||
# Result
|
|
||||||
return reading
|
return reading
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
77
app/helpers/MqttClient.rb
Normal file
77
app/helpers/MqttClient.rb
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
begin
|
||||||
|
require "mqtt"
|
||||||
|
rescue LoadError
|
||||||
|
# Fallback to mosquitto_pub when the mqtt gem is not available.
|
||||||
|
end
|
||||||
|
|
||||||
|
module MqttClient
|
||||||
|
class << self
|
||||||
|
def publish(topic, payload, retain: false, qos: 0)
|
||||||
|
return false if topic.nil? || topic.empty?
|
||||||
|
|
||||||
|
if defined?(MQTT)
|
||||||
|
publish_with_ruby_mqtt(topic, payload, retain, qos)
|
||||||
|
else
|
||||||
|
publish_with_mosquitto_pub(topic, payload, retain, qos)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
warn("[MqttClient] publish failed: #{e.class}: #{e.message}")
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def publish_with_ruby_mqtt(topic, payload, retain, qos)
|
||||||
|
options = {
|
||||||
|
host: mqtt_host,
|
||||||
|
port: mqtt_port,
|
||||||
|
keep_alive: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
options[:username] = mqtt_username if mqtt_username
|
||||||
|
options[:password] = mqtt_password if mqtt_password
|
||||||
|
|
||||||
|
MQTT::Client.connect(options) do |client|
|
||||||
|
client.publish(topic, payload.to_s, retain, qos)
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def publish_with_mosquitto_pub(topic, payload, retain, qos)
|
||||||
|
command = [
|
||||||
|
"mosquitto_pub",
|
||||||
|
"-h", mqtt_host,
|
||||||
|
"-p", mqtt_port.to_s,
|
||||||
|
"-q", qos.to_s,
|
||||||
|
"-t", topic,
|
||||||
|
"-m", payload.to_s
|
||||||
|
]
|
||||||
|
|
||||||
|
command << "-r" if retain
|
||||||
|
|
||||||
|
ok = system(*command)
|
||||||
|
warn("[MqttClient] mosquitto_pub failed") unless ok
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def mqtt_host
|
||||||
|
ENV.fetch("MQTT_HOST", "localhost")
|
||||||
|
end
|
||||||
|
|
||||||
|
def mqtt_port
|
||||||
|
Integer(ENV.fetch("MQTT_PORT", "1883"))
|
||||||
|
rescue ArgumentError
|
||||||
|
1883
|
||||||
|
end
|
||||||
|
|
||||||
|
def mqtt_username
|
||||||
|
v = ENV["MQTT_USERNAME"]
|
||||||
|
v.nil? || v.strip.empty? ? nil : v
|
||||||
|
end
|
||||||
|
|
||||||
|
def mqtt_password
|
||||||
|
ENV["MQTT_PASSWORD"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,8 +6,8 @@ EASY_ENERGY_TARIFFS = {}
|
|||||||
|
|
||||||
# See https://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/zakelijk/overige_belastingen/belastingen_op_milieugrondslag/tarieven_milieubelastingen/tabellen_tarieven_milieubelastingen
|
# See https://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/zakelijk/overige_belastingen/belastingen_op_milieugrondslag/tarieven_milieubelastingen/tabellen_tarieven_milieubelastingen
|
||||||
# Without VAT
|
# Without VAT
|
||||||
ENERGY_TAX_KWH = { 2020 => 0.09770, 2021 => 0.09428, 2022 => 0.03679, 2023 => 0.12599, 2024 => 0.10880, 2025 => 0.10154}
|
ENERGY_TAX_KWH = { 2020 => 0.09770, 2021 => 0.09428, 2022 => 0.03679, 2023 => 0.12599, 2024 => 0.10880, 2025 => 0.10154, 2026 => 0.09157 }
|
||||||
ODE_KWH = { 2020 => 0.0273, 2021 => 0.0300, 2022 => 0.0305, 2023 => 0.0, 2024 => 0.0, 2025 => 0.0}
|
ODE_KWH = { 2020 => 0.0273, 2021 => 0.0300, 2022 => 0.0305, 2023 => 0.0, 2024 => 0.0, 2025 => 0.0, 2026 =>0.0}
|
||||||
# merge by adding values
|
# merge by adding values
|
||||||
TAX_KWH = ENERGY_TAX_KWH.merge(ODE_KWH){|key, energy_tax, ode| energy_tax + ode}
|
TAX_KWH = ENERGY_TAX_KWH.merge(ODE_KWH){|key, energy_tax, ode| energy_tax + ode}
|
||||||
|
|
||||||
@@ -82,10 +82,7 @@ class Cost
|
|||||||
end
|
end
|
||||||
when 2023
|
when 2023
|
||||||
0.018
|
0.018
|
||||||
when 2024
|
when 2024..2026
|
||||||
# opslag met BTW: 0,02178
|
|
||||||
0.018457
|
|
||||||
when 2025
|
|
||||||
# opslag met BTW: 0,02178
|
# opslag met BTW: 0,02178
|
||||||
0.018457
|
0.018457
|
||||||
end
|
end
|
||||||
@@ -93,6 +90,7 @@ class Cost
|
|||||||
|
|
||||||
def easy_energy_cost(formatted_hour, usage_kwh, return_kwh)
|
def easy_energy_cost(formatted_hour, usage_kwh, return_kwh)
|
||||||
return nil if (usage_kwh.nil? || return_kwh.nil?)
|
return nil if (usage_kwh.nil? || return_kwh.nil?)
|
||||||
|
#p "easy_energy_cost for " + formatted_hour
|
||||||
|
|
||||||
usage_kwh_cost = return_kwh_cost = (entsoe.price_at(formatted_hour)+easy_energy_rate(formatted_hour))*(1+vat_at(Date.parse(formatted_hour)))
|
usage_kwh_cost = return_kwh_cost = (entsoe.price_at(formatted_hour)+easy_energy_rate(formatted_hour))*(1+vat_at(Date.parse(formatted_hour)))
|
||||||
add_tax(formatted_hour, usage_kwh, usage_kwh_cost, return_kwh, return_kwh_cost)
|
add_tax(formatted_hour, usage_kwh, usage_kwh_cost, return_kwh, return_kwh_cost)
|
||||||
@@ -260,6 +258,8 @@ class Cost
|
|||||||
0.25767769
|
0.25767769
|
||||||
when 2025
|
when 2025
|
||||||
high_tariff ? 0.2695 : 0.2296
|
high_tariff ? 0.2695 : 0.2296
|
||||||
|
when 2026
|
||||||
|
high_tariff ? 0.23186 : 0.22442
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -303,6 +303,9 @@ class Cost
|
|||||||
vat = 1 + vat_at(Date.parse(formatted_hour))
|
vat = 1 + vat_at(Date.parse(formatted_hour))
|
||||||
normaal_kwh_cost = 0.2695*vat
|
normaal_kwh_cost = 0.2695*vat
|
||||||
dal_kwh_cost = 0.2296*vat
|
dal_kwh_cost = 0.2296*vat
|
||||||
|
when 1767225600..1785887999 # 2026 full year
|
||||||
|
normaal_kwh_cost = 0.19161
|
||||||
|
dal_kwh_cost = 0.18547
|
||||||
else
|
else
|
||||||
p "Not supported interval Oxxio for value: %d" % date.to_time.to_i
|
p "Not supported interval Oxxio for value: %d" % date.to_time.to_i
|
||||||
# catch-all, incase 'formated_hour' is outside any of the cases
|
# catch-all, incase 'formated_hour' is outside any of the cases
|
||||||
|
|||||||
@@ -104,14 +104,60 @@ class Entsoe
|
|||||||
|
|
||||||
doc = Nokogiri::XML(URI.open(url))
|
doc = Nokogiri::XML(URI.open(url))
|
||||||
|
|
||||||
|
#p "Entsoe prices: %s" % doc
|
||||||
|
|
||||||
prices = doc.xpath('.//xmlns:Point').map{|p| parse_point(p)}
|
prices = doc.xpath('.//xmlns:Point').map{|p| parse_point(p)}
|
||||||
begin
|
begin
|
||||||
# get start_time (in UTC) from XML docment
|
# get start_time (in UTC) from XML docment
|
||||||
start_time = DateTime.parse(doc.xpath('.//xmlns:period.timeInterval//xmlns:start').text)
|
start_time = DateTime.parse(doc.xpath('.//xmlns:period.timeInterval//xmlns:start').text)
|
||||||
|
|
||||||
|
# Get resolution to determine expected number of positions
|
||||||
|
resolution = doc.xpath('.//xmlns:resolution').text
|
||||||
|
interval_minutes = resolution == 'PT15M' ? 15 : 60
|
||||||
|
expected_positions = (24 * 60) / interval_minutes
|
||||||
|
|
||||||
|
# Create hash from available prices
|
||||||
|
price_hash = prices.to_h
|
||||||
|
|
||||||
|
# Fill in missing positions by interpolating from adjacent values
|
||||||
|
complete_prices = {}
|
||||||
|
(1..expected_positions).each do |position|
|
||||||
|
if price_hash.key?(position)
|
||||||
|
complete_prices[position] = price_hash[position]
|
||||||
|
else
|
||||||
|
# Find previous and next available prices for interpolation
|
||||||
|
prev_price = (position-1).downto(1).find { |p| price_hash.key?(p) }
|
||||||
|
next_price = (position+1).upto(expected_positions).find { |p| price_hash.key?(p) }
|
||||||
|
|
||||||
|
if prev_price && next_price
|
||||||
|
# Interpolate between previous and next
|
||||||
|
complete_prices[position] = ((price_hash[prev_price] + price_hash[next_price]) / 2.0).round(5)
|
||||||
|
elsif prev_price
|
||||||
|
# Use previous price as fallback
|
||||||
|
complete_prices[position] = price_hash[prev_price]
|
||||||
|
elsif next_price
|
||||||
|
# Use next price as fallback
|
||||||
|
complete_prices[position] = price_hash[next_price]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calculate the formatted hour for the warning message
|
||||||
|
hour_offset = interval_minutes == 15 ? (position - 1) / 4 : (position - 1)
|
||||||
|
missing_hour = start_time.advance(hours: hour_offset).in_time_zone(zone).strftime("%F %H")
|
||||||
|
p "Warning: Missing Entsoe data for #{missing_hour}, interpolated value: #{complete_prices[position]}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
#returns a hash with keys formatted "yyyy-mm-dd hr" and values price (per kwh)
|
#returns a hash with keys formatted "yyyy-mm-dd hr" and values price (per kwh)
|
||||||
# <position> tag runs from 1-24. We need hours from 00-23, therefore substracting 1
|
# <position> tag runs from 1-96 for 15min intervals. Convert to hourly by taking first interval of each hour
|
||||||
prices.map{|p| [start_time.advance(hours: (p[0]-1)), p[1]]}.to_h
|
if interval_minutes == 15
|
||||||
|
# For 15-minute intervals, use the first interval of each hour (positions 1, 5, 9, 13, ...)
|
||||||
|
complete_prices.select { |pos, _| (pos - 1) % 4 == 0 }
|
||||||
|
.map { |pos, price| [start_time.advance(hours: ((pos - 1) / 4)), price] }
|
||||||
|
.to_h
|
||||||
|
else
|
||||||
|
# For hourly data (position runs from 1-24, we need hours from 00-23)
|
||||||
|
complete_prices.map { |pos, price| [start_time.advance(hours: (pos - 1)), price] }.to_h
|
||||||
|
end
|
||||||
rescue Date::Error => e
|
rescue Date::Error => e
|
||||||
p e.message
|
p e.message
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class CreatesPrices << ActiveRecord::Migration[4.2]
|
class CreatesPrices < ActiveRecord::Migration[4.2]
|
||||||
def change
|
def change
|
||||||
create_table :prices do |t|
|
create_table :prices do |t|
|
||||||
t.datetime :hour
|
t.datetime :hour
|
||||||
@@ -7,4 +7,5 @@ class CreatesPrices << ActiveRecord::Migration[4.2]
|
|||||||
end
|
end
|
||||||
|
|
||||||
add_index :prices, :hour
|
add_index :prices, :hour
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: mysql:8.3
|
image: mysql:8.3
|
||||||
volumes:
|
volumes:
|
||||||
- /home/pcog/smartmeter/data:/var/lib/mysql
|
- mysql-data:/var/lib/mysql
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
environment:
|
environment:
|
||||||
@@ -12,24 +12,19 @@ services:
|
|||||||
MYSQL_DATABASE: smartmeter
|
MYSQL_DATABASE: smartmeter
|
||||||
smartmeter:
|
smartmeter:
|
||||||
container_name: smartmeter
|
container_name: smartmeter
|
||||||
|
labels:
|
||||||
|
- diun.enable=false
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
build: .
|
build: .
|
||||||
|
environment:
|
||||||
|
MQTT_HOST: 10.0.0.240
|
||||||
command: 'ruby ./smartmeter.rb'
|
command: 'ruby ./smartmeter.rb'
|
||||||
devices:
|
devices:
|
||||||
- "/dev/ttyUSB1:/dev/ttyUSB0"
|
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/usr/src/app
|
- .:/usr/src/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
rstudio:
|
|
||||||
container_name: rstudio
|
|
||||||
restart: unless-stopped
|
|
||||||
build: ./rstudio
|
|
||||||
environment:
|
|
||||||
PASSWORD: secret
|
|
||||||
volumes:
|
|
||||||
- ./rstudio:/home/rstudio/smartmeter
|
|
||||||
ports:
|
|
||||||
- 8787:8787
|
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql-data:
|
||||||
|
|||||||
10
docker-entrypoint.sh
Normal file
10
docker-entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
unset BUNDLE_PATH
|
||||||
|
unset BUNDLE_BIN
|
||||||
|
|
||||||
|
# Start cron in background
|
||||||
|
cron
|
||||||
|
|
||||||
|
# Start your Ruby process in foreground
|
||||||
|
ruby /usr/src/app/smartmeter.rb
|
||||||
@@ -1,121 +1,185 @@
|
|||||||
|
3233)
|
||||||
/ISk5\2ME382-1003
|
1-0:1.8.1(001402.671*kWh)
|
||||||
|
1-0:1.8.2(001130.208*kWh)
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-0:2.8.1(000080.565*kWh)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
1-0:2.8.2(000205.556*kWh)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
0-0:96.14.0(0001)
|
||||||
1-0:2.8.1(00093.034*kWh)
|
1-0:1.7.0(01.283*kW)
|
||||||
1-0:2.8.2(00147.035*kWh)
|
1-0:2.7.0(00.000*kW)
|
||||||
0-0:96.14.0(0002)
|
0-0:96.7.21(00015)
|
||||||
1-0:1.7.0(0000.00*kW)
|
0-0:96.7.9(00004)
|
||||||
1-0:2.7.0(0000.28*kW)
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
0-0:17.0.0(0999.00*kW)
|
1-0:32.32.0(00000)
|
||||||
0-0:96.3.10(1)
|
1-0:32.36.0(00003)
|
||||||
0-0:96.13.1()
|
|
||||||
0-0:96.13.0()
|
0-0:96.13.0()
|
||||||
0-1:24.1.0(3)
|
1-0:32.7.0(236.0*V)
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
1-0:31.7.0(005*A)
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
1-0:21.7.0(01.283*kW)
|
||||||
(00309.466)
|
1-0:22.7.0(00.000*kW)
|
||||||
0-1:24.4.0(1)
|
0-1:24.1.0(003)
|
||||||
!
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
/ISk5\2ME382-1003
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!7603
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-3:2.671*kWh)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
1-0:1.8.2(001130.208*kWh)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
1-0:2.8.1(000080.565*kWh)
|
||||||
1-0:2.8.1(00093.034*kWh)
|
1-0:2.8.2(000205.556*kWh)
|
||||||
1-0:2.8.2(00147.036*kWh)
|
0-0:96.14.0(0001)
|
||||||
0-0:96.14.0(0002)
|
1-0:1.7.0(01.286*kW)
|
||||||
1-0:1.7.0(0000.00*kW)
|
1-0:2.7.0(00.000*kW)
|
||||||
1-0:2.7.0(0000.31*kW)
|
0-0:96.7.21(00015)
|
||||||
0-0:17.0.0(0999.00*kW)
|
0-0:96.7.9(00004)
|
||||||
0-0:96.3.10(1)
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
0-0:96.13.1()
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
0-0:96.13.0()
|
0-0:96.13.0()
|
||||||
0-1:24.1.0(3)
|
1-0:32.7.0(236.0*V)
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
1-0:31.7.0(005*A)
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
1-0:21.7.0(01.286*kW)
|
||||||
(00309.466)
|
1-0:22.7.0(00.000*kW)
|
||||||
0-1:24.4.0(1)
|
0-1:24.1.0(003)
|
||||||
!
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
/ISk5\2ME382-1003
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!B5E2
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-3:0.2.8(50)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
0-0::1.8.2(001130.208*kWh)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
1-0:2.8.1(000080.565*kWh)
|
||||||
1-0:2.8.1(00093.034*kWh)
|
1-0:2.8.2(000205.556*kWh)
|
||||||
1-0:2.8.2(00147.037*kWh)
|
0-0:96.14.0(0001)
|
||||||
0-0:96.14.0(0002)
|
1-0:1.7.0(01.335*kW)
|
||||||
1-0:1.7.0(0000.00*kW)
|
1-0:2.7.0(00.000*kW)
|
||||||
1-0:2.7.0(0000.32*kW)
|
0-0:96.7.21(00015)
|
||||||
0-0:17.0.0(0999.00*kW)
|
0-0:96.7.9(00004)
|
||||||
0-0:96.3.10(1)
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
0-0:96.13.1()
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
0-0:96.13.0()
|
0-0:96.13.0()
|
||||||
0-1:24.1.0(3)
|
1-0:32.7.0(236.0*V)
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
1-0:31.7.0(005*A)
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
1-0:21.7.0(01.335*kW)
|
||||||
(00309.466)
|
1-0:22.7.0(00.000*kW)
|
||||||
0-1:24.4.0(1)
|
0-1:24.1.0(003)
|
||||||
!
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
/ISk5\2ME382-1003
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!6D5D
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-3:0.2.8(50)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
0-0:1.0.0(260103201104W)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
|
||||||
1-0:2.8.1(00093.034*kWh)
|
|
||||||
1-0:2.8.2(00147.038*kWh)
|
|
||||||
0-0:96.14.0(0002)
|
|
||||||
1-0:1.7.0(0000.00*kW)
|
|
||||||
1-0:2.7.0(0000.32*kW)
|
|
||||||
0-0:17.0.0(0999.00*kW)
|
|
||||||
0-0:96.3.10(1)
|
|
||||||
0-0:96.13.1()
|
|
||||||
0-0:96.13.0()
|
|
||||||
0-1:24.1.0(3)
|
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
|
||||||
(00309.466)
|
|
||||||
0-1:24.4.0(1)
|
|
||||||
!
|
|
||||||
/ISk5\2ME382-1003
|
|
||||||
|
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-0:2.8.1(000080.565*kWh)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
1-0:2.8.2(000205.556*kWh)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
0-0:96.14.0(0001)
|
||||||
1-0:2.8.1(00093.034*kWh)
|
1-0:1.7.0(01.276*kW)
|
||||||
1-0:2.8.2(00147.039*kWh)
|
1-0:2.7.0(00.000*kW)
|
||||||
0-0:96.14.0(0002)
|
0-0:96.7.21(00015)
|
||||||
1-0:1.7.0(0000.00*kW)
|
0-0:96.7.9(00004)
|
||||||
1-0:2.7.0(0000.32*kW)
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
0-0:17.0.0(0999.00*kW)
|
1-0:32.32.0(00000)
|
||||||
0-0:96.3.10(1)
|
1-0:32.36.0(00003)
|
||||||
0-0:96.13.1()
|
|
||||||
0-0:96.13.0()
|
0-0:96.13.0()
|
||||||
0-1:24.1.0(3)
|
1-0:32.7.0(236.0*V)
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
1-0:31.7.0(005*A)
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
1-0:21.7.0(01.276*kW)
|
||||||
(00309.466)
|
1-0:22.7.0(00.000*kW)
|
||||||
0-1:24.4.0(1)
|
0-1:24.1.0(003)
|
||||||
!
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
/ISk5\2ME382-1003
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!FDB1
|
||||||
|
3233)
|
||||||
|
1-0:1.8.1(001402.673*kWh)
|
||||||
|
1-0:1.8.2(001130.208*kWh)
|
||||||
|
1-0:2.8.1(000080.565*kWh)
|
||||||
|
1-0:2.8.2(000205.556*kWh)
|
||||||
|
0-0:96.14.0(0001)
|
||||||
|
1-0:1.7.0(01.344*kW)
|
||||||
|
1-0:2.7.0(00.000*kW)
|
||||||
|
0-0:96.7.21(00015)
|
||||||
|
0-0:96.7.9(00004)
|
||||||
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
|
0-0:96.13.0()
|
||||||
|
1-0:32.7.0(236.0*V)
|
||||||
|
1-0:31.7.0(006*A)
|
||||||
|
1-0:21.7.0(01.344*kW)
|
||||||
|
1-0:22.7.0(00.000*kW)
|
||||||
|
0-1:24.1.0(003)
|
||||||
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!E9B8
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
0-0:96.1.1(4B413650303035313238303430383132)
|
1-3:0.2.3*kWh)
|
||||||
1-0:1.8.1(00553.931*kWh)
|
1-0:1.8.2(001130.208*kWh)
|
||||||
1-0:1.8.2(00431.594*kWh)
|
1-0:2.8.1(000080.565*kWh)
|
||||||
1-0:2.8.1(00093.034*kWh)
|
1-0:2.8.2(000205.556*kWh)
|
||||||
1-0:2.8.2(00147.040*kWh)
|
0-0:96.14.0(0001)
|
||||||
0-0:96.14.0(0002)
|
1-0:1.7.0(01.319*kW)
|
||||||
1-0:1.7.0(0000.00*kW)
|
1-0:2.7.0(00.000*kW)
|
||||||
1-0:2.7.0(0000.30*kW)
|
0-0:96.7.21(00015)
|
||||||
0-0:17.0.0(0999.00*kW)
|
0-0:96.7.9(00004)
|
||||||
0-0:96.3.10(1)
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
0-0:96.13.1()
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
0-0:96.13.0()
|
0-0:96.13.0()
|
||||||
0-1:24.1.0(3)
|
1-0:32.7.0(236.0*V)
|
||||||
0-1:96.1.0(3238303131303031323439333134383132)
|
1-0:31.7.0(005*A)
|
||||||
0-1:24.3.0(130626090000)(00)(60)(1)(0-1:24.2.1)(m3)
|
1-0:21.7.0(01.319*kW)
|
||||||
(00309.466)
|
1-0:22.7.0(00.000*kW)
|
||||||
0-1:24.4.0(3)
|
0-1:24.1.0(003)
|
||||||
!
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!F0CD
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
|
1-3:0.2.8(50)
|
||||||
|
0-0:1.0.0(2001130.208*kWh)
|
||||||
|
1-0:2.8.1(000080.565*kWh)
|
||||||
|
1-0:2.8.2(000205.556*kWh)
|
||||||
|
0-0:96.14.0(0001)
|
||||||
|
1-0:1.7.0(01.336*kW)
|
||||||
|
1-0:2.7.0(00.000*kW)
|
||||||
|
0-0:96.7.21(00015)
|
||||||
|
0-0:96.7.9(00004)
|
||||||
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
|
0-0:96.13.0()
|
||||||
|
1-0:32.7.0(236.0*V)
|
||||||
|
1-0:31.7.0(005*A)
|
||||||
|
1-0:21.7.0(01.336*kW)
|
||||||
|
1-0:22.7.0(00.000*kW)
|
||||||
|
0-1:24.1.0(003)
|
||||||
|
0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!CCE2
|
||||||
|
/CTA5ZIV-METER
|
||||||
|
|
||||||
|
1-3:0.2.8(50)
|
||||||
|
0-0:1.0.0(260103201108W)
|
||||||
|
0-0:96.1.1(4530303839303031303230373731393233)
|
||||||
|
1-0:1.8.1(001402.674*kWh)
|
||||||
|
1-0:1.8.2(001130.208*kWh)
|
||||||
|
1-0:2.8.1(000080.565*kWh)
|
||||||
|
1-0:2.8.2(000205.556*kWh)
|
||||||
|
0-0:96.14.0(0001)
|
||||||
|
1-0:1.7.0(01.319*kW)
|
||||||
|
1-0:2.7.0(00.000*kW)
|
||||||
|
0-0:96.7.21(00015)
|
||||||
|
0-0:96.7.9(00004)
|
||||||
|
1-0:99.97.0(0)(0-0:96.7.19)
|
||||||
|
1-0:32.32.0(00000)
|
||||||
|
1-0:32.36.0(00003)
|
||||||
|
0-0:96.13.0()
|
||||||
|
1-0:32.7.0(236.0*V)
|
||||||
|
1-0:31.7.0(005*A)
|
||||||
|
1-0:21.7.0(01.319*kW)
|
||||||
|
1-0:22.7.0(00.000*kW)
|
||||||
|
0-1:24.1.0-1:96.1.0(4730303933303034333734333338333235)
|
||||||
|
0-1:24.2.1(260103201000W)(00272.130*m3)
|
||||||
|
!B4B3
|
||||||
|
|||||||
3
reportcron
Normal file
3
reportcron
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
PATH=/usr/local/bundle/bin:/usr/local/bundle/gems/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
GEM_HOME=/usr/local/bundle
|
||||||
|
1 7 * * * root cd /usr/src/app && bundle exec ruby report_mailer.rb >> /var/log/cron.log 2>&1
|
||||||
Reference in New Issue
Block a user