diff --git a/.env.example b/.env.example index 789fa1e..ec3e0d2 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,6 @@ # IP of your Shelly device SHELLY_HOST=192.168.1.1 -# Generation of your Shelly device (1 or 2 or 3, default is 2) -SHELLY_GEN=2 - # Interval in seconds to get data from Shelly SHELLY_INTERVAL=5 diff --git a/.env.test b/.env.test index 9582132..9e21664 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,4 @@ +SHELLY_CLOUD_SERVER=https://shelly-95-eu.shelly.cloud SHELLY_HOST=shelly-pro-3em SHELLY_INTERVAL=5 diff --git a/lib/config.rb b/lib/config.rb index e149b83..ca15535 100644 --- a/lib/config.rb +++ b/lib/config.rb @@ -1,12 +1,13 @@ -require 'shelly_gen1_adapter' -require 'shelly_gen2_adapter' -require 'shelly_gen3_adapter' +require 'shelly_local_adapter' +require 'shelly_cloud_adapter' require 'blank' require 'null_logger' KEYS = %i[ shelly_host - shelly_gen + shelly_cloud_server + shelly_device_id + shelly_auth_key shelly_interval influx_schema influx_host @@ -19,7 +20,6 @@ ].freeze DEFAULTS = { - shelly_gen: 2, shelly_interval: 5, influx_schema: :http, influx_port: 8086, @@ -54,7 +54,7 @@ def convert_types end # Integer - %i[shelly_interval influx_port shelly_gen].each do |key| + %i[shelly_interval influx_port].each do |key| self[key] = self[key]&.to_i end end @@ -74,20 +74,19 @@ def limit_interval def validate! validate_influx_settings! validate_interval!(shelly_interval) - validate_gen!(shelly_gen) end def influx_url "#{influx_schema}://#{influx_host}:#{influx_port}" end - def shelly_url - "http://#{shelly_host}" - end - def adapter - # Instance of ShellyGen1Adapter, ShellyGen2Adapter, or ShellyGen3Adapter - @adapter ||= Object.const_get("ShellyGen#{shelly_gen}Adapter").new(config: self) + @adapter ||= + if shelly_cloud_server + ShellyCloudAdapter.new(config: self) + else + ShellyLocalAdapter.new(config: self) + end end attr_writer :logger @@ -102,10 +101,6 @@ def validate_interval!(interval) (interval.is_a?(Integer) && interval.positive?) || throw("SHELLY_INTERVAL is invalid: #{interval}") end - def validate_gen!(gen) - [1, 2, 3].include?(gen) || throw("SHELLY_GEN is invalid: #{gen}") - end - def validate_influx_settings! %i[ influx_schema @@ -137,7 +132,9 @@ def self.from_env(options = {}) new( { shelly_host: ENV.fetch('SHELLY_HOST', nil), - shelly_gen: ENV.fetch('SHELLY_GEN', nil), + shelly_cloud_server: ENV.fetch('SHELLY_CLOUD_SERVER', nil), + shelly_device_id: ENV.fetch('SHELLY_DEVICE_ID', nil), + shelly_auth_key: ENV.fetch('SHELLY_AUTH_KEY', nil), shelly_interval: ENV.fetch('SHELLY_INTERVAL', nil), influx_host: ENV.fetch('INFLUX_HOST'), influx_schema: ENV.fetch('INFLUX_SCHEMA', nil), diff --git a/lib/shelly_cloud_adapter.rb b/lib/shelly_cloud_adapter.rb new file mode 100644 index 0000000..3ec9d76 --- /dev/null +++ b/lib/shelly_cloud_adapter.rb @@ -0,0 +1,69 @@ +require 'solectrus_record' +require 'forwardable' +require 'faraday' +require 'faraday-request-timer' + +class ShellyCloudAdapter + extend Forwardable + def_delegators :config, :logger + + def initialize(config:) + @config = config + + logger.info "Pulling from Shelly (Cloud) for device #{config.shelly_device_id} every #{config.shelly_interval} seconds" + end + + attr_reader :config + + def connection + @connection ||= Faraday.new(url: config.shelly_cloud_server) do |f| + f.adapter Faraday.default_adapter + f.request :timer + end + end + + def solectrus_record(id = 1) + # Reset cache + @data = nil + @raw_response = nil + + parser = ShellyResponseParser.new(raw_response.body) + record = parser.solectrus_record(id:, response_duration:) + logger.info success_message(record) + record + rescue StandardError => e + logger.error failure_message(e) + nil + end + + private + + def raw_response + @raw_response ||= begin + response = connection.get('/device/status') do |req| + req.params['id'] = config.shelly_device_id + req.params['auth_key'] = config.shelly_auth_key + end + + raise StandardError, response.status unless response.success? + + response + end + end + + def success_message(record) + "\nGot record ##{record.id} at " \ + "#{Time.at(record.time).localtime} " \ + "within #{record.response_duration} ms, " \ + "Power #{record.power.round(1)} W, " \ + "Temperature #{record.temp} °C" + end + + def failure_message(error) + "Error getting data from Shelly at #{config.shelly_host}: #{error}" + end + + def response_duration + (raw_response.env[:duration] * 1000).round + end +end diff --git a/lib/shelly_gen2_adapter.rb b/lib/shelly_gen2_adapter.rb deleted file mode 100644 index 152fe9a..0000000 --- a/lib/shelly_gen2_adapter.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'solectrus_record' -require 'forwardable' -require 'faraday' -require 'faraday-request-timer' - -class ShellyGen2Adapter - extend Forwardable - def_delegators :config, :logger - - def initialize(config:) - @config = config - - logger.info "Pulling from your Shelly (Gen2) at #{config.shelly_url}#{path} every #{config.shelly_interval} seconds" - end - - attr_reader :config - - def connection - @connection ||= Faraday.new(url: config.shelly_url) do |f| - f.adapter Faraday.default_adapter - f.request :timer - end - end - - def solectrus_record(id = 1) - # Reset cache - @data = nil - @raw_response = nil - - SolectrusRecord.new(id:, time:, payload: record_hash).tap do |record| - logger.info success_message(record) - end - rescue StandardError => e - logger.error failure_message(e) - nil - end - - private - - def record_hash - { - temp:, - power:, - power_a:, - power_b:, - power_c:, - response_duration:, - }.compact - end - - def path - '/rpc/Shelly.GetStatus' - end - - def raw_response - @raw_response ||= begin - response = connection.get(path) - raise StandardError, response.status unless response.success? - - response - end - end - - def data - @data ||= JSON.parse(raw_response.body) - end - - def success_message(record) - "\nGot record ##{record.id} at " \ - "#{Time.at(record.time).localtime} " \ - "within #{record.response_duration} ms, " \ - "Power #{record.power.round(1)} W, " \ - "Temperature #{record.temp} °C" - end - - def failure_message(error) - "Error getting data from Shelly at #{config.shelly_url}: #{error}" - end - - def response_duration - (raw_response.env[:duration] * 1000).round - end - - def time - data.dig('sys', 'unixtime') - end - - def temp - data.dig('temperature:0', 'tC') || data.dig('switch:0', 'temperature', 'tC') - end - - def power - data.dig('em:0', 'total_act_power') || data.dig('switch:0', 'apower') - end - - def power_a - data.dig('em:0', 'a_act_power') - end - - def power_b - data.dig('em:0', 'b_act_power') - end - - def power_c - data.dig('em:0', 'c_act_power') - end -end diff --git a/lib/shelly_gen3_adapter.rb b/lib/shelly_gen3_adapter.rb deleted file mode 100644 index f4f93cf..0000000 --- a/lib/shelly_gen3_adapter.rb +++ /dev/null @@ -1,106 +0,0 @@ -require 'solectrus_record' -require 'forwardable' -require 'faraday' -require 'faraday-request-timer' - -class ShellyGen3Adapter - extend Forwardable - def_delegators :config, :logger - - def initialize(config:) - @config = config - - logger.info "Pulling from your Shelly (Gen3) at #{config.shelly_url}#{path} every #{config.shelly_interval} seconds" - end - - attr_reader :config - - def connection - @connection ||= Faraday.new(url: config.shelly_url) do |f| - f.adapter Faraday.default_adapter - f.request :timer - end - end - - def solectrus_record(id = 1) - # Reset cache - @data = nil - @raw_response = nil - - SolectrusRecord.new(id:, time:, payload: record_hash).tap do |record| - logger.info success_message(record) - end - rescue StandardError => e - logger.error failure_message(e) - nil - end - - private - - def record_hash - { - temp:, - power:, - power_a:, - power_b:, - power_c:, - response_duration:, - }.compact - end - - def path - '/rpc/Shelly.GetStatus' - end - - def raw_response - @raw_response ||= begin - response = connection.get(path) - raise StandardError, response.status unless response.success? - - response - end - end - - def data - @data ||= JSON.parse(raw_response.body) - end - - def success_message(record) - "\nGot record ##{record.id} at " \ - "#{Time.at(record.time).localtime} " \ - "within #{record.response_duration} ms, " \ - "Power #{record.power.round(1)} W " - end - - def failure_message(error) - "Error getting data from Shelly at #{config.shelly_url}: #{error}" - end - - def response_duration - (raw_response.env[:duration] * 1000).round - end - - def time - data.dig('sys', 'unixtime') - end - - def temp - # does not exist in Shelly PM Mini Gen. 3 - end - - def power - data.dig('pm1:0', 'apower') - end - - def power_a - # does not exist in Shelly PM Mini Gen. 3 - end - - def power_b - # does not exist in Shelly PM Mini Gen. 3 - end - - def power_c - # does not exist in Shelly PM Mini Gen. 3 - end -end diff --git a/lib/shelly_gen1_adapter.rb b/lib/shelly_local_adapter.rb similarity index 50% rename from lib/shelly_gen1_adapter.rb rename to lib/shelly_local_adapter.rb index 423b067..74f91f6 100644 --- a/lib/shelly_gen1_adapter.rb +++ b/lib/shelly_local_adapter.rb @@ -1,22 +1,27 @@ +require 'shelly_response_parser' require 'solectrus_record' + require 'forwardable' require 'faraday' require 'faraday-request-timer' -class ShellyGen1Adapter +class ShellyLocalAdapter extend Forwardable def_delegators :config, :logger + GEN1_PATH = '/status'.freeze + GEN2_PATH = '/rpc/Shelly.GetStatus'.freeze + def initialize(config:) @config = config - logger.info "Pulling from your Shelly (Gen1) at #{config.shelly_url}#{path} every #{config.shelly_interval} seconds" + logger.info "Pulling from your Shelly at #{shelly_url} every #{config.shelly_interval} seconds" end attr_reader :config def connection - @connection ||= Faraday.new(url: config.shelly_url) do |f| + @connection ||= Faraday.new(url: shelly_url) do |f| f.adapter Faraday.default_adapter f.request :timer end @@ -27,30 +32,15 @@ def solectrus_record(id = 1) @data = nil @raw_response = nil - SolectrusRecord.new(id:, time:, payload: record_hash).tap do |record| - logger.info success_message(record) - end + parser = ShellyResponseParser.new(raw_response.body) + record = parser.solectrus_record(id:, response_duration:) + logger.info success_message(record) + record rescue StandardError => e logger.error failure_message(e) nil end - private - - def record_hash - { - power:, - power_a:, - power_b:, - power_c:, - response_duration:, - }.compact - end - - def path - '/status' - end - def raw_response @raw_response ||= begin response = connection.get(path) @@ -60,8 +50,26 @@ def raw_response end end - def data - @data ||= JSON.parse(raw_response.body) + def shelly_url + "http://#{config.shelly_host}" + end + + def path + @path ||= + if can_connect_to?(GEN1_PATH) + GEN1_PATH + elsif can_connect_to?(GEN2_PATH) + GEN2_PATH + else + raise StandardError, + "Unknown Shelly generation, the device at #{shelly_url} does not not respond to #{GEN1_PATH} or #{GEN2_PATH}" + end + end + + def can_connect_to?(path) + connection.get(path).success? + rescue StandardError + false end def success_message(record) @@ -72,30 +80,10 @@ def success_message(record) end def failure_message(error) - "Error getting data from Shelly at #{config.shelly_url}: #{error}" + "Error getting data from Shelly at #{shelly_url}: #{error}" end def response_duration (raw_response.env[:duration] * 1000).round end - - def time - data['unixtime'] - end - - def power - data['total_power'] || (power_a.to_f + power_b.to_f + power_c.to_f) - end - - def power_a - data.dig('emeters', 0, 'power') || data.dig('meters', 0, 'power') - end - - def power_b - data.dig('emeters', 1, 'power') || data.dig('meters', 1, 'power') - end - - def power_c - data.dig('emeters', 2, 'power') || data.dig('meters', 2, 'power') - end end diff --git a/lib/shelly_response_parser.rb b/lib/shelly_response_parser.rb new file mode 100644 index 0000000..7da3d78 --- /dev/null +++ b/lib/shelly_response_parser.rb @@ -0,0 +1,76 @@ +require 'solectrus_record' + +class ShellyResponseParser + def initialize(json) + @data = JSON.parse(json) + end + attr_reader :data + + def solectrus_record(id: 1, response_duration: nil) + SolectrusRecord.new(id:, response_duration:, time:, payload: record_hash) + end + + private + + def record_hash + { + temp:, + power:, + power_a:, + power_b:, + power_c:, + }.compact + end + + def time + data['unixtime'] || + data.dig('sys', 'unixtime') || + device_status['ts'] + end + + def temp + data.dig('temperature:0', 'tC') || + data.dig('switch:0', 'temperature', 'tC') || + device_status&.dig('temperature:0', 'tC') || + device_status&.dig('switch:0', 'temperature', 'tC') + end + + def power # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + data['total_power'] || + data.dig('pm1:0', 'apower') || + data.dig('em:0', 'total_act_power') || + data.dig('switch:0', 'apower') || + device_status&.dig('em:0', 'total_act_power') || + device_status&.dig('switch:0', 'apower') || + phases_total + end + + def phases_total + power_a.to_f + power_b.to_f + power_c.to_f + end + + def power_a + data.dig('emeters', 0, 'power') || + data.dig('meters', 0, 'power') || + data.dig('em:0', 'a_act_power') || + device_status&.dig('em:0', 'a_act_power') + end + + def power_b + data.dig('emeters', 1, 'power') || + data.dig('meters', 1, 'power') || + data.dig('em:0', 'b_act_power') || + device_status&.dig('em:0', 'b_act_power') + end + + def power_c + data.dig('emeters', 2, 'power') || + data.dig('meters', 2, 'power') || + data.dig('em:0', 'c_act_power') || + device_status&.dig('em:0', 'c_act_power') + end + + def device_status + data.dig('data', 'device_status') + end +end diff --git a/lib/solectrus_record.rb b/lib/solectrus_record.rb index c398656..1eb5d1e 100644 --- a/lib/solectrus_record.rb +++ b/lib/solectrus_record.rb @@ -1,11 +1,12 @@ class SolectrusRecord - def initialize(id:, time:, payload:) + def initialize(id:, time:, payload:, response_duration: nil) @id = id @time = time @payload = payload + @response_duration = response_duration end - attr_reader :id, :time + attr_reader :id, :time, :response_duration def to_hash @payload @@ -17,7 +18,6 @@ def to_hash power_a power_b power_c - response_duration ].each do |method| define_method(method) do @payload[method] diff --git a/spec/cassettes/shelly-3em.yml b/spec/cassettes/shelly-3em.yml index 97b1af8..430d464 100644 --- a/spec/cassettes/shelly-3em.yml +++ b/spec/cassettes/shelly-3em.yml @@ -1,5 +1,29 @@ --- http_interactions: +- request: + method: get + uri: http://shelly-3em/status + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.0 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '1258' + connection: + - close + body: + encoding: UTF-8 + string: '{"wifi_sta":{"connected":true,"ssid":"FRITZ!Box 7590 YF","ip":"192.168.178.79","rssi":-45},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":true},"time":"19:50","unixtime":1726163405,"serial":15879,"has_update":false,"mac":"485519DAC918","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"is_valid":true,"source":"input"}],"emeters":[{"power":33.59,"pf":0.12,"current":1.25,"voltage":229.41,"is_valid":true,"total":399476.5,"total_returned":0.0},{"power":5.22,"pf":0.02,"current":0.99,"voltage":230.72,"is_valid":true,"total":310273.8,"total_returned":919.7},{"power":2.66,"pf":0.01,"current":0.98,"voltage":22994,"is_valid":true,"total":224937.6,"total_returned":630.1}],"total_power":41.47,"emeter_n":{"current":0.00,"ixsum":0.35,"mismatch":false,"is_valid":false},"fs_mounted":true,"v_data":1,"ct_calst":0,"update":{"status":"idle","has_update":false,"new_version":"20230913-114244/v1.14.0-gcb84623","old_version":"20230913-114244/v1.14.0-gcb84623","beta_version":"20231107-165007/v1.14.1-rc1-g0617c15"},"ram_total":49920,"ram_free":29956,"fs_size":233681,"fs_free":154365,"uptime":169698}' + recorded_at: Thu, 26 Sep 2024 08:52:31 GMT - request: method: get uri: http://shelly-3em/status diff --git a/spec/cassettes/shelly-cloud.yml b/spec/cassettes/shelly-cloud.yml new file mode 100644 index 0000000..c0e331f --- /dev/null +++ b/spec/cassettes/shelly-cloud.yml @@ -0,0 +1,38 @@ +--- +http_interactions: +- request: + method: get + uri: "/device/status?auth_key=&id=" + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 200 + message: OK + headers: + server: + - nginx/1.18.0 (Ubuntu) + date: + - Wed, 15 Jan 2025 14:38:31 GMT + content-type: + - application/json; charset=utf-8 + content-length: + - '1433' + connection: + - keep-alive + x-powered-by: + - Express + access-control-allow-origin: + - "*" + etag: + - W/"599-r6X+mEJWKfIezKoaY0bXniO4kKY" + body: + encoding: UTF-8 + string: '{"isok":true,"data":{"online":true,"device_status":{"_updated":"2025-01-15 + 14:38:22","id":"","temperature:0":{"id":0,"tC":47.5,"tF":117.6},"ts":1736951888.29,"bthome":{"errors":["bluetooth_disabled"]},"eth":{"ip":"192.168.178.83"},"ws":{"connected":false},"code":"SPEM-003CEBEU","mqtt":{"connected":false},"wifi":{"sta_ip":null,"status":"disconnected","ssid":null,"rssi":0},"serial":70465,"ble":{},"em:0":{"id":0,"a_act_power":232.1,"a_aprt_power":389.5,"a_current":1.661,"a_freq":50,"a_pf":0.6,"a_voltage":234.5,"b_act_power":153.6,"b_aprt_power":353.2,"b_current":1.503,"b_freq":50,"b_pf":0.43,"b_voltage":235.1,"c_act_power":124.3,"c_aprt_power":337.4,"c_current":1.438,"c_freq":50,"c_pf":0.37,"c_voltage":234.9,"total_act_power":509.977,"total_aprt_power":1080.095,"total_current":4.602,"user_calibrated_phase":[]},"emdata:0":{"id":0,"a_total_act_energy":804960.02,"a_total_act_ret_energy":0.57,"b_total_act_energy":688094.52,"b_total_act_ret_energy":0.01,"c_total_act_energy":574760.91,"c_total_act_ret_energy":3856.74,"total_act":2067815.44,"total_act_ret":3857.31},"sys":{"available_updates":{"beta":{"version":"1.5.0-beta1"}},"mac":"34987A45A6A0","restart_required":false,"time":"14:16","unixtime":1736169379,"uptime":6941177,"ram_size":247552,"ram_free":129988,"fs_size":524288,"fs_free":180224,"cfg_rev":38,"kvs_rev":2,"schedule_rev":0,"webhook_rev":2,"reset_reason":3},"modbus":{},"cloud":{"connected":true}}}}' + recorded_at: Wed, 15 Jan 2025 14:38:31 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/cassettes/shelly-em.yml b/spec/cassettes/shelly-em.yml index c319704..4ab955d 100644 --- a/spec/cassettes/shelly-em.yml +++ b/spec/cassettes/shelly-em.yml @@ -1,5 +1,29 @@ --- http_interactions: +- request: + method: get + uri: http://shelly-em/status + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.0 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '1258' + connection: + - close + body: + encoding: UTF-8 + string: '{"wifi_sta":{"connected":true,"ssid":"XXXXXXXXXXXXXXXXXX","ip":"XXXXXXXXXXXXXXXXXX","rssi":-37},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":true},"time":"09:39","unixtime":1727422761,"serial":21128,"has_update":false,"mac":"XXXXXXXXXXXXXXXXXX","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"is_valid":true,"source":"input"}],"emeters":[{"power":3.63,"reactive":0,"pf":0,"voltage":226.02,"is_valid":true,"total":1724256.1,"total_returned":785.7},{"power":6.45,"reactive":-13.82,"pf":-0.42,"voltage":226.02,"is_valid":true,"total":510346.8,"total_returned":194.1}],"update":{"status":"idle","has_update":false,"new_version":"20230913-114150\/v1.14.0-gcb84623","old_version":"20230913-114150\/v1.14.0-gcb84623","beta_version":"20231107-164916\/v1.14.1-rc1-g0617c15"},"ram_total":51064,"ram_free":34936,"fs_size":233681,"fs_free":150349,"uptime":5071906}' + recorded_at: Thu, 26 Sep 2024 08:52:31 GMT - request: method: get uri: http://shelly-em/status diff --git a/spec/cassettes/shelly-plug-s-gen1.yml b/spec/cassettes/shelly-plug-s-gen1.yml index 434d24f..f40a45a 100644 --- a/spec/cassettes/shelly-plug-s-gen1.yml +++ b/spec/cassettes/shelly-plug-s-gen1.yml @@ -1,5 +1,29 @@ --- http_interactions: +- request: + method: get + uri: http://shelly-plug-s-gen1/status + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.0 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '1258' + connection: + - close + body: + encoding: UTF-8 + string: '{"wifi_sta":{"connected":true,"ssid":"xxxx","ip":"10.31.22.36","rssi":-86},"cloud":{"enabled":true,"connected":true},"mqtt":{"connected":false},"time":"12:31","unixtime":1734521482,"serial":3,"has_update":false,"mac":"80646F838326","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":true,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"source":"input"}],"meters":[{"power":99.28,"overpower":0,"is_valid":true,"timestamp":1734525082,"counters":[40.336,0,0],"total":40}],"temperature":31.32,"overtemperature":false,"tmp":{"tC":31.32,"tF":88.38,"is_valid":true},"update":{"status":"idle","has_update":false,"new_version":"20230913-113421/v1.14.0-gcb84623","old_version":"20230913-113421/v1.14.0-gcb84623","beta_version":"20231107-164219/v1.14.1-rc1-g0617c15"},"ram_total":52056,"ram_free":38896,"fs_size":233681,"fs_free":166413,"uptime":139}' + recorded_at: Thu, 26 Sep 2024 08:52:31 GMT - request: method: get uri: http://shelly-plug-s-gen1/status diff --git a/spec/cassettes/shelly-plug-s-gen2.yml b/spec/cassettes/shelly-plug-s-gen2.yml index a6f4c27..e587799 100644 --- a/spec/cassettes/shelly-plug-s-gen2.yml +++ b/spec/cassettes/shelly-plug-s-gen2.yml @@ -2,13 +2,67 @@ http_interactions: - request: method: get - uri: http://shelly-plug-s-gen2/rpc/Shelly.GetStatus + uri: http://shelly-plug-s/status body: encoding: US-ASCII string: '' headers: User-Agent: - - Faraday v2.12.0 + - Faraday v2.12.2 + response: + status: + code: 404 + message: Not Found + headers: + content-length: + - '9' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: Not Found + recorded_at: Thu, 16 Jan 2025 13:22:21 GMT +- request: + method: get + uri: http://shelly-plug-s/rpc/Shelly.GetStatus + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '728' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: '{"ble":{},"cloud":{"connected":true},"mqtt":{"connected":true},"plugs_ui":{},"switch:0":{"id":0, + "source":"init", "output":true, "apower":3.7, "voltage":235.0, "current":0.067, + "aenergy":{"total":131343.622,"by_minute":[62.812,63.257,62.812],"minute_ts":1737033720},"temperature":{"tC":38.4, + "tF":101.1}},"sys":{"mac":"E465B8B12BF0","restart_required":false,"time":"14:22","unixtime":1737033741,"uptime":2866416,"ram_size":253688,"ram_free":133624,"fs_size":393216,"fs_free":94208,"cfg_rev":17,"kvs_rev":0,"schedule_rev":1,"webhook_rev":0,"available_updates":{"beta":{"version":"1.5.0-beta1"}},"reset_reason":3},"wifi":{"sta_ip":"192.168.178.90","status":"got + ip","ssid":"FRITZ!Box 4060 RX","rssi":-74},"ws":{"connected":false}}' + recorded_at: Thu, 16 Jan 2025 13:22:21 GMT +- request: + method: get + uri: http://shelly-plug-s/rpc/Shelly.GetStatus + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 response: status: code: 200 @@ -17,17 +71,17 @@ http_interactions: content-type: - application/json content-length: - - '700' + - '728' server: - ShellyHTTP/1.0.0 connection: - close body: encoding: UTF-8 - string: '{"ble":{},"cloud":{"connected":true},"mqtt":{"connected":false},"plugs_ui":{},"switch:0":{"id":0, - "source":"init", "output":true, "apower":74.9, "voltage":234.6, "current":0.051, - "aenergy":{"total":34100.726,"by_minute":[775.992,737.589,656.609],"minute_ts":1727340720},"temperature":{"tC":39.4, - "tF":102.9}},"sys":{"mac":"FCB467272FA0","restart_required":false,"time":"10:52","unixtime":1727340759,"uptime":3124102,"ram_size":253764,"ram_free":134512,"fs_size":393216,"fs_free":94208,"cfg_rev":31,"kvs_rev":0,"schedule_rev":1,"webhook_rev":0,"available_updates":{},"reset_reason":3},"wifi":{"sta_ip":"192.168.178.88","status":"got - ip","ssid":"FRITZ!Box 4060 RX","rssi":-67},"ws":{"connected":false}}' - recorded_at: Thu, 26 Sep 2024 08:52:39 GMT + string: '{"ble":{},"cloud":{"connected":true},"mqtt":{"connected":true},"plugs_ui":{},"switch:0":{"id":0, + "source":"init", "output":true, "apower":3.7, "voltage":235.0, "current":0.067, + "aenergy":{"total":131343.622,"by_minute":[62.812,63.257,62.812],"minute_ts":1737033720},"temperature":{"tC":38.4, + "tF":101.1}},"sys":{"mac":"E465B8B12BF0","restart_required":false,"time":"14:22","unixtime":1737033742,"uptime":2866416,"ram_size":253688,"ram_free":133616,"fs_size":393216,"fs_free":94208,"cfg_rev":17,"kvs_rev":0,"schedule_rev":1,"webhook_rev":0,"available_updates":{"beta":{"version":"1.5.0-beta1"}},"reset_reason":3},"wifi":{"sta_ip":"192.168.178.90","status":"got + ip","ssid":"FRITZ!Box 4060 RX","rssi":-74},"ws":{"connected":false}}' + recorded_at: Thu, 16 Jan 2025 13:22:22 GMT recorded_with: VCR 6.3.1 diff --git a/spec/cassettes/shelly-pm-mini-gen3.yml b/spec/cassettes/shelly-pm-mini-gen3.yml index 9fdc0b8..e506b4f 100644 --- a/spec/cassettes/shelly-pm-mini-gen3.yml +++ b/spec/cassettes/shelly-pm-mini-gen3.yml @@ -1,5 +1,55 @@ --- http_interactions: +- request: + method: get + uri: http://shelly-pm-mini-gen3/status + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 404 + message: Not Found + headers: + content-length: + - '9' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: Not Found + recorded_at: Thu, 16 Jan 2025 13:22:21 GMT +- request: + method: get + uri: http://shelly-pm-mini-gen3/rpc/Shelly.GetStatus + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '700' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: '{"ble":{},"bthome":{"errors":["observer_disabled"]},"cloud":{"connected":true},"mqtt":{"connected":true},"pm1:0":{"id":0,"voltage":225.7,"current":0.483,"apower":5.9,"freq":49.9,"aenergy":{"total":38332.91,"by_minute":[0,212.986,0],"minute_ts":1728147840},"ret_aenergy":{"total":0,"by_minute":[0,0,0],"minute_ts":1728147840}},"script:1":{"id":1,"running":true,"mem_used":560,"mem_peak":4900,"mem_free":24626},"sys":{"mac":"ECDA3BC4D4DC","restart_required":false,"time":"19:04","unixtime":1728147850,"uptime":152243,"ram_size":259852,"ram_free":78408,"fs_size":1048576,"fs_free":614400,"cfg_rev":26,"kvs_rev":0,"schedule_rev":0,"webhook_rev":0,"available_updates":{},"reset_reason":3},"wifi":{"sta_ip":"192.168.178.124","status":"got ip","ssid":"FRITZ!Box 6591 Cable GJ","rssi":-71},"ws":{"connected":false}}' + recorded_at: Thu, 16 Jan 2025 13:22:21 GMT - request: method: get uri: http://shelly-pm-mini-gen3/rpc/Shelly.GetStatus diff --git a/spec/cassettes/shelly-pro-3em.yml b/spec/cassettes/shelly-pro-3em.yml index fb27c74..b414b3f 100644 --- a/spec/cassettes/shelly-pro-3em.yml +++ b/spec/cassettes/shelly-pro-3em.yml @@ -1,5 +1,58 @@ --- http_interactions: +- request: + method: get + uri: http://shelly-pro-3em/status + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 404 + message: Not Found + headers: + content-length: + - '9' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: Not Found + recorded_at: Thu, 16 Jan 2025 13:15:01 GMT +- request: + method: get + uri: http://shelly-pro-3em/rpc/Shelly.GetStatus + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday v2.12.2 + response: + status: + code: 200 + message: OK + headers: + content-type: + - application/json + content-length: + - '1298' + server: + - ShellyHTTP/1.0.0 + connection: + - close + body: + encoding: UTF-8 + string: '{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em:0":{"id":0,"a_current":1.667,"a_voltage":234.2,"a_act_power":234.8,"a_aprt_power":390.3,"a_pf":0.60,"a_freq":50.0,"b_current":1.555,"b_voltage":234.8,"b_act_power":160.6,"b_aprt_power":365.0,"b_pf":0.44,"b_freq":50.0,"c_current":1.461,"c_voltage":234.5,"c_act_power":130.6,"c_aprt_power":342.3,"c_pf":0.38,"c_freq":50.0,"n_current":null,"total_current":4.683,"total_act_power":526.009,"total_aprt_power":1097.506, + "user_calibrated_phase":[]},"emdata:0":{"id":0,"a_total_act_energy":811415.06,"a_total_act_ret_energy":0.57,"b_total_act_energy":692807.69,"b_total_act_ret_energy":0.01,"c_total_act_energy":578729.08,"c_total_act_ret_energy":3856.74,"total_act":2082951.84, + "total_act_ret":3857.31},"eth":{"ip":"192.168.178.83"},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"34987A45A6A0","restart_required":false,"time":"14:15","unixtime":1737033302,"uptime":7805113,"ram_size":247508,"ram_free":130068,"fs_size":524288,"fs_free":180224,"cfg_rev":38,"kvs_rev":2,"schedule_rev":0,"webhook_rev":2,"available_updates":{"beta":{"version":"1.5.0-beta1"}},"reset_reason":3},"temperature:0":{"id": + 0,"tC":46.8, "tF":116.2},"wifi":{"sta_ip":null,"status":"disconnected","ssid":null,"rssi":0},"ws":{"connected":false}}' + recorded_at: Thu, 16 Jan 2025 13:15:02 GMT - request: method: get uri: http://shelly-pro-3em/rpc/Shelly.GetStatus @@ -8,7 +61,7 @@ http_interactions: string: '' headers: User-Agent: - - Faraday v2.12.0 + - Faraday v2.12.2 response: status: code: 200 @@ -17,16 +70,16 @@ http_interactions: content-type: - application/json content-length: - - '1258' + - '1298' server: - ShellyHTTP/1.0.0 connection: - close body: encoding: UTF-8 - string: '{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em:0":{"id":0,"a_current":0.616,"a_voltage":233.7,"a_act_power":13.6,"a_aprt_power":144.0,"a_pf":0.09,"a_freq":50.0,"b_current":0.651,"b_voltage":234.8,"b_act_power":4.9,"b_aprt_power":152.7,"b_pf":0.03,"b_freq":50.0,"c_current":0.620,"c_voltage":233.9,"c_act_power":0.0,"c_aprt_power":144.8,"c_pf":0.00,"c_freq":50.0,"n_current":null,"total_current":1.887,"total_act_power":18.471,"total_aprt_power":441.560, - "user_calibrated_phase":[]},"emdata:0":{"id":0,"a_total_act_energy":374452.09,"a_total_act_ret_energy":0.53,"b_total_act_energy":300556.69,"b_total_act_ret_energy":0.01,"c_total_act_energy":249501.26,"c_total_act_ret_energy":3103.02,"total_act":924510.05, - "total_act_ret":3103.55},"eth":{"ip":"192.168.178.83"},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"34987A45A6A0","restart_required":false,"time":"10:52","unixtime":1727340752,"uptime":2561415,"ram_size":247504,"ram_free":130624,"fs_size":524288,"fs_free":180224,"cfg_rev":37,"kvs_rev":2,"schedule_rev":0,"webhook_rev":2,"available_updates":{},"reset_reason":3},"temperature:0":{"id": - 0,"tC":46.2, "tF":115.1},"wifi":{"sta_ip":null,"status":"disconnected","ssid":null,"rssi":0},"ws":{"connected":false}}' - recorded_at: Thu, 26 Sep 2024 08:52:31 GMT + string: '{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em:0":{"id":0,"a_current":1.667,"a_voltage":234.2,"a_act_power":234.8,"a_aprt_power":390.3,"a_pf":0.60,"a_freq":50.0,"b_current":1.555,"b_voltage":234.8,"b_act_power":160.6,"b_aprt_power":365.0,"b_pf":0.44,"b_freq":50.0,"c_current":1.461,"c_voltage":234.5,"c_act_power":130.6,"c_aprt_power":342.3,"c_pf":0.38,"c_freq":50.0,"n_current":null,"total_current":4.683,"total_act_power":526.009,"total_aprt_power":1097.506, + "user_calibrated_phase":[]},"emdata:0":{"id":0,"a_total_act_energy":811415.06,"a_total_act_ret_energy":0.57,"b_total_act_energy":692807.69,"b_total_act_ret_energy":0.01,"c_total_act_energy":578729.08,"c_total_act_ret_energy":3856.74,"total_act":2082951.84, + "total_act_ret":3857.31},"eth":{"ip":"192.168.178.83"},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"34987A45A6A0","restart_required":false,"time":"14:15","unixtime":1737033302,"uptime":7805113,"ram_size":247508,"ram_free":130068,"fs_size":524288,"fs_free":180224,"cfg_rev":38,"kvs_rev":2,"schedule_rev":0,"webhook_rev":2,"available_updates":{"beta":{"version":"1.5.0-beta1"}},"reset_reason":3},"temperature:0":{"id": + 0,"tC":46.8, "tF":116.2},"wifi":{"sta_ip":null,"status":"disconnected","ssid":null,"rssi":0},"ws":{"connected":false}}' + recorded_at: Thu, 16 Jan 2025 13:15:02 GMT recorded_with: VCR 6.3.1 diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb index 006406e..d3dc037 100644 --- a/spec/lib/config_spec.rb +++ b/spec/lib/config_spec.rb @@ -67,60 +67,6 @@ expect(config.shelly_interval).to eq(2) end - - it 'raises an error for invalid SHELLY_GEN' do - expect do - described_class.new(valid_options.merge(shelly_gen: '42')) - end.to raise_error(Exception, /SHELLY_GEN is invalid/) - end - end - - describe 'shelly methods' do - subject(:config) { described_class.new(valid_options.merge(shelly_gen:)) } - - context 'when no shelly_gen is given' do - let(:shelly_gen) { nil } - - it 'returns correct shelly_host' do - expect(config.shelly_host).to eq('1.2.3.4') - end - - it 'returns default shelly_interval' do - expect(config.shelly_interval).to eq(5) - end - - it 'returns default shelly_gen' do - expect(config.shelly_gen).to eq(2) - end - - it 'returns default adapter' do - expect(config.adapter).to be_a(ShellyGen2Adapter) - end - end - - context 'when shelly_gen is 1' do - let(:shelly_gen) { 1 } - - it 'returns adapter' do - expect(config.adapter).to be_a(ShellyGen1Adapter) - end - end - - context 'when shelly_gen is 2' do - let(:shelly_gen) { 2 } - - it 'returns adapter' do - expect(config.adapter).to be_a(ShellyGen2Adapter) - end - end - - context 'when shelly_gen is 3' do - let(:shelly_gen) { 3 } - - it 'returns adapter' do - expect(config.adapter).to be_a(ShellyGen3Adapter) - end - end end describe 'influx methods' do diff --git a/spec/lib/influx_push_spec.rb b/spec/lib/influx_push_spec.rb index 1f44273..728247d 100644 --- a/spec/lib/influx_push_spec.rb +++ b/spec/lib/influx_push_spec.rb @@ -3,7 +3,7 @@ require 'config' describe InfluxPush do - let(:config) { Config.from_env(shelly_interval: 5) } + let(:config) { Config.from_env(shelly_cloud_server: nil) } let(:queue) { Queue.new } let!(:shelly_pull) do ShellyPull.new(config:, queue:) diff --git a/spec/lib/loop_spec.rb b/spec/lib/loop_spec.rb index bbfbc3f..95b805c 100644 --- a/spec/lib/loop_spec.rb +++ b/spec/lib/loop_spec.rb @@ -2,7 +2,7 @@ require 'config' describe Loop do - let(:config) { Config.from_env(shelly_interval: 5) } + let(:config) { Config.from_env(shelly_cloud_server: nil, shelly_host: 'shelly-pro-3em') } let(:logger) { MemoryLogger.new } before do @@ -19,7 +19,7 @@ end it 'handles Interrupt' do - allow(config.adapter).to receive(:data).and_raise(SystemExit) + allow(config.adapter).to receive(:raw_response).and_raise(SystemExit) described_class.start(config:) @@ -27,7 +27,7 @@ end it 'handles errors' do - allow(config.adapter).to receive(:data).and_raise(StandardError) + allow(config.adapter).to receive(:raw_response).and_raise(StandardError) described_class.start(config:, max_count: 1) diff --git a/spec/lib/shelly_gen3_adapter_spec.rb b/spec/lib/shelly_cloud_adapter_spec.rb similarity index 64% rename from spec/lib/shelly_gen3_adapter_spec.rb rename to spec/lib/shelly_cloud_adapter_spec.rb index bd11034..cdca3c4 100644 --- a/spec/lib/shelly_gen3_adapter_spec.rb +++ b/spec/lib/shelly_cloud_adapter_spec.rb @@ -1,13 +1,12 @@ -require 'shelly_gen3_adapter' +require 'shelly_cloud_adapter' require 'config' -describe ShellyGen3Adapter do +describe ShellyCloudAdapter do subject(:adapter) do described_class.new(config:) end - let(:config) { Config.from_env(shelly_host:, shelly_gen: 3, shelly_interval: 5) } - let(:shelly_host) { '192.168.178.83' } + let(:config) { Config.from_env } let(:logger) { MemoryLogger.new } before do @@ -17,7 +16,11 @@ describe '#initialize' do before { adapter } - it { expect(logger.info_messages).to include('Pulling from your Shelly (Gen3) at http://192.168.178.83/rpc/Shelly.GetStatus every 5 seconds') } + it { + expect(logger.info_messages.join).to match( + /Pulling from Shelly \(Cloud\) for device .* every 5 seconds/, + ) + } end describe '#connection' do @@ -26,11 +29,9 @@ it { is_expected.to be_a(Faraday::Connection) } end - describe '#solectrus_record', vcr: 'shelly-pm-mini-gen3' do + describe '#solectrus_record', vcr: 'shelly-cloud' do subject(:solectrus_record) { adapter.solectrus_record } - let(:shelly_host) { 'shelly-pm-mini-gen3' } - it { is_expected.to be_a(SolectrusRecord) } it 'has an automatic id' do @@ -39,6 +40,13 @@ it 'has values' do expect(solectrus_record.power).to be > 0 + expect(solectrus_record.temp).to be > 0 + end + + it 'has phase power' do + expect(solectrus_record.power_a).to be >= 0 + expect(solectrus_record.power_b).to be >= 0 + expect(solectrus_record.power_c).to be >= 0 end it 'has a valid time' do diff --git a/spec/lib/shelly_gen1_adapter_spec.rb b/spec/lib/shelly_gen1_adapter_spec.rb deleted file mode 100644 index d31cda0..0000000 --- a/spec/lib/shelly_gen1_adapter_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -require 'shelly_gen1_adapter' -require 'config' - -describe ShellyGen1Adapter do - subject(:adapter) do - described_class.new(config:) - end - - let(:config) { Config.from_env(shelly_host:, shelly_gen: 1, shelly_interval: 5) } - let(:shelly_host) { 'shelly-3em' } - let(:logger) { MemoryLogger.new } - - before do - config.logger = logger - end - - describe '#initialize' do - before { adapter } - - it { expect(logger.info_messages).to include('Pulling from your Shelly (Gen1) at http://shelly-3em/status every 5 seconds') } - end - - describe '#connection' do - subject { adapter.connection } - - it { is_expected.to be_a(Faraday::Connection) } - end - - describe '#solectrus_record', vcr: 'shelly-3em' do # Manually created cassette! - subject(:solectrus_record) { adapter.solectrus_record } - - let(:shelly_host) { 'shelly-3em' } - - it { is_expected.to be_a(SolectrusRecord) } - - it 'has an automatic id' do - expect(solectrus_record.id).to eq(1) - end - - it 'has total power' do - expect(solectrus_record.power).to be > 0 - end - - it 'has phase power' do - expect(solectrus_record.power_a).to be >= 0 - expect(solectrus_record.power_b).to be >= 0 - expect(solectrus_record.power_c).to be >= 0 - end - - it 'has a valid time' do - expect(solectrus_record.time).to be > 1_700_000_000 - end - - it 'handles errors' do - allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) - - solectrus_record - expect(logger.error_messages).to include(/Error getting data from Shelly at/) - end - end - - describe '#solectrus_record', vcr: 'shelly-em' do # Manually created cassette! - subject(:solectrus_record) { adapter.solectrus_record } - - let(:shelly_host) { 'shelly-em' } - - it { is_expected.to be_a(SolectrusRecord) } - - it 'has an automatic id' do - expect(solectrus_record.id).to eq(1) - end - - it 'has total power' do - expect(solectrus_record.power).to be > 0 - end - - it 'has phase power' do - expect(solectrus_record.power_a).to be >= 0 - expect(solectrus_record.power_b).to be >= 0 - expect(solectrus_record.power_c).to be_nil - end - - it 'has a valid time' do - expect(solectrus_record.time).to be > 1_700_000_000 - end - - it 'handles errors' do - allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) - - solectrus_record - expect(logger.error_messages).to include(/Error getting data from Shelly at/) - end - end - - describe '#solectrus_record', vcr: 'shelly-plug-s-gen1' do # Manually created cassette! - subject(:solectrus_record) { adapter.solectrus_record } - - let(:shelly_host) { 'shelly-plug-s-gen1' } - - it { is_expected.to be_a(SolectrusRecord) } - - it 'has an automatic id' do - expect(solectrus_record.id).to eq(1) - end - - it 'has total power' do - expect(solectrus_record.power).to be > 0 - end - - it 'has phase power' do - expect(solectrus_record.power_a).to be >= 0 - expect(solectrus_record.power_b).to be_nil - expect(solectrus_record.power_c).to be_nil - end - - it 'has a valid time' do - expect(solectrus_record.time).to be > 1_700_000_000 - end - - it 'handles errors' do - allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) - - solectrus_record - expect(logger.error_messages).to include(/Error getting data from Shelly at/) - end - end -end diff --git a/spec/lib/shelly_gen2_adapter_spec.rb b/spec/lib/shelly_gen2_adapter_spec.rb deleted file mode 100644 index 69f7b15..0000000 --- a/spec/lib/shelly_gen2_adapter_spec.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'shelly_gen2_adapter' -require 'config' - -describe ShellyGen2Adapter do - subject(:adapter) do - described_class.new(config:) - end - - let(:config) { Config.from_env(shelly_host:, shelly_interval: 5) } - let(:shelly_host) { '192.168.178.83' } - let(:logger) { MemoryLogger.new } - - before do - config.logger = logger - end - - describe '#initialize' do - before { adapter } - - it { expect(logger.info_messages).to include('Pulling from your Shelly (Gen2) at http://192.168.178.83/rpc/Shelly.GetStatus every 5 seconds') } - end - - describe '#connection' do - subject { adapter.connection } - - it { is_expected.to be_a(Faraday::Connection) } - end - - describe '#solectrus_record', vcr: 'shelly-pro-3em' do - subject(:solectrus_record) { adapter.solectrus_record } - - let(:shelly_host) { 'shelly-pro-3em' } - - it { is_expected.to be_a(SolectrusRecord) } - - it 'has an automatic id' do - expect(solectrus_record.id).to eq(1) - end - - it 'has values' do - expect(solectrus_record.power).to be > 0 - expect(solectrus_record.temp).to be > 0 - end - - it 'has phase power' do - expect(solectrus_record.power_a).to be >= 0 - expect(solectrus_record.power_b).to be >= 0 - expect(solectrus_record.power_c).to be >= 0 - end - - it 'has a valid time' do - expect(solectrus_record.time).to be > 1_700_000_000 - end - - it 'handles errors' do - allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) - - solectrus_record - expect(logger.error_messages).to include(/Error getting data from Shelly at/) - end - end - - describe '#solectrus_record', vcr: 'shelly-plug-s-gen2' do - subject(:solectrus_record) { adapter.solectrus_record } - - let(:shelly_host) { 'shelly-plug-s-gen2' } - - it { is_expected.to be_a(SolectrusRecord) } - - it 'has an automatic id' do - expect(solectrus_record.id).to eq(1) - end - - it 'has values' do - expect(solectrus_record.power).to be > 0 - expect(solectrus_record.temp).to be > 0 - end - - it 'has a valid time' do - expect(solectrus_record.time).to be > 1_700_000_000 - end - - it 'handles errors' do - allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) - - solectrus_record - expect(logger.error_messages).to include(/Error getting data from Shelly at/) - end - end -end diff --git a/spec/lib/shelly_local_adapter_spec.rb b/spec/lib/shelly_local_adapter_spec.rb new file mode 100644 index 0000000..84be875 --- /dev/null +++ b/spec/lib/shelly_local_adapter_spec.rb @@ -0,0 +1,164 @@ +require 'shelly_local_adapter' +require 'config' + +describe ShellyLocalAdapter do + subject(:adapter) do + described_class.new(config:) + end + + let(:config) { Config.from_env(shelly_host:, shelly_interval: 5) } + let(:shelly_host) { '192.168.178.83' } + let(:logger) { MemoryLogger.new } + + before do + config.logger = logger + end + + describe '#initialize' do + before { adapter } + + it { expect(logger.info_messages).to include('Pulling from your Shelly at http://192.168.178.83 every 5 seconds') } + end + + describe '#connection' do + subject { adapter.connection } + + it { is_expected.to be_a(Faraday::Connection) } + end + + describe '#solectrus_record' do + subject(:solectrus_record) { adapter.solectrus_record } + + context 'when Shelly Pro 3EM', vcr: 'shelly-pro-3em' do + let(:shelly_host) { 'shelly-pro-3em' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + expect(solectrus_record.temp).to be > 0 + end + + it 'has phase power' do + expect(solectrus_record.power_a).to be >= 0 + expect(solectrus_record.power_b).to be >= 0 + expect(solectrus_record.power_c).to be >= 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + + context 'when Shelly Plug S (Gen2)', vcr: 'shelly-plug-s-gen2' do + subject(:solectrus_record) { adapter.solectrus_record } + + let(:shelly_host) { 'shelly-plug-s' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + expect(solectrus_record.temp).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + + context 'when Shelly PM Mini (Gen3)', vcr: 'shelly-pm-mini-gen3' do + subject(:solectrus_record) { adapter.solectrus_record } + + let(:shelly_host) { 'shelly-pm-mini-gen3' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + + context 'when Shelly PM Mini (Gen3)', vcr: 'shelly-plug-s-gen1' do + subject(:solectrus_record) { adapter.solectrus_record } + + let(:shelly_host) { 'shelly-plug-s-gen1' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + + context 'when Shelly EM', vcr: 'shelly-em' do + subject(:solectrus_record) { adapter.solectrus_record } + + let(:shelly_host) { 'shelly-em' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + + context 'when Shelly 3EM', vcr: 'shelly-3em' do + subject(:solectrus_record) { adapter.solectrus_record } + + let(:shelly_host) { 'shelly-3em' } + + it 'has values' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + + it 'handles errors' do + allow(Faraday::Adapter).to receive(:new).and_raise(StandardError) + + solectrus_record + expect(logger.error_messages).to include(/Error getting data from Shelly at/) + end + end + end +end diff --git a/spec/lib/shelly_response_parser_spec.rb b/spec/lib/shelly_response_parser_spec.rb new file mode 100644 index 0000000..2b853c4 --- /dev/null +++ b/spec/lib/shelly_response_parser_spec.rb @@ -0,0 +1,155 @@ +require 'shelly_response_parser' + +describe ShellyResponseParser do + subject(:parser) { described_class.new(json) } + + describe '#solectrus_record' do + subject(:solectrus_record) { parser.solectrus_record } + + context 'when Shelly Plug S (Gen1)' do + let(:json) { read_json('shelly-plug-s-gen1') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Shelly Plug S (Gen2)' do + let(:json) { read_json('shelly-plug-s-gen2') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has temp' do + expect(solectrus_record.temp).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Shelly Pro EM' do + let(:json) { read_json('shelly-em') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Shelly Pro 3EM (Gen2)' do + let(:json) { read_json('shelly-pro-3em') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has temp' do + expect(solectrus_record.temp).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Shelly PM Mini (Gen3)' do + let(:json) { read_json('shelly-pm-mini-gen3') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Shelly 3EM' do + let(:json) { read_json('shelly-3em') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + + context 'when Cloud' do + let(:json) { read_json('shelly-cloud') } + + it { is_expected.to be_a(SolectrusRecord) } + + it 'has an automatic id' do + expect(solectrus_record.id).to eq(1) + end + + it 'has power' do + expect(solectrus_record.power).to be > 0 + end + + it 'has temp' do + expect(solectrus_record.temp).to be > 0 + end + + it 'has a valid time' do + expect(solectrus_record.time).to be > 1_700_000_000 + end + end + end + + def read_json(cassette_name) + cassette_path = VCR.configuration.cassette_library_dir + "/#{cassette_name}.yml" + yaml_content = YAML.load_file(cassette_path) + http_interactions = yaml_content['http_interactions'] + + http_interactions.last.dig('response', 'body', 'string') + end +end diff --git a/spec/lib/solectrus_record_spec.rb b/spec/lib/solectrus_record_spec.rb index 038f3c8..265e2cb 100644 --- a/spec/lib/solectrus_record_spec.rb +++ b/spec/lib/solectrus_record_spec.rb @@ -1,7 +1,7 @@ require 'solectrus_record' describe SolectrusRecord do - subject(:record) { described_class.new(id: 1, time: Time.now, payload:) } + subject(:record) { described_class.new(id: 1, response_duration: 20.5, time: Time.now, payload:) } let(:payload) do { @@ -10,7 +10,6 @@ power_a: 10.2, power_b: 20.5, power_c: 30.3, - response_duration: 20.5, } end @@ -43,7 +42,13 @@ end end - %i[temp power power_a power_b power_c response_duration].each do |method| + describe '#response_duration' do + it 'returns the value' do + expect(record.response_duration).to eq(20.5) + end + end + + %i[temp power power_a power_b power_c].each do |method| describe "##{method}" do it "returns the value of #{method} from the payload" do expect(record.send(method)).to eq(payload[method]) diff --git a/spec/support/vcr_setup.rb b/spec/support/vcr_setup.rb index 5ece28d..18dca8c 100644 --- a/spec/support/vcr_setup.rb +++ b/spec/support/vcr_setup.rb @@ -11,6 +11,9 @@ INFLUX_TOKEN INFLUX_ORG INFLUX_BUCKET + SHELLY_CLOUD_SERVER + SHELLY_DEVICE_ID + SHELLY_AUTH_KEY ] sensitive_environment_variables.each do |key_name| config.filter_sensitive_data("<#{key_name}>") { ENV.fetch(key_name, nil) }