sim-meter-au 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e71372ec0601bc9629010ed55c2a980570d729cac3f8919ffb4899a689231a28
4
+ data.tar.gz: a00ea1d08ab83fd4e61bba171de9d5565ca71da34e276e246b7c68cb8b49de45
5
+ SHA512:
6
+ metadata.gz: c9cf1f8112060047adff81045cceb2673c61963743d2800852760bafdc46c35feaa87b395cb94134bf5a0a2757d0952ff5a001b9fb339cad26b79f82bfa05024
7
+ data.tar.gz: 31773f757c185ead61ecaf975b7b37df4b15d844a3b22c9b175e830c10aebe65b740495c8de15d78ee693bb9df985757a729a4c2b9fbeb744c54cd2051d8d695
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Darren Jeacocke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Sim Meter AU
2
+
3
+ Sim Meter AU provides a unified Ruby API for retrieving services, plan details,
4
+ data balances, expiry dates and usage from Australian mobile providers.
5
+
6
+ It currently supports ALDImobile through authenticated HTTP requests and
7
+ HTML/JSON parsing. It does not execute the provider portal's JavaScript and does
8
+ not require a web browser.
9
+
10
+ ## Installation
11
+
12
+ Add the gem to your Gemfile:
13
+
14
+ ```ruby
15
+ gem "sim-meter-au"
16
+ ```
17
+
18
+ Then install it:
19
+
20
+ ```sh
21
+ bundle install
22
+ ```
23
+
24
+ ## Providers
25
+
26
+ The currently available provider is:
27
+
28
+ - `:aldi_mobile` — ALDImobile
29
+
30
+ Every provider inherits from `SimMeterAU::Provider` and implements the same
31
+ operations: authentication, services, service details, usage and orders. This
32
+ keeps application code consistent as additional Australian providers are
33
+ added.
34
+
35
+ Use `SimMeterAU.providers` to get the registered provider names.
36
+
37
+ ## Ruby API
38
+
39
+ Create a client by selecting a provider and supplying that provider's
40
+ credentials:
41
+
42
+ ```ruby
43
+ require "sim_meter_au"
44
+
45
+ client = SimMeterAU.client(
46
+ provider: :aldi_mobile,
47
+ login: ENV.fetch("ALDI_MOBILE_LOGIN"),
48
+ password: ENV.fetch("ALDI_MOBILE_PASSWORD")
49
+ )
50
+
51
+ client.authenticate!
52
+ ```
53
+
54
+ ### Services
55
+
56
+ List every service on the account:
57
+
58
+ ```ruby
59
+ services = client.services
60
+ services.map(&:to_h)
61
+ ```
62
+
63
+ ```ruby
64
+ [
65
+ {id: "1234567", name: "Data SIM", number: "0400000001"},
66
+ {id: "7654321", name: "Camera", number: "0400000002"}
67
+ ]
68
+ ```
69
+
70
+ ### Service details
71
+
72
+ A service can be selected by its provider ID, mobile number or unique nickname:
73
+
74
+ ```ruby
75
+ details = client.service_details("0400000001")
76
+ details.to_h
77
+ ```
78
+
79
+ ```ruby
80
+ {
81
+ service: {
82
+ id: "1234567",
83
+ name: "Data SIM",
84
+ number: "0400000001"
85
+ },
86
+ status: "active_plan",
87
+ plan: {
88
+ name: "$240 Data Plan",
89
+ type: "data",
90
+ expires_on: "2027-04-28",
91
+ days_remaining: 246,
92
+ auto_recharge: false
93
+ },
94
+ data: {
95
+ plan_remaining_bytes: 176_093_659_136,
96
+ rollover_bytes: 0,
97
+ total_remaining_bytes: 176_093_659_136
98
+ },
99
+ payg: {
100
+ balance_cents: nil,
101
+ expires_on: "2027-05-01",
102
+ days_until_expiry: 249
103
+ },
104
+ roaming_enabled: false,
105
+ retrieved_at: "2026-08-25T05:00:00Z"
106
+ }
107
+ ```
108
+
109
+ ### Other available API
110
+
111
+ - `SimMeterAU.providers` lists the registered provider names.
112
+ - `SimMeterAU.provider_class(provider)` returns the adapter class for a provider.
113
+ - `client.provider` returns the selected provider name.
114
+ - `client.service(identifier)` finds a service by provider ID, mobile number or
115
+ unique nickname.
116
+ - `client.usage(identifier, from:, to:)` retrieves usage summaries and
117
+ individual usage records. Dates may be `Date` objects or ISO date strings.
118
+ - `client.orders(identifier)` retrieves order history, costs and transaction
119
+ references.
120
+ - `client.authenticated?` reports whether the current provider session is
121
+ authenticated.
122
+
123
+ ## Command-line utility
124
+
125
+ The gem includes a provider-aware `sim-meter-au` command. For local development,
126
+ put the provider credentials in a `.env` file:
127
+
128
+ ```dotenv
129
+ ALDI_MOBILE_LOGIN=0412345678
130
+ ALDI_MOBILE_PASSWORD=your-password
131
+ ```
132
+
133
+ Provider selection is explicit. Pass `--provider NAME` or `-p NAME` before each
134
+ data command. Credential environment variables follow the provider key, so a
135
+ future `example_mobile` adapter can declare variables such as
136
+ `EXAMPLE_MOBILE_LOGIN` and `EXAMPLE_MOBILE_PASSWORD`.
137
+
138
+ Run the command without arguments, or use the `providers` command, to see the
139
+ available providers:
140
+
141
+ ```sh
142
+ bundle exec sim-meter-au
143
+ bundle exec sim-meter-au providers
144
+ ```
145
+
146
+ ```text
147
+ PROVIDER NAME
148
+ ----------- ----------
149
+ aldi_mobile ALDImobile
150
+ ```
151
+
152
+ ### Services
153
+
154
+ ```sh
155
+ bundle exec sim-meter-au --provider aldi_mobile services
156
+ ```
157
+
158
+ ```text
159
+ NAME NUMBER SERVICE ID
160
+ -------- ---------- ----------
161
+ Data SIM 0400000001 1234567
162
+ Camera 0400000002 7654321
163
+ ```
164
+
165
+ ### Service details
166
+
167
+ ```sh
168
+ bundle exec sim-meter-au --provider aldi_mobile service 0400000001
169
+ ```
170
+
171
+ ```text
172
+ Data SIM (0400000001)
173
+ ---------------------
174
+ Service ID: 1234567
175
+ Status: active plan
176
+
177
+ Plan
178
+ Name: $240 Data Plan
179
+ Type: data
180
+ Expires: 28/04/2027
181
+ Days remaining: 246
182
+ Auto recharge: Disabled
183
+
184
+ Data
185
+ Plan remaining: 164 GB
186
+ Rollover: 0 B
187
+ Total remaining: 164 GB
188
+
189
+ PAYG and service
190
+ Service expires: 01/05/2027
191
+ Days until expiry: 249
192
+
193
+ Roaming: Disabled
194
+ Retrieved: 2026-08-25T15:00:00+10:00
195
+ ```
196
+
197
+ There is also a `--json` option for all data commands if you need formatted
198
+ output.
199
+
200
+ ### Available commands
201
+
202
+ ```sh
203
+ # List available providers
204
+ bundle exec sim-meter-au
205
+ bundle exec sim-meter-au providers
206
+
207
+ # Select a provider explicitly
208
+ bundle exec sim-meter-au --provider aldi_mobile services
209
+
210
+ # List services
211
+ bundle exec sim-meter-au --provider aldi_mobile services
212
+
213
+ # Retrieve details for every service
214
+ bundle exec sim-meter-au --provider aldi_mobile services --details
215
+
216
+ # Retrieve details for one service
217
+ bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER
218
+
219
+ # Retrieve a usage summary for the last 30 days
220
+ bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage
221
+
222
+ # Query a specific usage range
223
+ bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage \
224
+ --from 2026-08-01 \
225
+ --to 2026-08-25
226
+
227
+ # Include individual usage records
228
+ bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage --details
229
+
230
+ # Retrieve order history
231
+ bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER orders
232
+
233
+ # Show help or the gem version
234
+ bundle exec sim-meter-au --help
235
+ bundle exec sim-meter-au --version
236
+ ```
237
+
238
+ Sim Meter AU is not affiliated with or endorsed by any supported provider,
239
+ including ALDI or Medion Australia.
240
+
241
+ ## License
242
+
243
+ Sim Meter AU is available under the [MIT License](LICENSE.md).
data/exe/sim-meter-au ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "dotenv/load"
5
+ require "sim_meter_au"
6
+
7
+ exit SimMeterAU::CLI.new.run(ARGV)
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Supports Bundler's automatic require for the dashed gem name.
4
+ require_relative "sim_meter_au"
@@ -0,0 +1,416 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require "date"
6
+
7
+ module SimMeterAU
8
+ class CLI
9
+ PROVIDER_COMMANDS = %w[services service].freeze
10
+
11
+ def initialize(out: $stdout, err: $stderr, env: ENV, client_factory: nil)
12
+ @out = out
13
+ @err = err
14
+ @env = env
15
+ @client_factory = client_factory || method(:build_client)
16
+ end
17
+
18
+ def run(arguments)
19
+ arguments = arguments.dup
20
+ global_options = parse_global_options(arguments)
21
+ return global_options if global_options.is_a?(Integer)
22
+
23
+ @provider_name = global_options.fetch(:provider)
24
+ command = arguments.shift || "providers"
25
+
26
+ if PROVIDER_COMMANDS.include?(command) && @provider_name.to_s.empty?
27
+ raise ConfigurationError,
28
+ "Select a provider with --provider NAME (available: #{SimMeterAU.providers.join(", ")})"
29
+ end
30
+
31
+ case command
32
+ when "providers"
33
+ list_providers(arguments)
34
+ when "services"
35
+ list_services(arguments)
36
+ when "service"
37
+ service_command(arguments)
38
+ when "-h", "--help"
39
+ @out.puts help
40
+ 0
41
+ when "-v", "--version"
42
+ @out.puts VERSION
43
+ 0
44
+ else
45
+ @err.puts "Unknown command: #{command}"
46
+ @err.puts help
47
+ 1
48
+ end
49
+ rescue ConfigurationError, AuthenticationError, ConnectionError,
50
+ UnexpectedResponseError, ServiceNotFoundError, AmbiguousServiceError => error
51
+ @err.puts error.message
52
+ 1
53
+ end
54
+
55
+ private
56
+
57
+ def list_providers(arguments)
58
+ options = {json: false}
59
+ parser = OptionParser.new do |opts|
60
+ opts.banner = "Usage: sim-meter-au providers [--json]"
61
+ opts.on("--json", "Print providers as JSON") { options[:json] = true }
62
+ end
63
+ parser.parse!(arguments)
64
+
65
+ providers = SimMeterAU.providers.map do |key|
66
+ provider_class = SimMeterAU.provider_class(key)
67
+ {key:, name: provider_class.display_name}
68
+ end
69
+
70
+ if options[:json]
71
+ @out.puts JSON.pretty_generate(providers)
72
+ else
73
+ print_table(
74
+ ["PROVIDER", "NAME"],
75
+ providers.map { |provider| [provider[:key], provider[:name]] }
76
+ )
77
+ end
78
+ 0
79
+ rescue OptionParser::ParseError => error
80
+ option_error(error, parser)
81
+ end
82
+
83
+ def parse_global_options(arguments)
84
+ options = {
85
+ provider: nil
86
+ }
87
+ parser = OptionParser.new do |opts|
88
+ opts.banner = "Usage: sim-meter-au [options] COMMAND"
89
+ opts.on("-p", "--provider NAME", "Provider to use") do |name|
90
+ options[:provider] = name
91
+ end
92
+ opts.on("-h", "--help", "Show this help") { options[:help] = true }
93
+ opts.on("-v", "--version", "Show the gem version") { options[:version] = true }
94
+ end
95
+ parser.order!(arguments)
96
+ if options[:help]
97
+ @out.puts help
98
+ return 0
99
+ end
100
+ if options[:version]
101
+ @out.puts VERSION
102
+ return 0
103
+ end
104
+ options
105
+ rescue OptionParser::ParseError => error
106
+ @err.puts error.message
107
+ @err.puts parser
108
+ 1
109
+ end
110
+
111
+ def list_services(arguments)
112
+ options = {json: false, details: false}
113
+ parser = OptionParser.new do |opts|
114
+ opts.banner = "Usage: sim-meter-au services [--details] [--json]"
115
+ opts.on("--details", "Retrieve plan and balance details") { options[:details] = true }
116
+ opts.on("--json", "Print services as JSON") { options[:json] = true }
117
+ end
118
+ parser.parse!(arguments)
119
+
120
+ client.authenticate!
121
+ services = client.services
122
+ results = if options[:details]
123
+ services.map { |service| client.service_details(service) }
124
+ else
125
+ services
126
+ end
127
+
128
+ if options[:json]
129
+ @out.puts JSON.pretty_generate(results.map(&:to_h))
130
+ elsif options[:details]
131
+ results.each_with_index do |details, index|
132
+ @out.puts if index.positive?
133
+ print_service_details(details)
134
+ end
135
+ else
136
+ print_service_table(services)
137
+ end
138
+
139
+ 0
140
+ rescue OptionParser::ParseError => error
141
+ @err.puts error.message
142
+ @err.puts parser
143
+ 1
144
+ end
145
+
146
+ def service_command(arguments)
147
+ identifier = arguments.shift
148
+ raise ConfigurationError, "Usage: sim-meter-au service IDENTIFIER [usage|orders]" if identifier.to_s.empty?
149
+
150
+ subcommand = arguments.first
151
+ case subcommand
152
+ when "usage"
153
+ arguments.shift
154
+ service_usage(identifier, arguments)
155
+ when "orders"
156
+ arguments.shift
157
+ service_orders(identifier, arguments)
158
+ else
159
+ service_details(identifier, arguments)
160
+ end
161
+ end
162
+
163
+ def service_details(identifier, arguments)
164
+ options = {json: false}
165
+ parser = OptionParser.new do |opts|
166
+ opts.banner = "Usage: sim-meter-au service IDENTIFIER [--json]"
167
+ opts.on("--json", "Print details as JSON") { options[:json] = true }
168
+ end
169
+ parser.parse!(arguments)
170
+
171
+ client.authenticate!
172
+ details = client.service_details(identifier)
173
+
174
+ if options[:json]
175
+ @out.puts JSON.pretty_generate(details.to_h)
176
+ else
177
+ print_service_details(details)
178
+ end
179
+ 0
180
+ rescue OptionParser::ParseError => error
181
+ option_error(error, parser)
182
+ end
183
+
184
+ def service_usage(identifier, arguments)
185
+ options = {
186
+ json: false,
187
+ details: false,
188
+ from: Date.today - 30,
189
+ to: Date.today
190
+ }
191
+ parser = OptionParser.new do |opts|
192
+ opts.banner = "Usage: sim-meter-au service IDENTIFIER usage [options]"
193
+ opts.on("--from DATE", "Start date in YYYY-MM-DD format") { |date| options[:from] = parse_date(date) }
194
+ opts.on("--to DATE", "End date in YYYY-MM-DD format") { |date| options[:to] = parse_date(date) }
195
+ opts.on("--details", "Include individual usage records") { options[:details] = true }
196
+ opts.on("--json", "Print usage as JSON") { options[:json] = true }
197
+ end
198
+ parser.parse!(arguments)
199
+
200
+ client.authenticate!
201
+ report = client.usage(identifier, from: options[:from], to: options[:to])
202
+
203
+ if options[:json]
204
+ payload = report.to_h
205
+ payload[:records] = [] unless options[:details]
206
+ payload[:record_count] = report.records.length
207
+ @out.puts JSON.pretty_generate(payload)
208
+ else
209
+ print_usage(report, include_records: options[:details])
210
+ end
211
+ 0
212
+ rescue OptionParser::ParseError => error
213
+ option_error(error, parser)
214
+ end
215
+
216
+ def service_orders(identifier, arguments)
217
+ options = {json: false}
218
+ parser = OptionParser.new do |opts|
219
+ opts.banner = "Usage: sim-meter-au service IDENTIFIER orders [--json]"
220
+ opts.on("--json", "Print orders as JSON") { options[:json] = true }
221
+ end
222
+ parser.parse!(arguments)
223
+
224
+ client.authenticate!
225
+ orders = client.orders(identifier)
226
+
227
+ if options[:json]
228
+ @out.puts JSON.pretty_generate(orders.map(&:to_h))
229
+ else
230
+ print_orders(orders)
231
+ end
232
+ 0
233
+ rescue OptionParser::ParseError => error
234
+ option_error(error, parser)
235
+ end
236
+
237
+ def print_service_table(services)
238
+ if services.empty?
239
+ @out.puts "No services found."
240
+ return
241
+ end
242
+
243
+ print_table(
244
+ ["NAME", "NUMBER", "SERVICE ID"],
245
+ services.map { |service| [service.name, service.number, service.id] }
246
+ )
247
+ end
248
+
249
+ def print_service_details(details)
250
+ service = details.service
251
+ @out.puts "#{service.name} (#{service.number})"
252
+ @out.puts "-" * (service.name.length + service.number.length + 3)
253
+ print_value("Service ID", service.id)
254
+ print_value("Status", details.status.tr("_", " "))
255
+
256
+ if details.plan
257
+ @out.puts
258
+ @out.puts "Plan"
259
+ print_value("Name", details.plan.name)
260
+ print_value("Type", details.plan.type)
261
+ print_value("Expires", format_date(details.plan.expires_on))
262
+ print_value("Days remaining", details.plan.days_remaining)
263
+ print_value("Auto recharge", format_boolean(details.plan.auto_recharge))
264
+ else
265
+ @out.puts
266
+ print_value("Plan", "None")
267
+ end
268
+
269
+ if details.data
270
+ @out.puts
271
+ @out.puts "Data"
272
+ print_value("Plan remaining", format_bytes(details.data.plan_remaining_bytes))
273
+ print_value("Rollover", format_bytes(details.data.rollover_bytes))
274
+ print_value("Total remaining", format_bytes(details.data.total_remaining_bytes))
275
+ end
276
+
277
+ if details.payg
278
+ @out.puts
279
+ @out.puts "PAYG and service"
280
+ print_value("Balance", format_cents(details.payg.balance_cents))
281
+ print_value("Service expires", format_date(details.payg.expires_on))
282
+ print_value("Days until expiry", details.payg.days_until_expiry)
283
+ end
284
+
285
+ @out.puts
286
+ print_value("Roaming", format_boolean(details.roaming_enabled))
287
+ print_value("Retrieved", details.retrieved_at.iso8601)
288
+ end
289
+
290
+ def print_usage(report, include_records:)
291
+ @out.puts "Usage for #{report.service.name} (#{report.service.number})"
292
+ @out.puts "#{report.from.iso8601} to #{report.to.iso8601}"
293
+ @out.puts
294
+
295
+ summary_rows = report.summaries.map do |summary|
296
+ [summary.type, summary.count.to_s, summary.rounded_usage, "$#{summary.charge_dollars}"]
297
+ end
298
+ print_table(["TYPE", "COUNT", "ROUNDED USAGE", "CHARGE"], summary_rows)
299
+ @out.puts "No usage found." if summary_rows.empty?
300
+ @out.puts "Individual records: #{report.records.length}"
301
+
302
+ return unless include_records && report.records.any?
303
+
304
+ @out.puts
305
+ rows = report.records.map do |record|
306
+ [record.type.to_s, record.destination, record.usage, "$#{record.charge_dollars}", record.date_label]
307
+ end
308
+ print_table(["TYPE", "DESTINATION", "USAGE", "CHARGE", "DATE"], rows)
309
+ end
310
+
311
+ def print_orders(orders)
312
+ if orders.empty?
313
+ @out.puts "No orders found."
314
+ return
315
+ end
316
+
317
+ rows = orders.map do |order|
318
+ [order.id, order.type, order.plan_name, format_cents(order.total_cents), order.date_label]
319
+ end
320
+ print_table(["ORDER", "TYPE", "PLAN", "TOTAL", "DATE"], rows)
321
+ end
322
+
323
+ def print_table(headers, rows)
324
+ all_rows = [headers] + rows.map { |row| row.map(&:to_s) }
325
+ widths = all_rows.transpose.map { |column| column.map(&:length).max }
326
+
327
+ all_rows.each_with_index do |row, index|
328
+ @out.puts row.each_with_index.map { |value, column| value.ljust(widths[column]) }.join(" ").rstrip
329
+ @out.puts widths.map { |width| "-" * width }.join(" ") if index.zero?
330
+ end
331
+ end
332
+
333
+ def print_value(label, value)
334
+ return if value.nil?
335
+
336
+ @out.puts format("%-18s %s", "#{label}:", value)
337
+ end
338
+
339
+ def format_bytes(bytes)
340
+ return if bytes.nil?
341
+ return "0 B" if bytes.zero?
342
+
343
+ units = %w[B KB MB GB TB]
344
+ value = bytes.to_f
345
+ unit = units.shift
346
+ while value >= 1024 && units.any?
347
+ value /= 1024
348
+ unit = units.shift
349
+ end
350
+ precision = value >= 100 ? 0 : value >= 10 ? 1 : 2
351
+ "#{format("%.#{precision}f", value)} #{unit}"
352
+ end
353
+
354
+ def format_cents(cents)
355
+ return if cents.nil?
356
+
357
+ format("$%.2f", cents / 100.0)
358
+ end
359
+
360
+ def format_date(date)
361
+ date&.strftime("%d/%m/%Y")
362
+ end
363
+
364
+ def format_boolean(value)
365
+ return "Unknown" if value.nil?
366
+
367
+ value ? "Enabled" : "Disabled"
368
+ end
369
+
370
+ def parse_date(value)
371
+ Date.iso8601(value)
372
+ rescue Date::Error
373
+ raise OptionParser::InvalidArgument, "invalid date: #{value}"
374
+ end
375
+
376
+ def option_error(error, parser)
377
+ @err.puts error.message
378
+ @err.puts parser
379
+ 1
380
+ end
381
+
382
+ def client
383
+ @client ||= @client_factory.call(provider: @provider_name, **credentials)
384
+ end
385
+
386
+ def credentials
387
+ SimMeterAU.provider_class(@provider_name).credentials_from(@env)
388
+ end
389
+
390
+ def build_client(provider:, **credentials)
391
+ SimMeterAU.client(provider:, **credentials)
392
+ end
393
+
394
+ def help
395
+ <<~HELP
396
+ Usage: sim-meter-au [options] COMMAND
397
+
398
+ Provider:
399
+ -p, --provider NAME Select a provider (required for data commands)
400
+
401
+ Commands:
402
+ providers List available providers (default command)
403
+ services List services on the account
404
+ services --details
405
+ List details for every service
406
+ service ID Show plan, data, expiry and roaming details
407
+ service ID usage Show usage for a date range
408
+ service ID orders Show order history
409
+
410
+ Options:
411
+ -h, --help Show this help
412
+ -v, --version Show the gem version
413
+ HELP
414
+ end
415
+ end
416
+ end