app/services/senec/connection_test.rb
Rama develop · réplica de lectura del repositorio original.
require 'net/http'
require 'uri'
module Senec
# Connection-test service for the SENEC survey (see ConnectionTesting).
# Covers local access only (SENEC.Home V2.1 / V3) — cloud access goes
# through mein-senec.de and is not probed here.
#
# The probe POSTs a tiny JSON-RPC request to `/lala.cgi` — the same endpoint
# the senec-collector reads from — and confirms the device echoes the
# requested `ENERGY` section back. Local devices answer over https with a
# self-signed certificate, so certificate verification is disabled.
class ConnectionTest
include ConnectionTesting::ResultBuilder
include Loggable
extend Loggable
# SENEC devices are slow embedded boxes; allow generous timeouts so a slow
# TLS handshake or a slow `/lala.cgi` response isn't mistaken for an
# unreachable device.
OPEN_TIMEOUT = 5
READ_TIMEOUT = 10
# Smallest request that still proves the host speaks the SENEC protocol:
# the device echoes the section back with the value hex-encoded.
PROBE_BODY = { 'ENERGY' => { 'STAT_STATE' => '' } }.freeze
# `values` carries the relevant survey answers keyed by their field name.
def call(check:, values:)
case check
when 'reachability' then reachability(values)
else result(false, :error)
end
end
private
def reachability(values)
schema, host = values.values_at('schema', 'host')
return result(false, :incomplete) if [schema, host].any?(&:blank?)
senec?(probe(schema, host)) ? result(true, :senec_reachable) : result(false, :senec_not_senec)
rescue *ConnectionTesting::Http::PEER_RESET_ERRORS => e
# The host accepted the connection, then reset it — something is there,
# but it does not speak the SENEC protocol.
warn_and_result(e, 'host is not SENEC', :senec_not_senec)
rescue *ConnectionTesting::Http::CONNECTION_ERRORS => e
warn_probe(e, 'unreachable')
unreachable_result(host, :senec_unreachable)
rescue StandardError => e
warn_and_result(e, 'failed', :error)
end
def warn_and_result(error, context, reason)
warn_probe(error, context)
result(false, reason)
end
def warn_probe(error, context)
logger.warn("SENEC reachability check — #{context} (#{error.class}): #{error.message}")
end
# A SENEC device answers 2xx and echoes the `ENERGY` object back — that
# distinguishes it from some other service that happens to answer 2xx.
def senec?(response)
return false unless response.is_a?(Net::HTTPSuccess)
body = JSON.parse(response.body)
body.is_a?(Hash) && body['ENERGY'].is_a?(Hash)
rescue JSON::ParserError
false
end
def probe(schema, host)
uri = URI("#{schema}://#{host}/lala.cgi")
with_http(schema, host) do |http|
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = PROBE_BODY.to_json
http.request(request)
end
end
# SENEC has no port field — the device answers on the schema's default
# port (443 for https, 80 for http).
def with_http(schema, host, &)
ConnectionTesting::Http.start(
host:, port: (schema == 'https' ? 443 : 80), schema:,
open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT, verify: false, &
)
end
end
end