senec 0.22.1 → 0.24.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.
@@ -8,7 +8,7 @@ module Senec
8
8
 
9
9
  CLIENT_ID = 'endcustomer-app-frontend'.freeze
10
10
  REDIRECT_URI = 'senec-app-auth://keycloak.prod'.freeze
11
- SCOPE = 'roles meinsenec'.freeze
11
+ SCOPE = 'roles profile meinsenec'.freeze
12
12
 
13
13
  SYSTEMS_HOST = 'https://senec-app-systems-proxy.prod.senec.dev'.freeze
14
14
  MEASUREMENTS_HOST = 'https://senec-app-measurements-proxy.prod.senec.dev'.freeze
@@ -40,8 +40,8 @@ module Senec
40
40
  )
41
41
 
42
42
  # Manual HTTP needed for Keycloak cross-domain form handling
43
- login_form_url = fetch_login_form_url(auth_url)
44
- redirect_url = submit_credentials(login_form_url)
43
+ form_type, form_url = fetch_login_form(auth_url)
44
+ redirect_url = submit_credentials(form_type, form_url)
45
45
  authorization_code = extract_authorization_code(redirect_url)
46
46
 
47
47
  self.oauth_token =
@@ -57,38 +57,58 @@ module Senec
57
57
  end
58
58
 
59
59
  def systems
60
- get "#{SYSTEMS_HOST}/v1/systems"
60
+ get "#{SYSTEMS_HOST}/systems/api/v1"
61
61
  end
62
62
 
63
63
  def system_details(system_id)
64
- get "#{SYSTEMS_HOST}/systems/#{system_id}/details"
64
+ get "#{SYSTEMS_HOST}/systems/api/v1/#{system_id}/details"
65
65
  end
66
66
 
67
67
  def dashboard(system_id)
68
- get "#{MEASUREMENTS_HOST}/v1/systems/#{system_id}/dashboard"
68
+ get "#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/dashboard"
69
69
  end
70
70
 
71
71
  def wallbox(system_id, wallbox_id)
72
- get "#{WALLBOX_HOST}/v1/systems/#{system_id}/wallboxes/#{wallbox_id}"
72
+ get "#{WALLBOX_HOST}/wallbox/api/v1/systems/#{system_id}/wallboxes/#{wallbox_id}"
73
73
  end
74
74
 
75
75
  def wallbox_search(system_id)
76
- post "#{WALLBOX_HOST}/v1/systems/wallboxes/search", { systemIds: [system_id] }
76
+ post "#{WALLBOX_HOST}/wallbox/api/v1/systems/wallboxes/search", { systemIds: [system_id] }
77
+ end
78
+
79
+ def measurements(system_id, resolution:, from:, to:)
80
+ params = URI.encode_www_form(
81
+ resolution:,
82
+ from: from.strftime('%Y-%m-%dT%H:%M:%SZ'),
83
+ to: to.strftime('%Y-%m-%dT%H:%M:%SZ'),
84
+ )
85
+
86
+ get "#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/measurements?#{params}"
87
+ end
88
+
89
+ def data_availability(system_id, timezone:)
90
+ params = URI.encode_www_form(timezone:)
91
+
92
+ get "#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/data-availability/timespan?#{params}"
77
93
  end
78
94
 
79
95
  private
80
96
 
81
97
  attr_accessor :oauth_token
82
98
 
83
- def fetch_login_form_url(auth_url)
99
+ def fetch_login_form(auth_url)
84
100
  response = http_request(:get, auth_url)
85
101
  store_cookies(response) # Required for Keycloak CSRF protection
86
- extract_login_form_action_url(response.body) || raise('Login form not found')
102
+ detect_login_form(response.body) || raise('Login form not found')
87
103
  end
88
104
 
89
- def submit_credentials(form_url)
90
- credentials = { username:, password: }
91
- response = http_request(:post, form_url, data: credentials)
105
+ def submit_credentials(form_type, form_url)
106
+ case form_type
107
+ when :username_and_password
108
+ response = http_request(:post, form_url, data: { username:, password: })
109
+ when :username_only
110
+ response = submit_username_then_password(form_url)
111
+ end
92
112
 
93
113
  if response.status == 200
94
114
  # Check if MFA is required
@@ -108,6 +128,20 @@ module Senec
108
128
  handle_redirect_response(response)
109
129
  end
110
130
 
131
+ def submit_username_then_password(form_url)
132
+ response = http_request(:post, form_url, data: { username: })
133
+ store_cookies(response)
134
+
135
+ raise 'Password form not found' unless response.status == 200
136
+
137
+ password_form_url = find_form_action_url(response.body) do |f|
138
+ f.match(/name=["']?password["']?/i)
139
+ end
140
+ raise 'Password form not found' unless password_form_url
141
+
142
+ http_request(:post, password_form_url, data: { username:, password: })
143
+ end
144
+
111
145
  def submit_totp_form(form_url)
112
146
  totp = build_totp_from_uri(totp_uri)
113
147
 
@@ -124,12 +158,21 @@ module Senec
124
158
  params['code'] || raise('No authorization code found')
125
159
  end
126
160
 
127
- def extract_login_form_action_url(html)
128
- find_form_action_url(html) do |form|
129
- # Look for a form with username and password fields
161
+ def detect_login_form(html)
162
+ # Check for combined form first (backward compatible with old SENEC flow)
163
+ url = find_form_action_url(html) do |form|
130
164
  form.match(/name=["']?username["']?/i) &&
131
165
  form.match(/name=["']?password["']?/i)
132
166
  end
167
+ return [:username_and_password, url] if url
168
+
169
+ # Check for username-only form (new two-step SENEC flow)
170
+ url = find_form_action_url(html) do |form|
171
+ form.match(/name=["']?username["']?/i)
172
+ end
173
+ return [:username_only, url] if url
174
+
175
+ nil
133
176
  end
134
177
 
135
178
  def extract_totp_form_action_url(html)
@@ -186,10 +229,10 @@ module Senec
186
229
  response = oauth_token.get(url)
187
230
  JSON.parse(response.body)
188
231
  rescue StandardError => e
189
- # :nocov:
232
+ # simplecov:disable
190
233
  warn "API error: #{e.message}"
191
234
  nil
192
- # :nocov:
235
+ # simplecov:enable
193
236
  end
194
237
 
195
238
  def post(url, data)
@@ -203,10 +246,10 @@ module Senec
203
246
 
204
247
  JSON.parse(response.body)
205
248
  rescue StandardError => e
206
- # :nocov:
249
+ # simplecov:disable
207
250
  warn "API error: #{e.message}"
208
251
  nil
209
- # :nocov:
252
+ # simplecov:enable
210
253
  end
211
254
 
212
255
  def http_request(method, url, data: nil)
@@ -234,9 +277,9 @@ module Senec
234
277
  def openid_config
235
278
  @openid_config ||= JSON.parse(http_request(:get, CONFIG_URL).body)
236
279
  rescue StandardError => e
237
- # :nocov:
280
+ # simplecov:disable
238
281
  raise "Failed to load OpenID configuration: #{e.message}"
239
- # :nocov:
282
+ # simplecov:enable
240
283
  end
241
284
 
242
285
  def cookies
data/lib/senec/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Senec
2
- VERSION = '0.22.1'.freeze
2
+ VERSION = '0.24.0'.freeze
3
3
  end
Binary file
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: senec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.1
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georg Ledermann
@@ -96,15 +96,16 @@ files:
96
96
  - ".ruby-lsp/.gitignore"
97
97
  - ".ruby-lsp/Gemfile"
98
98
  - ".ruby-lsp/Gemfile.lock"
99
+ - ".ruby-lsp/freshness_hash"
99
100
  - ".ruby-lsp/last_updated"
100
101
  - ".ruby-lsp/main_lockfile_hash"
101
- - ".ruby-lsp/needs_update"
102
102
  - ".vscode/settings.json"
103
103
  - CODE_OF_CONDUCT.md
104
104
  - LICENSE
105
105
  - README.md
106
106
  - Rakefile
107
107
  - coverage/.last_run.json
108
+ - coverage/.report_stamp
108
109
  - coverage/.resultset.json
109
110
  - coverage/.resultset.json.lock
110
111
  - coverage/assets/0.13.2/DataTables-1.10.20/images/sort_asc.png
@@ -166,6 +167,8 @@ files:
166
167
  - pkg/senec-0.20.0.gem
167
168
  - pkg/senec-0.21.0.gem
168
169
  - pkg/senec-0.22.0.gem
170
+ - pkg/senec-0.22.1.gem
171
+ - pkg/senec-0.23.0.gem
169
172
  - pkg/senec-0.3.0.gem
170
173
  - pkg/senec-0.4.0.gem
171
174
  - pkg/senec-0.5.0.gem
@@ -199,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
202
  - !ruby/object:Gem::Version
200
203
  version: '0'
201
204
  requirements: []
202
- rubygems_version: 3.7.1
205
+ rubygems_version: 4.0.19
203
206
  specification_version: 4
204
207
  summary: Unofficial Ruby Client for SENEC Home
205
208
  test_files: []
File without changes