senec 0.23.0 → 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.
@@ -1,9 +1,40 @@
1
1
  {
2
+ "$schema": "https://raw.githubusercontent.com/simplecov-ruby/simplecov/main/schemas/coverage-v1.0.schema.json",
2
3
  "meta": {
3
- "simplecov_version": "0.22.0"
4
+ "schema_version": "1.0",
5
+ "simplecov_version": "1.1.1",
6
+ "command_name": "RSpec",
7
+ "project_name": "Senec",
8
+ "timestamp": "2026-09-03T17:12:12.173+02:00",
9
+ "root": "/Users/ledermann/Projects/solectrus/senec",
10
+ "commit": "15f873f607ff3a55406b4b0ded204567970eb10f",
11
+ "primary_coverage": "line",
12
+ "line_coverage": true,
13
+ "branch_coverage": false,
14
+ "method_coverage": false
15
+ },
16
+ "total": {
17
+ "lines": {
18
+ "covered": 297,
19
+ "missed": 2,
20
+ "omitted": 361,
21
+ "total": 299,
22
+ "percent": 99.33110367892976,
23
+ "strength": 13.494983277591974
24
+ }
4
25
  },
5
26
  "coverage": {
6
- "/Users/ledermann/Projects/solectrus/senec/lib/senec.rb": {
27
+ "lib/senec.rb": {
28
+ "source": [
29
+ "require 'senec/version'",
30
+ "require 'senec/local/connection'",
31
+ "require 'senec/local/state'",
32
+ "require 'senec/local/request'",
33
+ "require 'senec/local/error'",
34
+ "",
35
+ "require 'senec/cloud/connection'",
36
+ "require 'senec/cloud/error'"
37
+ ],
7
38
  "lines": [
8
39
  1,
9
40
  1,
@@ -13,9 +44,322 @@
13
44
  null,
14
45
  1,
15
46
  1
16
- ]
47
+ ],
48
+ "lines_covered_percent": 100.0,
49
+ "covered_lines": 7,
50
+ "missed_lines": 0,
51
+ "omitted_lines": 1,
52
+ "total_lines": 7
17
53
  },
18
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/cloud/connection.rb": {
54
+ "lib/senec/cloud/connection.rb": {
55
+ "source": [
56
+ "require 'oauth2'",
57
+ "require 'rotp'",
58
+ "",
59
+ "module Senec",
60
+ " module Cloud",
61
+ " CONFIG_URL =",
62
+ " 'https://sso.senec.com/realms/senec/.well-known/openid-configuration'.freeze",
63
+ "",
64
+ " CLIENT_ID = 'endcustomer-app-frontend'.freeze",
65
+ " REDIRECT_URI = 'senec-app-auth://keycloak.prod'.freeze",
66
+ " SCOPE = 'roles profile meinsenec'.freeze",
67
+ "",
68
+ " SYSTEMS_HOST = 'https://senec-app-systems-proxy.prod.senec.dev'.freeze",
69
+ " MEASUREMENTS_HOST = 'https://senec-app-measurements-proxy.prod.senec.dev'.freeze",
70
+ " WALLBOX_HOST = 'https://senec-app-wallbox-proxy.prod.senec.dev'.freeze",
71
+ "",
72
+ " class Connection",
73
+ " DEFAULT_USER_AGENT = \"ruby-senec/#{Senec::VERSION} (+https://github.com/solectrus/senec)\".freeze",
74
+ "",
75
+ " def initialize(username:, password:, user_agent: DEFAULT_USER_AGENT, totp_uri: nil)",
76
+ " @username = username",
77
+ " @password = password",
78
+ " @user_agent = user_agent",
79
+ " @totp_uri = totp_uri",
80
+ " end",
81
+ "",
82
+ " attr_reader :username, :password, :user_agent, :totp_uri",
83
+ "",
84
+ " def authenticate!",
85
+ " code_verifier = SecureRandom.alphanumeric(43)",
86
+ " digest = Digest::SHA256.digest(code_verifier)",
87
+ " code_challenge = Base64.urlsafe_encode64(digest).delete('=')",
88
+ "",
89
+ " auth_url =",
90
+ " oauth_client.auth_code.authorize_url(",
91
+ " redirect_uri: REDIRECT_URI,",
92
+ " scope: SCOPE,",
93
+ " code_challenge:,",
94
+ " code_challenge_method: 'S256',",
95
+ " )",
96
+ "",
97
+ " # Manual HTTP needed for Keycloak cross-domain form handling",
98
+ " form_type, form_url = fetch_login_form(auth_url)",
99
+ " redirect_url = submit_credentials(form_type, form_url)",
100
+ " authorization_code = extract_authorization_code(redirect_url)",
101
+ "",
102
+ " self.oauth_token =",
103
+ " oauth_client.auth_code.get_token(",
104
+ " authorization_code,",
105
+ " redirect_uri: REDIRECT_URI,",
106
+ " code_verifier:,",
107
+ " )",
108
+ " end",
109
+ "",
110
+ " def authenticated?",
111
+ " !!oauth_token",
112
+ " end",
113
+ "",
114
+ " def systems",
115
+ " get \"#{SYSTEMS_HOST}/systems/api/v1\"",
116
+ " end",
117
+ "",
118
+ " def system_details(system_id)",
119
+ " get \"#{SYSTEMS_HOST}/systems/api/v1/#{system_id}/details\"",
120
+ " end",
121
+ "",
122
+ " def dashboard(system_id)",
123
+ " get \"#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/dashboard\"",
124
+ " end",
125
+ "",
126
+ " def wallbox(system_id, wallbox_id)",
127
+ " get \"#{WALLBOX_HOST}/wallbox/api/v1/systems/#{system_id}/wallboxes/#{wallbox_id}\"",
128
+ " end",
129
+ "",
130
+ " def wallbox_search(system_id)",
131
+ " post \"#{WALLBOX_HOST}/wallbox/api/v1/systems/wallboxes/search\", { systemIds: [system_id] }",
132
+ " end",
133
+ "",
134
+ " def measurements(system_id, resolution:, from:, to:)",
135
+ " params = URI.encode_www_form(",
136
+ " resolution:,",
137
+ " from: from.strftime('%Y-%m-%dT%H:%M:%SZ'),",
138
+ " to: to.strftime('%Y-%m-%dT%H:%M:%SZ'),",
139
+ " )",
140
+ "",
141
+ " get \"#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/measurements?#{params}\"",
142
+ " end",
143
+ "",
144
+ " def data_availability(system_id, timezone:)",
145
+ " params = URI.encode_www_form(timezone:)",
146
+ "",
147
+ " get \"#{MEASUREMENTS_HOST}/measurements/api/v1/systems/#{system_id}/data-availability/timespan?#{params}\"",
148
+ " end",
149
+ "",
150
+ " private",
151
+ "",
152
+ " attr_accessor :oauth_token",
153
+ "",
154
+ " def fetch_login_form(auth_url)",
155
+ " response = http_request(:get, auth_url)",
156
+ " store_cookies(response) # Required for Keycloak CSRF protection",
157
+ " detect_login_form(response.body) || raise('Login form not found')",
158
+ " end",
159
+ "",
160
+ " def submit_credentials(form_type, form_url)",
161
+ " case form_type",
162
+ " when :username_and_password",
163
+ " response = http_request(:post, form_url, data: { username:, password: })",
164
+ " when :username_only",
165
+ " response = submit_username_then_password(form_url)",
166
+ " end",
167
+ "",
168
+ " if response.status == 200",
169
+ " # Check if MFA is required",
170
+ " if (totp_form_url = extract_totp_form_action_url(response.body))",
171
+ " unless totp_uri",
172
+ " raise 'MFA required but no TOTP URI provided'",
173
+ " end",
174
+ "",
175
+ " response = submit_totp_form(totp_form_url)",
176
+ " else",
177
+ " # No OTP form found, assume login failed",
178
+ " raise 'Login failed - invalid credentials or unexpected response'",
179
+ " end",
180
+ " end",
181
+ "",
182
+ " # Handle final redirect (same for both MFA and non-MFA flows)",
183
+ " handle_redirect_response(response)",
184
+ " end",
185
+ "",
186
+ " def submit_username_then_password(form_url)",
187
+ " response = http_request(:post, form_url, data: { username: })",
188
+ " store_cookies(response)",
189
+ "",
190
+ " raise 'Password form not found' unless response.status == 200",
191
+ "",
192
+ " password_form_url = find_form_action_url(response.body) do |f|",
193
+ " f.match(/name=[\"']?password[\"']?/i)",
194
+ " end",
195
+ " raise 'Password form not found' unless password_form_url",
196
+ "",
197
+ " http_request(:post, password_form_url, data: { username:, password: })",
198
+ " end",
199
+ "",
200
+ " def submit_totp_form(form_url)",
201
+ " totp = build_totp_from_uri(totp_uri)",
202
+ "",
203
+ " totp_data = { otp: totp.now }",
204
+ " http_request(:post, form_url, data: totp_data)",
205
+ " end",
206
+ "",
207
+ " def extract_authorization_code(redirect_url)",
208
+ " raise 'Invalid redirect URL' unless redirect_url&.start_with?(REDIRECT_URI)",
209
+ "",
210
+ " uri = URI(redirect_url)",
211
+ " params = URI.decode_www_form(uri.query).to_h",
212
+ "",
213
+ " params['code'] || raise('No authorization code found')",
214
+ " end",
215
+ "",
216
+ " def detect_login_form(html)",
217
+ " # Check for combined form first (backward compatible with old SENEC flow)",
218
+ " url = find_form_action_url(html) do |form|",
219
+ " form.match(/name=[\"']?username[\"']?/i) &&",
220
+ " form.match(/name=[\"']?password[\"']?/i)",
221
+ " end",
222
+ " return [:username_and_password, url] if url",
223
+ "",
224
+ " # Check for username-only form (new two-step SENEC flow)",
225
+ " url = find_form_action_url(html) do |form|",
226
+ " form.match(/name=[\"']?username[\"']?/i)",
227
+ " end",
228
+ " return [:username_only, url] if url",
229
+ "",
230
+ " nil",
231
+ " end",
232
+ "",
233
+ " def extract_totp_form_action_url(html)",
234
+ " find_form_action_url(html) do |form|",
235
+ " # Look for a form with an OTP field",
236
+ " form.match(/name=[\"']?otp[\"']?/i)",
237
+ " end",
238
+ " end",
239
+ "",
240
+ " def find_form_action_url(html)",
241
+ " forms = html.scan(%r{<form[^>]*action=\"([^\"]+)\"[^>]*>(.*?)</form>}mi)",
242
+ "",
243
+ " forms.each do |action_url, form_content|",
244
+ " return CGI.unescapeHTML(action_url) if yield(form_content)",
245
+ " end",
246
+ "",
247
+ " nil",
248
+ " end",
249
+ "",
250
+ " def build_totp_from_uri(uri_string)",
251
+ " params = URI.decode_www_form(URI.parse(uri_string).query).to_h",
252
+ " secret = params['secret'] || raise('Missing secret in TOTP URI')",
253
+ "",
254
+ " ROTP::TOTP.new(",
255
+ " secret,",
256
+ " digits: params['digits']&.to_i,",
257
+ " period: params['period']&.to_i,",
258
+ " algorithm: params['algorithm'],",
259
+ " )",
260
+ " end",
261
+ "",
262
+ " def handle_redirect_response(response)",
263
+ " unless response.status == 302",
264
+ " raise \"Authentication failed, got unexpected response: #{response.status} #{response.body}\"",
265
+ " end",
266
+ "",
267
+ " response.headers['location'] || raise('No redirect location')",
268
+ " end",
269
+ "",
270
+ " def ensure_token_valid!",
271
+ " authenticate! unless authenticated?",
272
+ " return unless oauth_token.expired?",
273
+ "",
274
+ " self.oauth_token = oauth_token.refresh!",
275
+ " rescue StandardError => e",
276
+ " warn \"Token refresh failed: #{e.message}, trying to re-authenticate...\"",
277
+ "",
278
+ " authenticate!",
279
+ " end",
280
+ "",
281
+ " def get(url)",
282
+ " ensure_token_valid!",
283
+ "",
284
+ " response = oauth_token.get(url)",
285
+ " JSON.parse(response.body)",
286
+ " rescue StandardError => e",
287
+ " # simplecov:disable",
288
+ " warn \"API error: #{e.message}\"",
289
+ " nil",
290
+ " # simplecov:enable",
291
+ " end",
292
+ "",
293
+ " def post(url, data)",
294
+ " ensure_token_valid!",
295
+ "",
296
+ " response = oauth_token.post(",
297
+ " url,",
298
+ " body: data.to_json,",
299
+ " headers: { 'Content-Type' => 'application/json' },",
300
+ " )",
301
+ "",
302
+ " JSON.parse(response.body)",
303
+ " rescue StandardError => e",
304
+ " # simplecov:disable",
305
+ " warn \"API error: #{e.message}\"",
306
+ " nil",
307
+ " # simplecov:enable",
308
+ " end",
309
+ "",
310
+ " def http_request(method, url, data: nil)",
311
+ " Faraday",
312
+ " .new",
313
+ " .send(method, url) do |req|",
314
+ " req.headers['user-agent'] = user_agent",
315
+ " req.headers['connection'] = 'keep-alive'",
316
+ " req.headers['cookie'] = cookie_string if cookies.any?",
317
+ " req.body = URI.encode_www_form(data) if data",
318
+ " end",
319
+ " end",
320
+ "",
321
+ " def oauth_client",
322
+ " @oauth_client ||=",
323
+ " OAuth2::Client.new(",
324
+ " CLIENT_ID,",
325
+ " nil,",
326
+ " site: openid_config['issuer'],",
327
+ " authorize_url: openid_config['authorization_endpoint'],",
328
+ " token_url: openid_config['token_endpoint'],",
329
+ " )",
330
+ " end",
331
+ "",
332
+ " def openid_config",
333
+ " @openid_config ||= JSON.parse(http_request(:get, CONFIG_URL).body)",
334
+ " rescue StandardError => e",
335
+ " # simplecov:disable",
336
+ " raise \"Failed to load OpenID configuration: #{e.message}\"",
337
+ " # simplecov:enable",
338
+ " end",
339
+ "",
340
+ " def cookies",
341
+ " @cookies ||= {}",
342
+ " end",
343
+ "",
344
+ " def cookie_string",
345
+ " cookies.map { |k, v| \"#{k}=#{v}\" }.join('; ')",
346
+ " end",
347
+ "",
348
+ " def store_cookies(response)",
349
+ " set_cookie = response.headers['set-cookie']",
350
+ " return unless set_cookie",
351
+ "",
352
+ " set_cookie",
353
+ " .split(', ')",
354
+ " .each do |cookie_header|",
355
+ " name, value = cookie_header.split(';').first.split('=', 2)",
356
+ " cookies[name] = value if name && value",
357
+ " end",
358
+ " end",
359
+ " end",
360
+ " end",
361
+ "end"
362
+ ],
19
363
  "lines": [
20
364
  1,
21
365
  1,
@@ -37,21 +381,21 @@
37
381
  1,
38
382
  null,
39
383
  1,
40
- 22,
41
- 22,
42
- 22,
43
- 22,
384
+ 31,
385
+ 31,
386
+ 31,
387
+ 31,
44
388
  null,
45
389
  null,
46
390
  1,
47
391
  null,
48
392
  1,
49
- 19,
50
- 19,
51
- 19,
393
+ 28,
394
+ 28,
395
+ 28,
52
396
  null,
53
397
  null,
54
- 19,
398
+ 28,
55
399
  null,
56
400
  null,
57
401
  null,
@@ -59,11 +403,11 @@
59
403
  null,
60
404
  null,
61
405
  null,
62
- 19,
63
- 19,
64
- 17,
406
+ 28,
407
+ 28,
408
+ 26,
65
409
  null,
66
- 17,
410
+ 26,
67
411
  null,
68
412
  null,
69
413
  null,
@@ -72,7 +416,7 @@
72
416
  null,
73
417
  null,
74
418
  1,
75
- 22,
419
+ 31,
76
420
  null,
77
421
  null,
78
422
  1,
@@ -96,24 +440,40 @@
96
440
  null,
97
441
  null,
98
442
  1,
443
+ 5,
444
+ null,
445
+ null,
446
+ null,
447
+ null,
448
+ null,
449
+ 5,
450
+ null,
99
451
  null,
100
452
  1,
453
+ 4,
454
+ null,
455
+ 4,
456
+ null,
101
457
  null,
102
458
  1,
103
- 19,
104
- 19,
105
- 19,
106
459
  null,
460
+ 1,
107
461
  null,
108
462
  1,
109
- 19,
463
+ 28,
464
+ 28,
465
+ 28,
466
+ null,
467
+ null,
468
+ 1,
469
+ 28,
110
470
  null,
111
471
  2,
112
472
  null,
113
- 17,
473
+ 26,
114
474
  null,
115
475
  null,
116
- 19,
476
+ 28,
117
477
  null,
118
478
  3,
119
479
  2,
@@ -128,21 +488,21 @@
128
488
  null,
129
489
  null,
130
490
  null,
131
- 17,
491
+ 26,
132
492
  null,
133
493
  null,
134
494
  1,
135
- 17,
136
- 17,
495
+ 26,
496
+ 26,
137
497
  null,
138
- 17,
498
+ 26,
139
499
  null,
140
- 17,
141
- 17,
500
+ 26,
501
+ 26,
142
502
  null,
143
- 17,
503
+ 26,
144
504
  null,
145
- 17,
505
+ 26,
146
506
  null,
147
507
  null,
148
508
  1,
@@ -153,27 +513,27 @@
153
513
  null,
154
514
  null,
155
515
  1,
156
- 17,
516
+ 26,
157
517
  null,
158
- 17,
159
- 17,
518
+ 26,
519
+ 26,
160
520
  null,
161
- 17,
521
+ 26,
162
522
  null,
163
523
  null,
164
524
  1,
165
525
  null,
166
- 19,
167
- 36,
526
+ 28,
527
+ 54,
168
528
  null,
169
529
  null,
170
- 19,
530
+ 28,
171
531
  null,
172
532
  null,
173
- 17,
174
- 17,
533
+ 26,
534
+ 26,
175
535
  null,
176
- 17,
536
+ 26,
177
537
  null,
178
538
  0,
179
539
  null,
@@ -186,13 +546,13 @@
186
546
  null,
187
547
  null,
188
548
  1,
189
- 56,
549
+ 83,
190
550
  null,
191
- 56,
192
- 73,
551
+ 83,
552
+ 109,
193
553
  null,
194
554
  null,
195
- 18,
555
+ 27,
196
556
  null,
197
557
  null,
198
558
  1,
@@ -208,16 +568,16 @@
208
568
  null,
209
569
  null,
210
570
  1,
211
- 17,
571
+ 26,
212
572
  0,
213
573
  null,
214
574
  null,
215
- 17,
575
+ 26,
216
576
  null,
217
577
  null,
218
578
  1,
219
- 18,
220
- 18,
579
+ 27,
580
+ 27,
221
581
  null,
222
582
  2,
223
583
  null,
@@ -227,10 +587,10 @@
227
587
  null,
228
588
  null,
229
589
  1,
230
- 15,
590
+ 24,
231
591
  null,
232
- 15,
233
- 12,
592
+ 24,
593
+ 19,
234
594
  null,
235
595
  "ignored",
236
596
  "ignored",
@@ -256,18 +616,18 @@
256
616
  null,
257
617
  null,
258
618
  1,
259
- 75,
619
+ 111,
260
620
  null,
261
621
  null,
262
- 75,
263
- 75,
264
- 75,
265
- 75,
622
+ 111,
623
+ 111,
624
+ 111,
625
+ 111,
266
626
  null,
267
627
  null,
268
628
  null,
269
629
  1,
270
- 36,
630
+ 54,
271
631
  null,
272
632
  null,
273
633
  null,
@@ -278,7 +638,7 @@
278
638
  null,
279
639
  null,
280
640
  1,
281
- 57,
641
+ 84,
282
642
  null,
283
643
  "ignored",
284
644
  "ignored",
@@ -286,30 +646,43 @@
286
646
  null,
287
647
  null,
288
648
  1,
289
- 169,
649
+ 250,
290
650
  null,
291
651
  null,
292
652
  1,
293
- 148,
653
+ 220,
294
654
  null,
295
655
  null,
296
656
  1,
297
- 36,
298
- 36,
657
+ 54,
658
+ 54,
299
659
  null,
300
- 19,
660
+ 28,
301
661
  null,
302
662
  null,
303
- 57,
304
- 57,
663
+ 84,
664
+ 84,
305
665
  null,
306
666
  null,
307
667
  null,
308
668
  null,
309
669
  null
310
- ]
670
+ ],
671
+ "lines_covered_percent": 98.55072463768116,
672
+ "covered_lines": 136,
673
+ "missed_lines": 2,
674
+ "omitted_lines": 157,
675
+ "total_lines": 138
311
676
  },
312
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/cloud/error.rb": {
677
+ "lib/senec/cloud/error.rb": {
678
+ "source": [
679
+ "module Senec",
680
+ " module Cloud",
681
+ " class Error < StandardError",
682
+ " end",
683
+ " end",
684
+ "end"
685
+ ],
313
686
  "lines": [
314
687
  1,
315
688
  1,
@@ -317,9 +690,51 @@
317
690
  null,
318
691
  null,
319
692
  null
320
- ]
693
+ ],
694
+ "lines_covered_percent": 100.0,
695
+ "covered_lines": 3,
696
+ "missed_lines": 0,
697
+ "omitted_lines": 3,
698
+ "total_lines": 3
321
699
  },
322
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/connection.rb": {
700
+ "lib/senec/local/connection.rb": {
701
+ "source": [
702
+ "require 'faraday'",
703
+ "require 'faraday/net_http_persistent'",
704
+ "require 'faraday-request-timer'",
705
+ "require 'forwardable'",
706
+ "",
707
+ "module Senec",
708
+ " module Local",
709
+ " class Connection",
710
+ " def initialize(host:, schema: 'http')",
711
+ " @url = \"#{schema}://#{host}\"",
712
+ " end",
713
+ "",
714
+ " attr_reader :url",
715
+ "",
716
+ " extend Forwardable",
717
+ "",
718
+ " def_delegators :faraday, :get, :post",
719
+ "",
720
+ " private",
721
+ "",
722
+ " def faraday",
723
+ " @faraday ||= Faraday.new @url,",
724
+ " ssl: { verify: false },",
725
+ " headers: {",
726
+ " 'Connection' => 'keep-alive'",
727
+ " } do |f|",
728
+ " f.request :timer",
729
+ " f.adapter :net_http_persistent, pool_size: 5 do |http|",
730
+ " http.idle_timeout = 30",
731
+ " end",
732
+ " end",
733
+ " end",
734
+ " end",
735
+ " end",
736
+ "end"
737
+ ],
323
738
  "lines": [
324
739
  1,
325
740
  1,
@@ -356,9 +771,75 @@
356
771
  null,
357
772
  null,
358
773
  null
359
- ]
774
+ ],
775
+ "lines_covered_percent": 100.0,
776
+ "covered_lines": 18,
777
+ "missed_lines": 0,
778
+ "omitted_lines": 17,
779
+ "total_lines": 18
360
780
  },
361
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/constants.rb": {
781
+ "lib/senec/local/constants.rb": {
782
+ "source": [
783
+ "module Senec",
784
+ " module Local",
785
+ " # For a full list of available vars, see https://[IP-of-your-SENEC]//Vars.html",
786
+ " # Comments taken from https://gist.github.com/smashnet/82ad0b9d7f0ba2e5098e6649ba08f88a",
787
+ " BASIC_REQUEST = {",
788
+ " ENERGY: {",
789
+ " STAT_STATE: '', # Current state of the system (int, see SYSTEM_STATE_NAME)",
790
+ " GUI_BAT_DATA_CURRENT: '', # Battery charge current: negative if discharging, positiv if charging (A)",
791
+ " GUI_BAT_DATA_FUEL_CHARGE: '', # Remaining battery (percent)",
792
+ " GUI_BAT_DATA_POWER: '', # Battery charge power: negative if discharging, positiv if charging (W)",
793
+ " GUI_BAT_DATA_VOLTAGE: '', # Battery voltage (V)",
794
+ " GUI_GRID_POW: '', # Grid power: negative if exporting, positiv if importing (W)",
795
+ " GUI_HOUSE_POW: '', # House power consumption (W)",
796
+ " GUI_INVERTER_POWER: '', # PV production (W)",
797
+ " STAT_HOURS_OF_OPERATION: '' # Appliance hours of operation",
798
+ " },",
799
+ " WIZARD: {",
800
+ " APPLICATION_VERSION: ''",
801
+ " },",
802
+ " RTC: {",
803
+ " UTC_OFFSET: '',",
804
+ " WEB_TIME: ''",
805
+ " },",
806
+ " PV1: {",
807
+ " MPP_POWER: '', # List: MPP power (W)",
808
+ " POWER_RATIO: '' # Grid export limit (percent)",
809
+ " },",
810
+ " TEMPMEASURE: {",
811
+ " CASE_TEMP: ''",
812
+ " },",
813
+ " WALLBOX: {",
814
+ " APPARENT_CHARGING_POWER: '',",
815
+ " EV_CONNECTED: ''",
816
+ " }",
817
+ " }.freeze",
818
+ "",
819
+ " SAFETY_CHARGE = {",
820
+ " ENERGY: {",
821
+ " SAFE_CHARGE_FORCE: 'u8_01',",
822
+ " SAFE_CHARGE_PROHIBIT: '',",
823
+ " SAFE_CHARGE_RUNNING: '',",
824
+ " LI_STORAGE_MODE_START: '',",
825
+ " LI_STORAGE_MODE_STOP: '',",
826
+ " LI_STORAGE_MODE_RUNNING: ''",
827
+ " }",
828
+ " }.freeze",
829
+ "",
830
+ " ALLOW_DISCHARGE = {",
831
+ " ENERGY: {",
832
+ " SAFE_CHARGE_FORCE: '',",
833
+ " SAFE_CHARGE_PROHIBIT: 'u8_01',",
834
+ " SAFE_CHARGE_RUNNING: '',",
835
+ " LI_STORAGE_MODE_START: '',",
836
+ " LI_STORAGE_MODE_STOP: '',",
837
+ " LI_STORAGE_MODE_RUNNING: ''",
838
+ " }",
839
+ " }.freeze",
840
+ " end",
841
+ "end"
842
+ ],
362
843
  "lines": [
363
844
  1,
364
845
  1,
@@ -419,9 +900,22 @@
419
900
  null,
420
901
  null,
421
902
  null
422
- ]
903
+ ],
904
+ "lines_covered_percent": 100.0,
905
+ "covered_lines": 5,
906
+ "missed_lines": 0,
907
+ "omitted_lines": 54,
908
+ "total_lines": 5
423
909
  },
424
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/error.rb": {
910
+ "lib/senec/local/error.rb": {
911
+ "source": [
912
+ "module Senec",
913
+ " module Local",
914
+ " class Error < StandardError; end",
915
+ " class DecodingError < StandardError; end",
916
+ " end",
917
+ "end"
918
+ ],
425
919
  "lines": [
426
920
  1,
427
921
  1,
@@ -429,9 +923,158 @@
429
923
  1,
430
924
  null,
431
925
  null
432
- ]
926
+ ],
927
+ "lines_covered_percent": 100.0,
928
+ "covered_lines": 4,
929
+ "missed_lines": 0,
930
+ "omitted_lines": 2,
931
+ "total_lines": 4
433
932
  },
434
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/request.rb": {
933
+ "lib/senec/local/request.rb": {
934
+ "source": [
935
+ "require_relative 'value'",
936
+ "require_relative 'constants'",
937
+ "",
938
+ "module Senec",
939
+ " module Local",
940
+ " class Request",
941
+ " def initialize(connection:, body: BASIC_REQUEST, state_names: nil)",
942
+ " @connection = connection",
943
+ " @body = body",
944
+ " @state_names = state_names",
945
+ " end",
946
+ "",
947
+ " attr_reader :connection, :body, :state_names",
948
+ "",
949
+ " def perform! # rubocop:disable Naming/PredicateMethod",
950
+ " parsed_response",
951
+ " true",
952
+ " end",
953
+ "",
954
+ " def house_power",
955
+ " get('ENERGY', 'GUI_HOUSE_POW')",
956
+ " end",
957
+ "",
958
+ " def inverter_power",
959
+ " get('ENERGY', 'GUI_INVERTER_POWER')",
960
+ " end",
961
+ "",
962
+ " def mpp_power",
963
+ " get('PV1', 'MPP_POWER')",
964
+ " end",
965
+ "",
966
+ " def power_ratio",
967
+ " get('PV1', 'POWER_RATIO')",
968
+ " end",
969
+ "",
970
+ " def bat_power",
971
+ " get('ENERGY', 'GUI_BAT_DATA_POWER')",
972
+ " end",
973
+ "",
974
+ " def bat_fuel_charge",
975
+ " get('ENERGY', 'GUI_BAT_DATA_FUEL_CHARGE')",
976
+ " end",
977
+ "",
978
+ " def bat_charge_current",
979
+ " get('ENERGY', 'GUI_BAT_DATA_CURRENT')",
980
+ " end",
981
+ "",
982
+ " def bat_voltage",
983
+ " get('ENERGY', 'GUI_BAT_DATA_VOLTAGE')",
984
+ " end",
985
+ "",
986
+ " def grid_power",
987
+ " get('ENERGY', 'GUI_GRID_POW')",
988
+ " end",
989
+ "",
990
+ " def ev_connected",
991
+ " get('WALLBOX', 'EV_CONNECTED')",
992
+ " end",
993
+ "",
994
+ " def wallbox_charge_power",
995
+ " get('WALLBOX', 'APPARENT_CHARGING_POWER')",
996
+ " end",
997
+ "",
998
+ " def case_temp",
999
+ " get('TEMPMEASURE', 'CASE_TEMP')",
1000
+ " end",
1001
+ "",
1002
+ " def application_version",
1003
+ " get('WIZARD', 'APPLICATION_VERSION')",
1004
+ " end",
1005
+ "",
1006
+ " def current_state_code",
1007
+ " get('ENERGY', 'STAT_STATE')",
1008
+ " end",
1009
+ "",
1010
+ " def current_state_name",
1011
+ " throw RuntimeError, 'No state names provided!' unless state_names",
1012
+ "",
1013
+ " state_names[current_state_code]",
1014
+ " end",
1015
+ "",
1016
+ " def measure_time",
1017
+ " web_time = get('RTC', 'WEB_TIME')",
1018
+ " utc_offset = get('RTC', 'UTC_OFFSET')",
1019
+ "",
1020
+ " web_time - (utc_offset * 60)",
1021
+ " end",
1022
+ "",
1023
+ " def response_duration",
1024
+ " raw_response.env[:duration]",
1025
+ " end",
1026
+ "",
1027
+ " def get(*keys)",
1028
+ " return unless parsed_response",
1029
+ "",
1030
+ " value = parsed_response.dig(*keys)",
1031
+ "",
1032
+ " if value.is_a?(Array)",
1033
+ " value.map do |v|",
1034
+ " Value.new(v).decoded",
1035
+ " end",
1036
+ " elsif value",
1037
+ " Value.new(value).decoded",
1038
+ " else",
1039
+ " raise Error, \"Value missing for #{keys.join('.')}\"",
1040
+ " end",
1041
+ " rescue Senec::Local::DecodingError => e",
1042
+ " raise Error, \"Decoding failed for #{keys.join('.')}: #{e.message}\"",
1043
+ " end",
1044
+ "",
1045
+ " def path",
1046
+ " '/lala.cgi'",
1047
+ " end",
1048
+ "",
1049
+ " private",
1050
+ "",
1051
+ " def parsed_response",
1052
+ " @parsed_response ||= JSON.parse(raw_response.body)",
1053
+ " end",
1054
+ "",
1055
+ " def raw_response",
1056
+ " @raw_response ||= begin",
1057
+ " response = connection.post(path, request_body, request_header)",
1058
+ " raise Error, response.status unless response.success?",
1059
+ "",
1060
+ " response",
1061
+ " end",
1062
+ " end",
1063
+ "",
1064
+ " def request_body",
1065
+ " JSON.generate(body)",
1066
+ " end",
1067
+ "",
1068
+ " def request_header",
1069
+ " {",
1070
+ " 'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',",
1071
+ " 'Accept' => 'application/json, text/javascript, */*; q=0.01'",
1072
+ " }",
1073
+ " end",
1074
+ " end",
1075
+ " end",
1076
+ "end"
1077
+ ],
435
1078
  "lines": [
436
1079
  1,
437
1080
  1,
@@ -575,9 +1218,82 @@
575
1218
  null,
576
1219
  null,
577
1220
  null
578
- ]
1221
+ ],
1222
+ "lines_covered_percent": 100.0,
1223
+ "covered_lines": 74,
1224
+ "missed_lines": 0,
1225
+ "omitted_lines": 68,
1226
+ "total_lines": 74
579
1227
  },
580
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/state.rb": {
1228
+ "lib/senec/local/state.rb": {
1229
+ "source": [
1230
+ "module Senec",
1231
+ " module Local",
1232
+ " class State",
1233
+ " def initialize(connection:)",
1234
+ " @connection = connection",
1235
+ " end",
1236
+ "",
1237
+ " attr_reader :connection",
1238
+ "",
1239
+ " # Extract state names from JavaScript file, which is formatted like this:",
1240
+ " #",
1241
+ " # var system_state_name = {",
1242
+ " # 0: \"FIRST STATE\",",
1243
+ " # 1: \"SECOND STATE\",",
1244
+ " # ...",
1245
+ " # };",
1246
+ " def names(language: :de)",
1247
+ " js_content = response(language:)",
1248
+ "",
1249
+ " # Extract the matched content",
1250
+ " match = js_content.match(FILE_REGEX)",
1251
+ " return unless match",
1252
+ "",
1253
+ " # Extract the JSON-like part",
1254
+ " json_like = \"{#{match[1]}}\"",
1255
+ "",
1256
+ " # The keys are numbers, which is not valid JSON, so we need to convert them to strings",
1257
+ " json = json_like.gsub(/(\\d+)\\s*:/, '\"\\1\":')",
1258
+ "",
1259
+ " # Convert the JSON string to a Ruby hash",
1260
+ " hash = JSON.parse(json)",
1261
+ "",
1262
+ " # Convert keys from strings to integers",
1263
+ " hash.transform_keys(&:to_i)",
1264
+ " end",
1265
+ "",
1266
+ " private",
1267
+ "",
1268
+ " # Regex pattern to match the system_state_name definition in the JavaScript file",
1269
+ " # The file may be minimized, so we need to be flexible with whitespace and line breaks",
1270
+ " FILE_REGEX = /system_state_name\\s*=\\s*{\\s*([^}]*)\\s*}/m",
1271
+ " private_constant :FILE_REGEX",
1272
+ "",
1273
+ " def response(language:)",
1274
+ " res = connection.get url(language:)",
1275
+ " raise Error, res.message unless res.success?",
1276
+ "",
1277
+ " res.body",
1278
+ " end",
1279
+ "",
1280
+ " # Use the JavaScript file containing English/German/Italian names from the SENEC web interface",
1281
+ " def url(language:)",
1282
+ " case language",
1283
+ " when :en",
1284
+ " '/js/EN-en.js'",
1285
+ " when :de",
1286
+ " '/js/DE-de.js'",
1287
+ " when :it",
1288
+ " '/js/IT-it.js'",
1289
+ " else",
1290
+ " raise Error, \"Language #{language} not supported\"",
1291
+ " end",
1292
+ " end",
1293
+ " end",
1294
+ " end",
1295
+ "end"
1296
+ ],
581
1297
  "lines": [
582
1298
  1,
583
1299
  1,
@@ -645,9 +1361,59 @@
645
1361
  null,
646
1362
  null,
647
1363
  null
648
- ]
1364
+ ],
1365
+ "lines_covered_percent": 100.0,
1366
+ "covered_lines": 27,
1367
+ "missed_lines": 0,
1368
+ "omitted_lines": 39,
1369
+ "total_lines": 27
649
1370
  },
650
- "/Users/ledermann/Projects/solectrus/senec/lib/senec/local/value.rb": {
1371
+ "lib/senec/local/value.rb": {
1372
+ "source": [
1373
+ "module Senec",
1374
+ " module Local",
1375
+ " class Value",
1376
+ " def initialize(raw)",
1377
+ " @raw = raw",
1378
+ " @prefix, @value = raw&.split('_')",
1379
+ " end",
1380
+ "",
1381
+ " attr_reader :prefix, :value, :raw",
1382
+ "",
1383
+ " def decoded",
1384
+ " case prefix",
1385
+ " when 'fl'",
1386
+ " decoded_float(value)",
1387
+ " when 'i3', 'u1', 'u3', 'u6', 'u8'",
1388
+ " decoded_int(value)",
1389
+ " when 'st'",
1390
+ " value",
1391
+ " # TODO: There are some more prefixes to handle",
1392
+ " else",
1393
+ " raise DecodingError, \"Unknown value '#{@raw}'\"",
1394
+ " end",
1395
+ " end",
1396
+ "",
1397
+ " alias to_i decoded",
1398
+ " alias to_f decoded",
1399
+ " alias to_s decoded",
1400
+ "",
1401
+ " private",
1402
+ "",
1403
+ " PREFIXES = %w[fl i3 u1 u3 u6 u8 st].freeze",
1404
+ " private_constant :PREFIXES",
1405
+ "",
1406
+ " def decoded_float(hex)",
1407
+ " [\"0x#{hex}\".to_i(16)].pack('L').unpack1('F').round(1)",
1408
+ " end",
1409
+ "",
1410
+ " def decoded_int(hex)",
1411
+ " \"0x#{hex}\".to_i(16)",
1412
+ " end",
1413
+ " end",
1414
+ " end",
1415
+ "end"
1416
+ ],
651
1417
  "lines": [
652
1418
  1,
653
1419
  1,
@@ -692,881 +1458,14 @@
692
1458
  null,
693
1459
  null,
694
1460
  null
695
- ]
696
- },
697
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/cloud/connection_spec.rb": {
698
- "lines": [
699
- 1,
700
- 23,
701
- null,
702
- 23,
703
- 22,
704
- 22,
705
- null,
706
- 1,
707
- 5,
708
- null,
709
- 1,
710
- 3,
711
- null,
712
- null,
713
- 1,
714
- 2,
715
- null,
716
- 3,
717
- null,
718
- null,
719
- 1,
720
- 3,
721
- null,
722
- null,
723
- 1,
724
- 2,
725
- null,
726
- 3,
727
- null,
728
- null,
729
- null,
730
- 1,
731
- 4,
732
- null,
733
- 4,
734
- null,
735
- 2,
736
- 2,
737
- null,
738
- 1,
739
- 1,
740
- null,
741
- null,
742
- null,
743
- null,
744
- null,
745
- null,
746
- null,
747
- null,
748
- null,
749
- null,
750
- null,
751
- null,
752
- null,
753
- null,
754
- null,
755
- 1,
756
- 4,
757
- null,
758
- 4,
759
- null,
760
- 1,
761
- 3,
762
- null,
763
- 2,
764
- null,
765
- 1,
766
- 1,
767
- null,
768
- null,
769
- null,
770
- null,
771
- null,
772
- null,
773
- null,
774
- null,
775
- null,
776
- null,
777
- null,
778
- 1,
779
- 2,
780
- null,
781
- 1,
782
- 1,
783
- null,
784
- null,
785
- null,
786
- null,
787
- 1,
788
- 4,
789
- null,
790
- 4,
791
- null,
792
- 1,
793
- 3,
794
- null,
795
- 2,
796
- null,
797
- 1,
798
- 1,
799
- null,
800
- null,
801
- null,
802
- null,
803
- null,
804
- null,
805
- null,
806
- null,
807
- null,
808
- null,
809
- null,
810
- null,
811
- 1,
812
- 2,
813
- null,
814
- 1,
815
- 1,
816
- null,
817
- null,
818
- null,
819
- null,
820
- 1,
821
- 4,
822
- null,
823
- 4,
824
- null,
825
- 1,
826
- 3,
827
- null,
828
- 2,
829
- null,
830
- 1,
831
- 1,
832
- null,
833
- null,
834
- null,
835
- null,
836
- null,
837
- null,
838
- null,
839
- null,
840
- null,
841
- null,
842
- null,
843
- null,
844
- null,
845
- null,
846
- null,
847
- null,
848
- null,
849
- 1,
850
- 2,
851
- null,
852
- 1,
853
- 1,
854
- null,
855
- null,
856
- null,
857
- null,
858
- 1,
859
- 4,
860
- null,
861
- 4,
862
- null,
863
- 1,
864
- 3,
865
- null,
866
- 2,
867
- null,
868
- 1,
869
- 1,
870
- null,
871
- null,
872
- null,
873
- null,
874
- null,
875
- null,
876
- null,
877
- null,
878
- null,
879
- null,
880
- null,
881
- null,
882
- null,
883
- null,
884
- null,
885
- null,
886
- null,
887
- 1,
888
- 2,
889
- null,
890
- 1,
891
- 1,
892
- null,
893
- null,
894
- null,
895
- null,
896
- 1,
897
- 4,
898
- null,
899
- 4,
900
- null,
901
- 4,
902
- null,
903
- 1,
904
- 1,
905
- 1,
906
- 1,
907
- null,
908
- 1,
909
- 1,
910
- null,
911
- null,
912
- 1,
913
- 1,
914
- 1,
915
- 1,
916
- 1,
917
- 1,
918
- null,
919
- 1,
920
- 1,
921
- null,
922
- null,
923
- 1,
924
- 1,
925
- 1,
926
- 1,
927
- 2,
928
- 1,
929
- 1,
930
- null,
931
- 1,
932
- 1,
933
- null,
934
- null,
935
- null
936
- ]
937
- },
938
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/request_spec.rb": {
939
- "lines": [
940
- 1,
941
- 23,
942
- 1,
943
- 23,
944
- null,
945
- null,
946
- 1,
947
- 19,
948
- null,
949
- 1,
950
- 2,
951
- null,
952
- 2,
953
- null,
954
- null,
955
- 1,
956
- 2,
957
- null,
958
- 2,
959
- null,
960
- null,
961
- 1,
962
- 2,
963
- null,
964
- 2,
965
- null,
966
- null,
967
- 1,
968
- 2,
969
- null,
970
- 2,
971
- null,
972
- null,
973
- 1,
974
- 2,
975
- null,
976
- 2,
977
- null,
978
- null,
979
- 1,
980
- 2,
981
- null,
982
- 2,
983
- null,
984
- null,
985
- 1,
986
- 2,
987
- null,
988
- 2,
989
- null,
990
- null,
991
- 1,
992
- 2,
993
- null,
994
- 2,
995
- null,
996
- null,
997
- 1,
998
- 2,
999
- null,
1000
- 2,
1001
- null,
1002
- null,
1003
- 1,
1004
- 2,
1005
- null,
1006
- 2,
1007
- null,
1008
- null,
1009
- 1,
1010
- 2,
1011
- null,
1012
- 2,
1013
- null,
1014
- null,
1015
- 1,
1016
- 2,
1017
- null,
1018
- 2,
1019
- null,
1020
- null,
1021
- 1,
1022
- 2,
1023
- null,
1024
- 2,
1025
- null,
1026
- null,
1027
- 1,
1028
- 3,
1029
- null,
1030
- 2,
1031
- 2,
1032
- null,
1033
- null,
1034
- 1,
1035
- 2,
1036
- null,
1037
- 2,
1038
- null,
1039
- null,
1040
- 1,
1041
- 2,
1042
- null,
1043
- 2,
1044
- null,
1045
- null,
1046
- 1,
1047
- 2,
1048
- null,
1049
- 2,
1050
- null,
1051
- null,
1052
- null,
1053
- 1,
1054
- 2,
1055
- null,
1056
- 2,
1057
- null,
1058
- 1,
1059
- 2,
1060
- null,
1061
- null,
1062
- null,
1063
- 1,
1064
- 2,
1065
- null,
1066
- 1,
1067
- 1,
1068
- null,
1069
- null,
1070
- 1,
1071
- 2,
1072
- null,
1073
- null,
1074
- null,
1075
- 1,
1076
- 2,
1077
- null,
1078
- 2,
1079
- null,
1080
- 1,
1081
- 2,
1082
- null,
1083
- null,
1084
- null,
1085
- 1,
1086
- 1,
1087
- 1,
1088
- null,
1089
- null,
1090
- null,
1091
- null,
1092
- null,
1093
- 1,
1094
- 2,
1095
- null,
1096
- 2,
1097
- null,
1098
- null,
1099
- null,
1100
- 1,
1101
- 1,
1102
- 1,
1103
- null,
1104
- null,
1105
- null,
1106
- null,
1107
- null,
1108
- 1,
1109
- 2,
1110
- null,
1111
- 2,
1112
- null,
1113
- null,
1114
- null,
1115
- 1,
1116
- 1,
1117
- 3,
1118
- null,
1119
- null,
1120
- null,
1121
- null,
1122
- null,
1123
- null,
1124
- null,
1125
- null,
1126
- null,
1127
- 1,
1128
- 1,
1129
- 2,
1130
- null,
1131
- 2,
1132
- null,
1133
- null,
1134
- 1,
1135
- 2,
1136
- null,
1137
- 1,
1138
- 2,
1139
- null,
1140
- null,
1141
- null,
1142
- 1,
1143
- 2,
1144
- null,
1145
- 1,
1146
- 1,
1147
- null,
1148
- null,
1149
- null,
1150
- null,
1151
- null,
1152
- null,
1153
- 1,
1154
- 2,
1155
- null,
1156
- null,
1157
- null,
1158
- null,
1159
- null,
1160
- null
1161
- ]
1162
- },
1163
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/state_spec.rb": {
1164
- "lines": [
1165
- 1,
1166
- 6,
1167
- null,
1168
- 6,
1169
- null,
1170
- 1,
1171
- 5,
1172
- null,
1173
- null,
1174
- null,
1175
- 1,
1176
- 3,
1177
- null,
1178
- null,
1179
- null,
1180
- null,
1181
- null,
1182
- null,
1183
- null,
1184
- null,
1185
- null,
1186
- null,
1187
- null,
1188
- null,
1189
- null,
1190
- null,
1191
- null,
1192
- 1,
1193
- 1,
1194
- null,
1195
- null,
1196
- null,
1197
- null,
1198
- 1,
1199
- 6,
1200
- null,
1201
- 1,
1202
- null,
1203
- 4,
1204
- null,
1205
- null,
1206
- null,
1207
- null,
1208
- null,
1209
- null,
1210
- null,
1211
- null,
1212
- 1,
1213
- 2,
1214
- null,
1215
- 1,
1216
- 1,
1217
- null,
1218
- null,
1219
- 2,
1220
- null,
1221
- null,
1222
- 1,
1223
- 2,
1224
- null,
1225
- 1,
1226
- 1,
1227
- null,
1228
- null,
1229
- 2,
1230
- null,
1231
- null,
1232
- 1,
1233
- 2,
1234
- null,
1235
- 1,
1236
- 1,
1237
- null,
1238
- null,
1239
- 2,
1240
- null,
1241
- null,
1242
- 1,
1243
- 2,
1244
- null,
1245
- 1,
1246
- 1,
1247
- null,
1248
- null,
1249
- 2,
1250
- null,
1251
- null,
1252
- 1,
1253
- 2,
1254
- null,
1255
- 1,
1256
- 2,
1257
- null,
1258
- null,
1259
- null,
1260
- null
1261
- ]
1262
- },
1263
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/value_spec.rb": {
1264
- "lines": [
1265
- 1,
1266
- 17,
1267
- null,
1268
- 1,
1269
- 17,
1270
- null,
1271
- 1,
1272
- 3,
1273
- null,
1274
- 2,
1275
- 2,
1276
- null,
1277
- null,
1278
- 1,
1279
- 3,
1280
- null,
1281
- 2,
1282
- 2,
1283
- null,
1284
- null,
1285
- 1,
1286
- 3,
1287
- null,
1288
- 2,
1289
- 2,
1290
- null,
1291
- null,
1292
- 1,
1293
- 3,
1294
- null,
1295
- 2,
1296
- 2,
1297
- null,
1298
- null,
1299
- 1,
1300
- 3,
1301
- null,
1302
- 2,
1303
- 2,
1304
- null,
1305
- null,
1306
- 1,
1307
- 3,
1308
- null,
1309
- 2,
1310
- 2,
1311
- null,
1312
- null,
1313
- 1,
1314
- 3,
1315
- null,
1316
- 2,
1317
- 2,
1318
- null,
1319
- null,
1320
- 1,
1321
- 2,
1322
- null,
1323
- 3,
1324
- null,
1325
- null,
1326
- 1,
1327
- 2,
1328
- null,
1329
- 1,
1330
- 2,
1331
- null,
1332
- null,
1333
- null,
1334
- null
1335
- ]
1336
- },
1337
- "/Users/ledermann/Projects/solectrus/senec/spec/senec_spec.rb": {
1338
- "lines": [
1339
- 1,
1340
- 1,
1341
- 1,
1342
- null,
1343
- null
1344
- ]
1345
- },
1346
- "/Users/ledermann/Projects/solectrus/senec/spec/support/vcr.rb": {
1347
- "lines": [
1348
- 1,
1349
- null,
1350
- 1,
1351
- 1,
1352
- 9,
1353
- null,
1354
- null,
1355
- null,
1356
- 1,
1357
- 1,
1358
- 6,
1359
- null,
1360
- null,
1361
- null,
1362
- 1,
1363
- 2,
1364
- 23,
1365
- null,
1366
- null,
1367
- null,
1368
- 1,
1369
- 4,
1370
- null,
1371
- null,
1372
- null,
1373
- 1,
1374
- null,
1375
- null,
1376
- null,
1377
- null,
1378
- null,
1379
- null,
1380
- null,
1381
- null,
1382
- null,
1383
- null,
1384
- null,
1385
- null,
1386
- null,
1387
- null,
1388
- null,
1389
- null,
1390
- null,
1391
- null,
1392
- null,
1393
- null,
1394
- null,
1395
- null,
1396
- null,
1397
- null,
1398
- null,
1399
- null,
1400
- null,
1401
- null,
1402
- null,
1403
- null,
1404
- null,
1405
- null,
1406
- null,
1407
- null,
1408
- null,
1409
- null,
1410
- null,
1411
- null,
1412
- null,
1413
- null,
1414
- null,
1415
- null,
1416
- null,
1417
- null,
1418
- null,
1419
- null,
1420
- null,
1421
- null,
1422
- null,
1423
- null,
1424
- null,
1425
- null,
1426
- null,
1427
- null,
1428
- null,
1429
- null,
1430
- null,
1431
- null,
1432
- null,
1433
- null,
1434
- null,
1435
- null,
1436
- null,
1437
- null,
1438
- 1,
1439
- 260,
1440
- null,
1441
- null,
1442
- null,
1443
- null,
1444
- null,
1445
- null,
1446
- null,
1447
- null,
1448
- null,
1449
- null,
1450
- 1,
1451
- 260,
1452
- null,
1453
- null,
1454
- 1,
1455
- 1,
1456
- 1,
1457
- 1,
1458
- 1,
1459
- null,
1460
- 1,
1461
- 1,
1462
- null,
1463
- null,
1464
- null,
1465
- null,
1466
- null,
1467
- null,
1468
- null,
1469
- 1,
1470
- 130,
1471
- 130,
1472
- 130,
1473
- null,
1474
- null,
1475
- null,
1476
- 1,
1477
- 132,
1478
- null,
1479
- 1,
1480
- 132,
1481
- null,
1482
- 1,
1483
- 132,
1484
- null,
1485
- 1,
1486
- 132,
1487
- 132,
1488
- null,
1489
- 1,
1490
- 132,
1491
- 132,
1492
- null,
1493
- null,
1494
- null,
1495
- "ignored",
1496
- "ignored",
1497
- "ignored",
1498
- "ignored",
1499
- "ignored",
1500
- "ignored",
1501
- "ignored",
1502
- "ignored",
1503
- "ignored",
1504
- "ignored",
1505
- "ignored",
1506
- "ignored",
1507
- "ignored",
1508
- "ignored",
1509
- "ignored",
1510
- "ignored",
1511
- "ignored",
1512
- "ignored",
1513
- "ignored",
1514
- "ignored",
1515
- "ignored",
1516
- "ignored",
1517
- "ignored",
1518
- "ignored",
1519
- "ignored",
1520
- "ignored",
1521
- "ignored",
1522
- "ignored",
1523
- "ignored",
1524
- "ignored",
1525
- "ignored",
1526
- "ignored",
1527
- "ignored",
1528
- "ignored",
1529
- "ignored",
1530
- "ignored",
1531
- "ignored",
1532
- "ignored",
1533
- "ignored",
1534
- "ignored",
1535
- "ignored",
1536
- "ignored",
1537
- "ignored",
1538
- "ignored",
1539
- "ignored",
1540
- "ignored",
1541
- "ignored",
1542
- "ignored",
1543
- "ignored",
1544
- "ignored",
1545
- "ignored",
1546
- "ignored",
1547
- "ignored",
1548
- null,
1549
- null,
1550
- null,
1551
- null,
1552
- 1,
1553
- 1,
1554
- null,
1555
- 1,
1556
- null,
1557
- null,
1558
- null,
1559
- 1,
1560
- 5,
1561
- 5,
1562
- null,
1563
- null,
1564
- null,
1565
- null,
1566
- 71,
1567
- null
1568
- ]
1461
+ ],
1462
+ "lines_covered_percent": 100.0,
1463
+ "covered_lines": 23,
1464
+ "missed_lines": 0,
1465
+ "omitted_lines": 20,
1466
+ "total_lines": 23
1569
1467
  }
1570
1468
  },
1571
- "groups": {}
1469
+ "groups": {},
1470
+ "errors": {}
1572
1471
  }