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.
@@ -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,20 +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,
110
- 19,
463
+ 28,
464
+ 28,
465
+ 28,
111
466
  null,
112
- 19,
467
+ null,
468
+ 1,
469
+ 28,
470
+ null,
471
+ 2,
472
+ null,
473
+ 26,
474
+ null,
475
+ null,
476
+ 28,
113
477
  null,
114
478
  3,
115
479
  2,
@@ -124,7 +488,21 @@
124
488
  null,
125
489
  null,
126
490
  null,
127
- 17,
491
+ 26,
492
+ null,
493
+ null,
494
+ 1,
495
+ 26,
496
+ 26,
497
+ null,
498
+ 26,
499
+ null,
500
+ 26,
501
+ 26,
502
+ null,
503
+ 26,
504
+ null,
505
+ 26,
128
506
  null,
129
507
  null,
130
508
  1,
@@ -135,20 +513,29 @@
135
513
  null,
136
514
  null,
137
515
  1,
138
- 17,
516
+ 26,
139
517
  null,
140
- 17,
141
- 17,
518
+ 26,
519
+ 26,
142
520
  null,
143
- 17,
521
+ 26,
144
522
  null,
145
523
  null,
146
524
  1,
147
- 19,
148
525
  null,
149
- 19,
526
+ 28,
527
+ 54,
528
+ null,
529
+ null,
530
+ 28,
150
531
  null,
151
532
  null,
533
+ 26,
534
+ 26,
535
+ null,
536
+ 26,
537
+ null,
538
+ 0,
152
539
  null,
153
540
  null,
154
541
  1,
@@ -159,13 +546,13 @@
159
546
  null,
160
547
  null,
161
548
  1,
162
- 22,
549
+ 83,
163
550
  null,
164
- 22,
165
- 22,
551
+ 83,
552
+ 109,
166
553
  null,
167
554
  null,
168
- 1,
555
+ 27,
169
556
  null,
170
557
  null,
171
558
  1,
@@ -181,16 +568,16 @@
181
568
  null,
182
569
  null,
183
570
  1,
184
- 17,
571
+ 26,
185
572
  0,
186
573
  null,
187
574
  null,
188
- 17,
575
+ 26,
189
576
  null,
190
577
  null,
191
578
  1,
192
- 18,
193
- 18,
579
+ 27,
580
+ 27,
194
581
  null,
195
582
  2,
196
583
  null,
@@ -200,10 +587,10 @@
200
587
  null,
201
588
  null,
202
589
  1,
203
- 15,
590
+ 24,
204
591
  null,
205
- 15,
206
- 12,
592
+ 24,
593
+ 19,
207
594
  null,
208
595
  "ignored",
209
596
  "ignored",
@@ -229,18 +616,18 @@
229
616
  null,
230
617
  null,
231
618
  1,
232
- 58,
619
+ 111,
233
620
  null,
234
621
  null,
235
- 58,
236
- 58,
237
- 58,
238
- 58,
622
+ 111,
623
+ 111,
624
+ 111,
625
+ 111,
239
626
  null,
240
627
  null,
241
628
  null,
242
629
  1,
243
- 36,
630
+ 54,
244
631
  null,
245
632
  null,
246
633
  null,
@@ -251,7 +638,7 @@
251
638
  null,
252
639
  null,
253
640
  1,
254
- 57,
641
+ 84,
255
642
  null,
256
643
  "ignored",
257
644
  "ignored",
@@ -259,30 +646,43 @@
259
646
  null,
260
647
  null,
261
648
  1,
262
- 135,
649
+ 250,
263
650
  null,
264
651
  null,
265
652
  1,
266
- 80,
653
+ 220,
267
654
  null,
268
655
  null,
269
656
  1,
270
- 19,
271
- 19,
657
+ 54,
658
+ 54,
272
659
  null,
273
- 19,
660
+ 28,
274
661
  null,
275
662
  null,
276
- 57,
277
- 57,
663
+ 84,
664
+ 84,
278
665
  null,
279
666
  null,
280
667
  null,
281
668
  null,
282
669
  null
283
- ]
670
+ ],
671
+ "lines_covered_percent": 98.55072463768116,
672
+ "covered_lines": 136,
673
+ "missed_lines": 2,
674
+ "omitted_lines": 157,
675
+ "total_lines": 138
284
676
  },
285
- "/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
+ ],
286
686
  "lines": [
287
687
  1,
288
688
  1,
@@ -290,9 +690,51 @@
290
690
  null,
291
691
  null,
292
692
  null
293
- ]
693
+ ],
694
+ "lines_covered_percent": 100.0,
695
+ "covered_lines": 3,
696
+ "missed_lines": 0,
697
+ "omitted_lines": 3,
698
+ "total_lines": 3
294
699
  },
295
- "/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
+ ],
296
738
  "lines": [
297
739
  1,
298
740
  1,
@@ -329,9 +771,75 @@
329
771
  null,
330
772
  null,
331
773
  null
332
- ]
774
+ ],
775
+ "lines_covered_percent": 100.0,
776
+ "covered_lines": 18,
777
+ "missed_lines": 0,
778
+ "omitted_lines": 17,
779
+ "total_lines": 18
333
780
  },
334
- "/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
+ ],
335
843
  "lines": [
336
844
  1,
337
845
  1,
@@ -392,9 +900,22 @@
392
900
  null,
393
901
  null,
394
902
  null
395
- ]
903
+ ],
904
+ "lines_covered_percent": 100.0,
905
+ "covered_lines": 5,
906
+ "missed_lines": 0,
907
+ "omitted_lines": 54,
908
+ "total_lines": 5
396
909
  },
397
- "/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
+ ],
398
919
  "lines": [
399
920
  1,
400
921
  1,
@@ -402,9 +923,158 @@
402
923
  1,
403
924
  null,
404
925
  null
405
- ]
926
+ ],
927
+ "lines_covered_percent": 100.0,
928
+ "covered_lines": 4,
929
+ "missed_lines": 0,
930
+ "omitted_lines": 2,
931
+ "total_lines": 4
406
932
  },
407
- "/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
+ ],
408
1078
  "lines": [
409
1079
  1,
410
1080
  1,
@@ -548,9 +1218,82 @@
548
1218
  null,
549
1219
  null,
550
1220
  null
551
- ]
1221
+ ],
1222
+ "lines_covered_percent": 100.0,
1223
+ "covered_lines": 74,
1224
+ "missed_lines": 0,
1225
+ "omitted_lines": 68,
1226
+ "total_lines": 74
552
1227
  },
553
- "/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
+ ],
554
1297
  "lines": [
555
1298
  1,
556
1299
  1,
@@ -618,9 +1361,59 @@
618
1361
  null,
619
1362
  null,
620
1363
  null
621
- ]
1364
+ ],
1365
+ "lines_covered_percent": 100.0,
1366
+ "covered_lines": 27,
1367
+ "missed_lines": 0,
1368
+ "omitted_lines": 39,
1369
+ "total_lines": 27
622
1370
  },
623
- "/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
+ ],
624
1417
  "lines": [
625
1418
  1,
626
1419
  1,
@@ -665,879 +1458,14 @@
665
1458
  null,
666
1459
  null,
667
1460
  null
668
- ]
669
- },
670
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/cloud/connection_spec.rb": {
671
- "lines": [
672
- 1,
673
- 23,
674
- null,
675
- 23,
676
- 22,
677
- 22,
678
- null,
679
- 1,
680
- 5,
681
- null,
682
- 1,
683
- 3,
684
- null,
685
- null,
686
- 1,
687
- 2,
688
- null,
689
- 3,
690
- null,
691
- null,
692
- 1,
693
- 3,
694
- null,
695
- null,
696
- 1,
697
- 2,
698
- null,
699
- 3,
700
- null,
701
- null,
702
- null,
703
- 1,
704
- 4,
705
- null,
706
- 4,
707
- null,
708
- 2,
709
- 2,
710
- null,
711
- 1,
712
- 1,
713
- null,
714
- null,
715
- null,
716
- null,
717
- null,
718
- null,
719
- null,
720
- null,
721
- null,
722
- null,
723
- null,
724
- null,
725
- null,
726
- null,
727
- null,
728
- 1,
729
- 4,
730
- null,
731
- 4,
732
- null,
733
- 1,
734
- 3,
735
- null,
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
- 1,
752
- 2,
753
- null,
754
- 1,
755
- 1,
756
- null,
757
- null,
758
- null,
759
- null,
760
- 1,
761
- 4,
762
- null,
763
- 4,
764
- null,
765
- 1,
766
- 3,
767
- null,
768
- 2,
769
- null,
770
- 1,
771
- 1,
772
- null,
773
- null,
774
- null,
775
- null,
776
- null,
777
- null,
778
- null,
779
- null,
780
- null,
781
- null,
782
- null,
783
- null,
784
- 1,
785
- 2,
786
- null,
787
- 1,
788
- 1,
789
- null,
790
- null,
791
- null,
792
- null,
793
- 1,
794
- 4,
795
- null,
796
- 4,
797
- null,
798
- 1,
799
- 3,
800
- null,
801
- 2,
802
- null,
803
- 1,
804
- 1,
805
- null,
806
- null,
807
- null,
808
- null,
809
- null,
810
- null,
811
- null,
812
- null,
813
- null,
814
- null,
815
- null,
816
- null,
817
- null,
818
- null,
819
- null,
820
- null,
821
- null,
822
- 1,
823
- 2,
824
- null,
825
- 1,
826
- 1,
827
- null,
828
- null,
829
- null,
830
- null,
831
- 1,
832
- 4,
833
- null,
834
- 4,
835
- null,
836
- 1,
837
- 3,
838
- null,
839
- 2,
840
- null,
841
- 1,
842
- 1,
843
- null,
844
- null,
845
- null,
846
- null,
847
- null,
848
- null,
849
- null,
850
- null,
851
- null,
852
- null,
853
- null,
854
- null,
855
- null,
856
- null,
857
- null,
858
- null,
859
- null,
860
- 1,
861
- 2,
862
- null,
863
- 1,
864
- 1,
865
- null,
866
- null,
867
- null,
868
- null,
869
- 1,
870
- 4,
871
- null,
872
- 4,
873
- null,
874
- 4,
875
- null,
876
- 1,
877
- 1,
878
- 1,
879
- 1,
880
- null,
881
- 1,
882
- 1,
883
- null,
884
- null,
885
- 1,
886
- 1,
887
- 1,
888
- 1,
889
- 1,
890
- 1,
891
- null,
892
- 1,
893
- 1,
894
- null,
895
- null,
896
- 1,
897
- 1,
898
- 1,
899
- 1,
900
- 2,
901
- 1,
902
- 1,
903
- null,
904
- 1,
905
- 1,
906
- null,
907
- null,
908
- null
909
- ]
910
- },
911
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/request_spec.rb": {
912
- "lines": [
913
- 1,
914
- 23,
915
- 1,
916
- 23,
917
- null,
918
- null,
919
- 1,
920
- 19,
921
- null,
922
- 1,
923
- 2,
924
- null,
925
- 2,
926
- null,
927
- null,
928
- 1,
929
- 2,
930
- null,
931
- 2,
932
- null,
933
- null,
934
- 1,
935
- 2,
936
- null,
937
- 2,
938
- null,
939
- null,
940
- 1,
941
- 2,
942
- null,
943
- 2,
944
- null,
945
- null,
946
- 1,
947
- 2,
948
- null,
949
- 2,
950
- null,
951
- null,
952
- 1,
953
- 2,
954
- null,
955
- 2,
956
- null,
957
- null,
958
- 1,
959
- 2,
960
- null,
961
- 2,
962
- null,
963
- null,
964
- 1,
965
- 2,
966
- null,
967
- 2,
968
- null,
969
- null,
970
- 1,
971
- 2,
972
- null,
973
- 2,
974
- null,
975
- null,
976
- 1,
977
- 2,
978
- null,
979
- 2,
980
- null,
981
- null,
982
- 1,
983
- 2,
984
- null,
985
- 2,
986
- null,
987
- null,
988
- 1,
989
- 2,
990
- null,
991
- 2,
992
- null,
993
- null,
994
- 1,
995
- 2,
996
- null,
997
- 2,
998
- null,
999
- null,
1000
- 1,
1001
- 3,
1002
- null,
1003
- 2,
1004
- 2,
1005
- null,
1006
- null,
1007
- 1,
1008
- 2,
1009
- null,
1010
- 2,
1011
- null,
1012
- null,
1013
- 1,
1014
- 2,
1015
- null,
1016
- 2,
1017
- null,
1018
- null,
1019
- 1,
1020
- 2,
1021
- null,
1022
- 2,
1023
- null,
1024
- null,
1025
- null,
1026
- 1,
1027
- 2,
1028
- null,
1029
- 2,
1030
- null,
1031
- 1,
1032
- 2,
1033
- null,
1034
- null,
1035
- null,
1036
- 1,
1037
- 2,
1038
- null,
1039
- 1,
1040
- 1,
1041
- null,
1042
- null,
1043
- 1,
1044
- 2,
1045
- null,
1046
- null,
1047
- null,
1048
- 1,
1049
- 2,
1050
- null,
1051
- 2,
1052
- null,
1053
- 1,
1054
- 2,
1055
- null,
1056
- null,
1057
- null,
1058
- 1,
1059
- 1,
1060
- 1,
1061
- null,
1062
- null,
1063
- null,
1064
- null,
1065
- null,
1066
- 1,
1067
- 2,
1068
- null,
1069
- 2,
1070
- null,
1071
- null,
1072
- null,
1073
- 1,
1074
- 1,
1075
- 1,
1076
- null,
1077
- null,
1078
- null,
1079
- null,
1080
- null,
1081
- 1,
1082
- 2,
1083
- null,
1084
- 2,
1085
- null,
1086
- null,
1087
- null,
1088
- 1,
1089
- 1,
1090
- 3,
1091
- null,
1092
- null,
1093
- null,
1094
- null,
1095
- null,
1096
- null,
1097
- null,
1098
- null,
1099
- null,
1100
- 1,
1101
- 1,
1102
- 2,
1103
- null,
1104
- 2,
1105
- null,
1106
- null,
1107
- 1,
1108
- 2,
1109
- null,
1110
- 1,
1111
- 2,
1112
- null,
1113
- null,
1114
- null,
1115
- 1,
1116
- 2,
1117
- null,
1118
- 1,
1119
- 1,
1120
- null,
1121
- null,
1122
- null,
1123
- null,
1124
- null,
1125
- null,
1126
- 1,
1127
- 2,
1128
- null,
1129
- null,
1130
- null,
1131
- null,
1132
- null,
1133
- null
1134
- ]
1135
- },
1136
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/state_spec.rb": {
1137
- "lines": [
1138
- 1,
1139
- 6,
1140
- null,
1141
- 6,
1142
- null,
1143
- 1,
1144
- 5,
1145
- null,
1146
- null,
1147
- null,
1148
- 1,
1149
- 3,
1150
- null,
1151
- null,
1152
- null,
1153
- null,
1154
- null,
1155
- null,
1156
- null,
1157
- null,
1158
- null,
1159
- null,
1160
- null,
1161
- null,
1162
- null,
1163
- null,
1164
- null,
1165
- 1,
1166
- 1,
1167
- null,
1168
- null,
1169
- null,
1170
- null,
1171
- 1,
1172
- 6,
1173
- null,
1174
- 1,
1175
- null,
1176
- 4,
1177
- null,
1178
- null,
1179
- null,
1180
- null,
1181
- null,
1182
- null,
1183
- null,
1184
- null,
1185
- 1,
1186
- 2,
1187
- null,
1188
- 1,
1189
- 1,
1190
- null,
1191
- null,
1192
- 2,
1193
- null,
1194
- null,
1195
- 1,
1196
- 2,
1197
- null,
1198
- 1,
1199
- 1,
1200
- null,
1201
- null,
1202
- 2,
1203
- null,
1204
- null,
1205
- 1,
1206
- 2,
1207
- null,
1208
- 1,
1209
- 1,
1210
- null,
1211
- null,
1212
- 2,
1213
- null,
1214
- null,
1215
- 1,
1216
- 2,
1217
- null,
1218
- 1,
1219
- 1,
1220
- null,
1221
- null,
1222
- 2,
1223
- null,
1224
- null,
1225
- 1,
1226
- 2,
1227
- null,
1228
- 1,
1229
- 2,
1230
- null,
1231
- null,
1232
- null,
1233
- null
1234
- ]
1235
- },
1236
- "/Users/ledermann/Projects/solectrus/senec/spec/lib/senec/local/value_spec.rb": {
1237
- "lines": [
1238
- 1,
1239
- 17,
1240
- null,
1241
- 1,
1242
- 17,
1243
- null,
1244
- 1,
1245
- 3,
1246
- null,
1247
- 2,
1248
- 2,
1249
- null,
1250
- null,
1251
- 1,
1252
- 3,
1253
- null,
1254
- 2,
1255
- 2,
1256
- null,
1257
- null,
1258
- 1,
1259
- 3,
1260
- null,
1261
- 2,
1262
- 2,
1263
- null,
1264
- null,
1265
- 1,
1266
- 3,
1267
- null,
1268
- 2,
1269
- 2,
1270
- null,
1271
- null,
1272
- 1,
1273
- 3,
1274
- null,
1275
- 2,
1276
- 2,
1277
- null,
1278
- null,
1279
- 1,
1280
- 3,
1281
- null,
1282
- 2,
1283
- 2,
1284
- null,
1285
- null,
1286
- 1,
1287
- 3,
1288
- null,
1289
- 2,
1290
- 2,
1291
- null,
1292
- null,
1293
- 1,
1294
- 2,
1295
- null,
1296
- 3,
1297
- null,
1298
- null,
1299
- 1,
1300
- 2,
1301
- null,
1302
- 1,
1303
- 2,
1304
- null,
1305
- null,
1306
- null,
1307
- null
1308
- ]
1309
- },
1310
- "/Users/ledermann/Projects/solectrus/senec/spec/senec_spec.rb": {
1311
- "lines": [
1312
- 1,
1313
- 1,
1314
- 1,
1315
- null,
1316
- null
1317
- ]
1318
- },
1319
- "/Users/ledermann/Projects/solectrus/senec/spec/support/vcr.rb": {
1320
- "lines": [
1321
- 1,
1322
- null,
1323
- 1,
1324
- 1,
1325
- 9,
1326
- null,
1327
- null,
1328
- null,
1329
- 1,
1330
- 1,
1331
- 6,
1332
- null,
1333
- null,
1334
- null,
1335
- 1,
1336
- 2,
1337
- 23,
1338
- null,
1339
- null,
1340
- null,
1341
- 1,
1342
- 4,
1343
- null,
1344
- null,
1345
- null,
1346
- 1,
1347
- null,
1348
- null,
1349
- null,
1350
- null,
1351
- null,
1352
- null,
1353
- null,
1354
- null,
1355
- null,
1356
- null,
1357
- null,
1358
- null,
1359
- null,
1360
- null,
1361
- null,
1362
- null,
1363
- null,
1364
- null,
1365
- null,
1366
- null,
1367
- null,
1368
- null,
1369
- null,
1370
- null,
1371
- null,
1372
- null,
1373
- null,
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
- 1,
1410
- 226,
1411
- null,
1412
- null,
1413
- null,
1414
- null,
1415
- null,
1416
- null,
1417
- null,
1418
- null,
1419
- null,
1420
- null,
1421
- 1,
1422
- 226,
1423
- null,
1424
- null,
1425
- 1,
1426
- 1,
1427
- 1,
1428
- 1,
1429
- 1,
1430
- null,
1431
- 1,
1432
- 1,
1433
- null,
1434
- null,
1435
- null,
1436
- null,
1437
- null,
1438
- null,
1439
- null,
1440
- 1,
1441
- 113,
1442
- 113,
1443
- 113,
1444
- null,
1445
- null,
1446
- null,
1447
- 1,
1448
- 115,
1449
- null,
1450
- 1,
1451
- 115,
1452
- null,
1453
- 1,
1454
- 115,
1455
- null,
1456
- 1,
1457
- 115,
1458
- 115,
1459
- null,
1460
- 1,
1461
- 115,
1462
- 115,
1463
- null,
1464
- null,
1465
- null,
1466
- "ignored",
1467
- "ignored",
1468
- "ignored",
1469
- "ignored",
1470
- "ignored",
1471
- "ignored",
1472
- "ignored",
1473
- "ignored",
1474
- "ignored",
1475
- "ignored",
1476
- "ignored",
1477
- "ignored",
1478
- "ignored",
1479
- "ignored",
1480
- "ignored",
1481
- "ignored",
1482
- "ignored",
1483
- "ignored",
1484
- "ignored",
1485
- "ignored",
1486
- "ignored",
1487
- "ignored",
1488
- "ignored",
1489
- "ignored",
1490
- "ignored",
1491
- "ignored",
1492
- "ignored",
1493
- "ignored",
1494
- "ignored",
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
- null,
1520
- null,
1521
- null,
1522
- null,
1523
- 1,
1524
- 1,
1525
- null,
1526
- 1,
1527
- null,
1528
- null,
1529
- null,
1530
- 1,
1531
- 5,
1532
- 5,
1533
- null,
1534
- null,
1535
- null,
1536
- null,
1537
- 71,
1538
- null
1539
- ]
1461
+ ],
1462
+ "lines_covered_percent": 100.0,
1463
+ "covered_lines": 23,
1464
+ "missed_lines": 0,
1465
+ "omitted_lines": 20,
1466
+ "total_lines": 23
1540
1467
  }
1541
1468
  },
1542
- "groups": {}
1469
+ "groups": {},
1470
+ "errors": {}
1543
1471
  }