fritzbox-smarthome 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8e0195a6e63ff7455d046a2795abecb92cc7f90534f2e66780b7b55a36e867c
4
- data.tar.gz: b3813af3a7b38b0fac17bb83bd24bea5d312cd7ab0db5c5adba39cb983599547
3
+ metadata.gz: c4ed289626e1d8c8cacccb9f89eed39178c5b11c781c99a649b3872521a3d0d4
4
+ data.tar.gz: 02b35061da61c6f1084b87192eefe55cc83113526addf7caff584240256997f7
5
5
  SHA512:
6
- metadata.gz: 0ba494fb0ba09894f97696a94c471f486cec8bc820fe1c02467f2071fc384bb94df6864e0f4d85e399e7851e6123061c036093048b7cde96369aa02352d38e92
7
- data.tar.gz: f58785489fab952caa6a37c10a32385f7eb14d55f1712ea034f2fced9e98b480840c6277642e264a76e902aae4166fd87c84e11deacff4165b342cd27b50ec4b
6
+ metadata.gz: d2840a781e1717f8e9b11fa1a18929b316c930e57ee3b5e57a24574ad2f065c1b44fa3837e85dc0e88367ff1f402826d5210d8bbf2b9a9079b48dbe655a9d1e8
7
+ data.tar.gz: 02f972de29e194f5c42f7671c3caddfdfe046951e0fd91095f4614412c5afb76a1861ed71211f89d0e44b52ff909dd79df4f7e93d558d50e9903d217ab0e8be4
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.1.2
1
+ 3.2.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.7.0
4
+
5
+ * Measure HTTP request duration
6
+ * Print the request time in debug log-level
7
+ * Cache authentication between requests
8
+ * Keep the returned SessionID for 60 minutes in memory
9
+
10
+ ## v0.6.0
11
+
12
+ * Add support for `Actor.find_by!(ain:)` and `Actor#reload`
13
+
3
14
  ## v0.5.0
4
15
 
5
16
  * Unify on/off interface
data/README.md CHANGED
@@ -41,6 +41,12 @@ actors = Fritzbox::Smarthome::Actor.all
41
41
  # Get all actors of type Heater
42
42
  heaters = Fritzbox::Smarthome::Heater.all
43
43
  heaters.last.update_hkr_temp_set(BigDecimal('21.5'))
44
+
45
+ # Get a specific actor via it's AIN
46
+ actor = Fritzbox::Smarthome::Actor.find_by!(ain: '0815 4711')
47
+
48
+ # Reload the data of an already fetched actor
49
+ actor.reload
44
50
  ```
45
51
 
46
52
  ## Development
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
- f.match(%r{^(test|spec|features)/})
17
+ f.match(%r{^(test|spec|features|examples)/})
18
18
  end
19
19
  spec.bindir = 'exe'
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
@@ -4,36 +4,63 @@ module Fritzbox
4
4
  include ActiveModel::Model
5
5
 
6
6
  attr_accessor \
7
- :id,
8
- :type,
9
- :ain,
10
- :present,
11
- :name,
12
- :manufacturer,
13
- :group_members
7
+ :id,
8
+ :type,
9
+ :ain,
10
+ :present,
11
+ :name,
12
+ :manufacturer,
13
+ :group_members
14
+
15
+ ResourceNotFound = Class.new(RuntimeError)
14
16
 
15
17
  class << self
16
18
  def all(types: ['group', 'device'])
17
- response = get(command: 'getdevicelistinfos')
18
- xml = nori.parse(response.body)
19
+ xml = parse(get(command: 'getdevicelistinfos'))
20
+
19
21
  Array.wrap(types.map { |type| xml.dig('devicelist', type) }.flatten).compact.map do |data|
20
22
  klass = Actor.descendants.find { |k| k.match?(data) } || Actor
21
23
  self.in?([klass, Actor]) ? klass.new_from_api(data) : nil
22
24
  end.compact
23
25
  end
24
26
 
27
+ def find_by!(ain: nil)
28
+ data = parse(get(command: 'getdeviceinfos', ain: ain)).fetch('device')
29
+ klass = Actor.descendants.find { |k| k.match?(data) } || Actor
30
+
31
+ instance = klass.new(ain: ain)
32
+ instance.assign_from_api(data)
33
+ instance
34
+ rescue KeyError
35
+ raise ResourceNotFound, "Unable to find actor with ain='#{ain}'"
36
+ end
37
+
25
38
  def new_from_api(data)
26
- new(
27
- id: data.dig('@id').to_s,
28
- type: data.dig('groupinfo').present? ? :group : :device,
29
- ain: data.dig('@identifier').to_s,
30
- present: data.dig('present') == '1',
31
- name: (data.dig('name') || data.dig('@productname')).to_s,
32
- manufacturer: (data.dig('manufacturer') || data.dig('@manufacturer')).to_s,
33
- group_members: data.dig('groupinfo', 'members').to_s.split(',').presence
34
- )
39
+ instance = new
40
+ instance.assign_from_api(data)
41
+ instance
35
42
  end
36
43
  end
44
+
45
+ def assign_from_api(data)
46
+ assign_attributes(
47
+ id: data.dig('@id').to_s,
48
+ type: data.dig('groupinfo').present? ? :group : :device,
49
+ ain: data.dig('@identifier').to_s,
50
+ present: data.dig('present') == '1',
51
+ name: (data.dig('name') || data.dig('@productname')).to_s,
52
+ manufacturer: (data.dig('manufacturer') || data.dig('@manufacturer')).to_s,
53
+ group_members: data.dig('groupinfo', 'members').to_s.split(',').presence
54
+ )
55
+ end
56
+
57
+ def reload
58
+ xml = parse(get(command: 'getdeviceinfos', ain: ain))
59
+ assign_from_api(xml.fetch('device'))
60
+ self
61
+ rescue KeyError
62
+ raise ResourceNotFound, "Unable to reload actor with ain='#{ain}'"
63
+ end
37
64
  end
38
65
  end
39
66
  end
@@ -14,19 +14,19 @@ module Fritzbox
14
14
  def match?(data)
15
15
  data.key?('hkr')
16
16
  end
17
+ end
17
18
 
18
- def new_from_api(data)
19
- instance = super
20
- instance.assign_attributes(
21
- battery: data.dig('battery').to_i,
22
- batterylow: data.dig('batterylow').to_i,
23
- hkr_temp_is: data.dig('hkr', 'tist').to_i * 0.5,
24
- hkr_temp_set: data.dig('hkr', 'tsoll').to_i * 0.5,
25
- hkr_next_change_period: Time.at(data.dig('hkr', 'nextchange', 'endperiod').to_i),
26
- hkr_next_change_temp: data.dig('hkr', 'nextchange', 'tchange').to_i * 0.5
27
- )
28
- instance
29
- end
19
+ def assign_from_api(data)
20
+ super(data)
21
+
22
+ assign_attributes(
23
+ battery: data.dig('battery').to_i,
24
+ batterylow: data.dig('batterylow').to_i,
25
+ hkr_temp_is: data.dig('hkr', 'tist').to_i * 0.5,
26
+ hkr_temp_set: data.dig('hkr', 'tsoll').to_i * 0.5,
27
+ hkr_next_change_period: Time.at(data.dig('hkr', 'nextchange', 'endperiod').to_i),
28
+ hkr_next_change_temp: data.dig('hkr', 'nextchange', 'tchange').to_i * 0.5
29
+ )
30
30
  end
31
31
 
32
32
  def update_hkr_temp_set(value)
@@ -1,6 +1,10 @@
1
1
  module Fritzbox
2
2
  module Smarthome
3
3
  class Resource
4
+ cattr_accessor :session
5
+
6
+ AuthenticationError = Class.new(RuntimeError)
7
+
4
8
  class << self
5
9
  # @param params [Hash] key/value pairs that will be appended to the switchcmd query string
6
10
  def get(command:, ain: nil, param: nil, **params)
@@ -12,9 +16,20 @@ module Fritzbox
12
16
  url = "#{url}&#{key}=#{value}"
13
17
  end
14
18
 
15
- config.logger.debug(url)
19
+ response = measure(url) { HTTParty.get(url, **httparty_options) }
20
+
21
+ raise AuthenticationError if response.code == 403
22
+
23
+ response
24
+ rescue AuthenticationError
25
+ raise if session.nil?
16
26
 
17
- HTTParty.get(url, **httparty_options)
27
+ self.session = nil
28
+ retry
29
+ end
30
+
31
+ def parse(response)
32
+ nori.parse(response.body)
18
33
  end
19
34
 
20
35
  private
@@ -22,19 +37,28 @@ module Fritzbox
22
37
  delegate :config, to: Smarthome
23
38
 
24
39
  def authenticate
25
- response = HTTParty.get(login_endpoint, **httparty_options)
26
- xml = nori.parse(response.body)
27
- challenge = xml.dig('SessionInfo', 'Challenge')
40
+ return session.id if session.present? && session.valid?
41
+
42
+ session_id = measure("authentication") do
43
+ response = HTTParty.get(login_endpoint, **httparty_options)
44
+ xml = nori.parse(response.body)
45
+ challenge = xml.dig('SessionInfo', 'Challenge')
28
46
 
29
- md5 = Digest::MD5.hexdigest("#{challenge}-#{config.password}".encode('UTF-16LE'))
47
+ md5 = Digest::MD5.hexdigest("#{challenge}-#{config.password}".encode('UTF-16LE'))
30
48
 
31
- url = "#{login_endpoint}?response=#{challenge}-#{md5}"
32
- url = "#{url}&username=#{config.username}" if config.username.present?
49
+ url = "#{login_endpoint}?response=#{challenge}-#{md5}"
50
+ url = "#{url}&username=#{config.username}" if config.username.present?
33
51
 
34
- response = HTTParty.get(url, **httparty_options)
52
+ response = HTTParty.get(url, **httparty_options)
35
53
 
36
- xml = nori.parse(response.body)
37
- xml.dig('SessionInfo', 'SID')
54
+ xml = nori.parse(response.body)
55
+
56
+ xml.dig('SessionInfo', 'SID')
57
+ end
58
+
59
+ self.session = Session.new(session_id)
60
+
61
+ session_id
38
62
  end
39
63
 
40
64
  def login_endpoint
@@ -51,7 +75,17 @@ module Fritzbox
51
75
  def nori
52
76
  @nori ||= Nori.new
53
77
  end
78
+
79
+ def measure(identifier, &block)
80
+ time_start = Time.now
81
+ result = block.call
82
+ time_elapsed = (Time.now - time_start).to_f.round(3)
83
+ config.logger.debug("Request `#{identifier}` took #{time_elapsed} seconds")
84
+ result
85
+ end
54
86
  end
87
+
88
+ delegate :get, :parse, to: :class
55
89
  end
56
90
  end
57
91
  end
@@ -0,0 +1,22 @@
1
+ module Fritzbox
2
+ module Smarthome
3
+ class Session
4
+ TIMEOUT_MINUTES = 60
5
+
6
+ def initialize(id)
7
+ self.id = id
8
+ self.valid_until = Time.now + TIMEOUT_MINUTES.minutes
9
+ end
10
+
11
+ def valid?
12
+ self.valid_until > Time.now
13
+ end
14
+
15
+ attr_reader :id, :valid_until
16
+
17
+ private
18
+
19
+ attr_writer :id, :valid_until
20
+ end
21
+ end
22
+ end
@@ -10,15 +10,15 @@ module Fritzbox
10
10
  def match?(data)
11
11
  data.key?('alert')
12
12
  end
13
+ end
13
14
 
14
- def new_from_api(data)
15
- instance = super
16
- instance.assign_attributes(
17
- alert_state: data.dig('alert', 'state').to_i,
18
- last_alert: Time.at(data.dig('alert', 'lastalertchgtimestamp').to_i)
19
- )
20
- instance
21
- end
15
+ def assign_from_api(data)
16
+ super(data)
17
+
18
+ assign_attributes(
19
+ alert_state: data.dig('alert', 'state').to_i,
20
+ last_alert: Time.at(data.dig('alert', 'lastalertchgtimestamp').to_i)
21
+ )
22
22
  end
23
23
  end
24
24
  end
@@ -19,22 +19,22 @@ module Fritzbox
19
19
  def match?(data)
20
20
  data.key?('switch')
21
21
  end
22
+ end
22
23
 
23
- def new_from_api(data)
24
- instance = super
25
- instance.assign_attributes(
26
- switch_state: data.dig('switch', 'state').to_i,
27
- switch_mode: data.dig('switch', 'mode').to_s,
28
- switch_lock: data.dig('switch', 'lock').to_i,
29
- switch_devicelock: data.dig('switch', 'devicelock').to_i,
30
- powermeter_voltage: data.dig('powermeter', 'voltage').to_i,
31
- powermeter_power: data.dig('powermeter', 'power').to_i,
32
- powermeter_energy: data.dig('powermeter', 'energy').to_i,
33
- temperature_celsius: data.dig('temperature', 'celsius').to_i,
34
- temperature_offset: data.dig('temperature', 'offset').to_i
35
- )
36
- instance
37
- end
24
+ def assign_from_api(data)
25
+ super(data)
26
+
27
+ assign_attributes(
28
+ switch_state: data.dig('switch', 'state').to_i,
29
+ switch_mode: data.dig('switch', 'mode').to_s,
30
+ switch_lock: data.dig('switch', 'lock').to_i,
31
+ switch_devicelock: data.dig('switch', 'devicelock').to_i,
32
+ powermeter_voltage: data.dig('powermeter', 'voltage').to_i,
33
+ powermeter_power: data.dig('powermeter', 'power').to_i,
34
+ powermeter_energy: data.dig('powermeter', 'energy').to_i,
35
+ temperature_celsius: data.dig('temperature', 'celsius').to_i,
36
+ temperature_offset: data.dig('temperature', 'offset').to_i
37
+ )
38
38
  end
39
39
  end
40
40
  end
@@ -1,5 +1,5 @@
1
1
  module Fritzbox
2
2
  module Smarthome
3
- VERSION = '0.5.0'.freeze
3
+ VERSION = '0.7.0'.freeze
4
4
  end
5
5
  end
@@ -6,6 +6,7 @@ require 'nori'
6
6
  require 'fritzbox/smarthome/version'
7
7
  require 'fritzbox/smarthome/null_logger'
8
8
  require 'fritzbox/smarthome/properties'
9
+ require 'fritzbox/smarthome/session'
9
10
  require 'fritzbox/smarthome/resource'
10
11
  require 'fritzbox/smarthome/actor'
11
12
  require 'fritzbox/smarthome/heater'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fritzbox-smarthome
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Klaus Meyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-09 00:00:00.000000000 Z
11
+ date: 2022-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -176,6 +176,7 @@ files:
176
176
  - lib/fritzbox/smarthome/properties.rb
177
177
  - lib/fritzbox/smarthome/properties/simple_on_off.rb
178
178
  - lib/fritzbox/smarthome/resource.rb
179
+ - lib/fritzbox/smarthome/session.rb
179
180
  - lib/fritzbox/smarthome/smoke_detector.rb
180
181
  - lib/fritzbox/smarthome/switch.rb
181
182
  - lib/fritzbox/smarthome/version.rb
@@ -198,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
199
  - !ruby/object:Gem::Version
199
200
  version: '0'
200
201
  requirements: []
201
- rubygems_version: 3.3.7
202
+ rubygems_version: 3.4.1
202
203
  signing_key:
203
204
  specification_version: 4
204
205
  summary: Client library to interface with Smarthome features of your FritzBox