mixergy 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: ba0e27e53239f2c48a1a6ea61fa42e6a4e2b2ecf5d030d588e35d58e47497ac2
4
+ data.tar.gz: 855226c79aa3e988c883fcbb2fb629e70acbeea7846f4a1b09198c752562911d
5
+ SHA512:
6
+ metadata.gz: fdc10ec896960d3e8afda031b8810504807b1c6bad3c74af12b8ee0f21379ac8d3e541d3b0e40ec4b2837eb2753f4f0b1c45d853032ba5901a13fdc4cb93a153
7
+ data.tar.gz: b6d8f53dcdcc806d378c6e9ad1d0dcd4fa9943069e2808d2d2c61924aea11f29311d72b4cca97ee25a52a1c230aee555fda681f39ad58d70d3c6d144ce650da6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Nicholas Humfrey
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+
2
+ # Mixergy Ruby Client
3
+
4
+ This gem provides a Ruby interface to the Mixergy Hot Water Tank REST API, making it easy to interact with tanks in a Rubyish way. There is also a CLI tool included to help with fetching an authentication token and interacting with the API.
5
+
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'mixergy', git: 'https://github.com/njh/ruby-mixergy.git'
13
+ ```
14
+
15
+ Or install it directly:
16
+
17
+ ```bash
18
+ git clone https://github.com/njh/ruby-mixergy.git
19
+ cd ruby-mixergy
20
+ bundle install
21
+ ```
22
+
23
+
24
+ ## Command Line Interface
25
+
26
+ Included in the gem is a command line tool, to perform actions with the Mixergy API:
27
+
28
+ ```sh
29
+ $ mixergy help
30
+ Commands:
31
+ mixergy boost # Charge tank to 100%
32
+ mixergy charge PERCENT # Charge tank to target percentage
33
+ mixergy help [COMMAND] # Describe available commands or one specific command
34
+ mixergy login # Login to Mixergy API and store token
35
+ mixergy status # Display the current status of a tank
36
+ mixergy tanks # List all your tanks
37
+ mixergy version # Prints the Mixergy gem version
38
+ ```
39
+
40
+ The `mixergy status` command displays some stats and a visualisation of the tank:
41
+
42
+ <img src="doc/screenshot.png" height="400" alt="A screenshot of a Mac OS terminal running `mixergy status`" />
43
+
44
+ To disable colour, set the `NO_COLOR` environment variable.
45
+
46
+
47
+ ## Config file format
48
+
49
+ To avoid logging-in before each API request, there is support for saving the authentication token to a configuration file, stored as YAML in `~/.mixergy`.
50
+
51
+ This file can be generated using the `mixergy login` CLI command.
52
+ The default tank ID is also stored, to avoid having to lookup a list of tanks each time too.
53
+
54
+ Example `~/.mixergy` file:
55
+
56
+ ```yaml
57
+ ---
58
+ token: user/ooShoh7naaR1lai0Ahtie5miechaig7ei/12ab/Tie6sha0vah7onohchifeich2aipheifaiqu9beiphoim4queitheinoMoo2a
59
+ default_tank_id: fcee127d-dad9-4ab4-a734-64586d0c0d68
60
+ ```
61
+
62
+
63
+ ## Development
64
+
65
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
66
+
67
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
68
+
69
+
70
+ ## Contributing
71
+
72
+ Bug reports and pull requests are welcome on GitHub at https://github.com/njh/ruby-mixergy.
73
+
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "standard/rake"
5
+ require "yard"
6
+
7
+ # YARD documentation task
8
+ YARD::Rake::YardocTask.new(:yard) do |t|
9
+ t.files = ["lib/**/*.rb"]
10
+ t.options = ["--output-dir", "doc"]
11
+ end
12
+
13
+ task default: :standard
Binary file
data/exe/mixergy ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+
7
+ require "thor"
8
+ require "mixergy"
9
+ require "mixergy/ascii_art"
10
+ require "io/console"
11
+
12
+ module Mixergy
13
+ class CLI < Thor
14
+ # Exit with a non-zero status code on failure
15
+ def self.exit_on_failure?
16
+ true
17
+ end
18
+
19
+ desc "version", "Prints the Mixergy gem version"
20
+ def version
21
+ puts Mixergy::VERSION
22
+ end
23
+
24
+ desc "login", "Login to Mixergy API and store token"
25
+ option :username, aliases: "-u", desc: "Username"
26
+ option :password, aliases: "-p", desc: "Password"
27
+ def login
28
+ username = options[:username] || prompt_for_username
29
+ password = options[:password] || prompt_for_password
30
+ client = Mixergy::Client.new
31
+ token = client.login(username, password)
32
+ if token
33
+ puts "Login successful!"
34
+ config = Mixergy::Config.new
35
+ puts "Writing token to #{config.filepath}"
36
+ config.load
37
+ config[:token] = token
38
+ if !config[:default_tank_id]
39
+ tank_id = client.tanks.first&.id
40
+ if tank_id
41
+ puts "Setting default tank ID to #{tank_id}"
42
+ config[:default_tank_id] = tank_id
43
+ end
44
+ end
45
+ config.save
46
+ end
47
+ end
48
+
49
+ desc "tanks", "List all your tanks"
50
+ def tanks
51
+ client = Mixergy::Client.new
52
+ client.load_config
53
+ tanks = client.tanks
54
+ if tanks.empty?
55
+ puts "No tanks found."
56
+ else
57
+ tanks.each do |tank|
58
+ puts "Serial: #{tank.serial_number}"
59
+ puts "Id: #{tank.id}"
60
+ puts
61
+ end
62
+ end
63
+ end
64
+
65
+ desc "status", "Display the current status of a tank"
66
+ def status
67
+ client = Mixergy::Client.new
68
+ client.load_config
69
+ # FIXME: add support for chosing a specific tank
70
+ status = client.status
71
+ puts "Charge: #{status.charge} %"
72
+ puts "Top Temperature: #{status.top_temperature} °C"
73
+ puts "Bottom Temperature: #{status.bottom_temperature} °C"
74
+ puts
75
+ puts Mixergy::AsciiArt.draw_tank(status.charge)
76
+ puts
77
+ end
78
+
79
+ desc "charge PERCENT", "Charge tank to target percentage"
80
+ def charge(percent)
81
+ percent = percent.to_i
82
+ unless percent.is_a?(Integer) && percent >= 0 && percent <= 100
83
+ raise ArgumentError, "percent must be an integer between 0 and 100."
84
+ end
85
+
86
+ client = Mixergy::Client.new
87
+ client.load_config
88
+ current_charge = client.status.charge
89
+ if client.set_charge(percent)
90
+ puts "Tank charging from #{current_charge} to #{percent}..."
91
+ else
92
+ puts "Failed to charge tank."
93
+ end
94
+ end
95
+
96
+ desc "boost", "Charge tank to 100%"
97
+ def boost
98
+ charge(100)
99
+ end
100
+
101
+ private
102
+
103
+ def prompt_for_username
104
+ print "Username: "
105
+ $stdin.gets.chomp
106
+ end
107
+
108
+ def prompt_for_password
109
+ print "Password: "
110
+ password = $stdin.noecho(&:gets)
111
+ puts
112
+ password.chomp
113
+ end
114
+ end
115
+ end
116
+
117
+ Mixergy::CLI.start
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Mixergy
6
+ module AsciiArt
7
+ # Draws an ASCII art hot water cylinder representing a charge level.
8
+ # @param charge [Numeric] The charge percentage (0-100).
9
+ # @param width [Integer] The width of the tank (default: 7).
10
+ # @param height [Integer] The height of the tank (default: 10).
11
+ # @return [String] The ASCII art representation of the tank.
12
+ def self.draw_tank(charge, width: 7, height: 10)
13
+ fill_height = (height * charge / 100.0).round
14
+ pastel = Pastel.new
15
+
16
+ tank = []
17
+ tank << " ╭#{"─" * width}╮"
18
+ height.times do |i|
19
+ tank << if i < height - fill_height
20
+ " │#{pastel.blue.on_blue(" ") * width}│"
21
+ else
22
+ " │#{pastel.red.on_red("█") * width}│"
23
+ end
24
+ end
25
+ tank << " ╰#{"─" * width}╯"
26
+ tank.join("\n")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require_relative "config"
6
+ require_relative "error"
7
+ require_relative "tank"
8
+ require_relative "status"
9
+
10
+ module Mixergy
11
+ class Client
12
+ API_ROOT = "https://www.mixergy.io/api/v2"
13
+
14
+ # Create a new Mixergy API client.
15
+ def initialize
16
+ @connection = Faraday.new(
17
+ url: API_ROOT
18
+ ) do |faraday|
19
+ faraday.request :json # Automatically encode request bodies as JSON
20
+ faraday.response :json # Automatically parse response bodies as JSON
21
+ end
22
+ end
23
+
24
+ # Loads configuration from ~/.mixergy and sets the API token if present.
25
+ # @return [Mixergy::Config] the loaded config object
26
+ def load_config
27
+ @config ||= begin
28
+ config = Mixergy::Config.new
29
+ config.load
30
+ # FIXME: find a better place to put this
31
+ @connection.headers["Authorization"] = "Bearer #{config[:token]}"
32
+ config
33
+ end
34
+ end
35
+
36
+ # Authenticates with the Mixergy API and stores the token in the connection.
37
+ # @param username [String] the username
38
+ # @param password [String] the password
39
+ # @return [String] the authentication token
40
+ # @raise [Mixergy::Error] if login fails
41
+ def login(username, password)
42
+ resp = @connection.post(
43
+ "account/login",
44
+ {username: username, password: password}
45
+ )
46
+ data = resp.body
47
+
48
+ if data["token"]
49
+ @connection.headers["Authorization"] = "Bearer #{data["token"]}"
50
+ data["token"]
51
+ else
52
+ raise Mixergy::Error, "Login failed (status: #{resp.status}, body: #{resp.body.inspect})"
53
+ end
54
+ end
55
+
56
+ # Fetches all tanks associated with the account.
57
+ # @return [Array<Tank>] list of Tank objects
58
+ def tanks
59
+ resp = @connection.get("tanks")
60
+ tank_list = resp.body.dig("_embedded", "tankList") || []
61
+ tank_list.map do |tank_data|
62
+ Tank.new(tank_data)
63
+ end
64
+ end
65
+
66
+ # Returns the default tank ID from config, or the first tank's ID.
67
+ # @return [String, nil] the default tank ID
68
+ def default_tank_id
69
+ @default_tank_id ||= begin
70
+ load_config
71
+ @config[:default_tank_id] || tanks.first&.id
72
+ end
73
+ end
74
+
75
+ # Fetches the latest status/measurement for a tank.
76
+ # @param tank [Tank, nil] the tank object (optional)
77
+ # @return [Status] the status object for the tank
78
+ def status(tank = nil)
79
+ tank_id = tank.id if tank.is_a?(Tank)
80
+ tank_id = default_tank_id if tank_id.nil?
81
+ resp = @connection.get("tanks/#{tank_id}/measurements/latest")
82
+ Status.new(resp.body)
83
+ end
84
+
85
+ # Sets the target charge for a tank via the control endpoint.
86
+ # @param percent [Integer] the target charge percentage
87
+ # @param tank [Tank, String, nil] the Tank object or Tank GUID (optional)
88
+ # @return [Boolean] true if successful, false otherwise
89
+ def set_charge(percent, tank = nil)
90
+ tank_id = tank.id if tank.is_a?(Tank)
91
+ tank_id = default_tank_id if tank_id.nil?
92
+ resp = @connection.put("tanks/#{tank_id}/control", {charge: percent})
93
+ resp.success?
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ # Mixergy::Config handles loading and saving user configuration as a YAML file.
7
+ # The default config file is ~/.mixergy.
8
+ module Mixergy
9
+ # Configuration handler for Mixergy CLI and API clients.
10
+ # Loads and saves config as YAML, provides hash-like access.
11
+ class Config
12
+ # Default path for the config file (~/.mixergy)
13
+ DEFAULT_CONFIG_PATH = File.expand_path("~/.mixergy")
14
+
15
+ # @return [String] Path to the config file
16
+ attr_reader :filepath
17
+ # @return [Hash] The loaded config data
18
+ attr_reader :data
19
+
20
+ # Create a new Config object and load config from disk.
21
+ # @param config_path [String] Optional path to config file
22
+ def initialize(config_path = DEFAULT_CONFIG_PATH)
23
+ @filepath = config_path
24
+ @data = load
25
+ end
26
+
27
+ # Load config from disk.
28
+ # @return [Hash] The loaded config data
29
+ def load
30
+ if File.exist?(@filepath)
31
+ YAML.load_file(@filepath) || {}
32
+ else
33
+ {}
34
+ end
35
+ end
36
+
37
+ # Save current config data to disk.
38
+ # @return [void]
39
+ def save
40
+ File.write(@filepath, YAML.dump(@data))
41
+ end
42
+
43
+ # Get a config value by key.
44
+ # @param key [String, Symbol]
45
+ # @return [Object, nil] Value for the key
46
+ def [](key)
47
+ @data[key.to_s]
48
+ end
49
+
50
+ # Set a config value by key.
51
+ # @param key [String, Symbol]
52
+ # @param value [Object]
53
+ # @return [void]
54
+ def []=(key, value)
55
+ @data[key.to_s] = value
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixergy::Error is the base exception class for all Mixergy-related errors.
4
+ # Raise this for API, configuration, or client errors.
5
+ module Mixergy
6
+ # Base exception class for Mixergy errors.
7
+ # All custom errors should inherit from this.
8
+ class Error < StandardError; end
9
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixergy::Status represents the latest measurement/status of a tank.
4
+ # It provides access to charge, temperature, and other metrics.
5
+ module Mixergy
6
+ # Represents the latest status/measurement for a Mixergy tank.
7
+ class Status
8
+ # @return [Float] Current charge percentage
9
+ attr_reader :charge
10
+ # @return [Float] Top temperature in Celsius
11
+ attr_reader :top_temperature
12
+ # @return [Float] Bottom temperature in Celsius
13
+ attr_reader :bottom_temperature
14
+ # @return [Float] Frequency in Hz
15
+ attr_reader :frequency
16
+ # @return [Float] Voltage in Volts
17
+ attr_reader :voltage
18
+ # @return [Float] Current in Amps
19
+ attr_reader :current
20
+
21
+ # Create a new Status object from API data.
22
+ # @param data [Hash] API response data for latest measurement
23
+ def initialize(data = {})
24
+ @charge = data["charge"]
25
+ @top_temperature = data["topTemperature"]
26
+ @bottom_temperature = data["bottomTemperature"]
27
+ @frequency = data["frequency"]
28
+ @voltage = data["voltage"]
29
+ @current = data["current"]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixergy::Tank represents a physical Mixergy tank and its metadata.
4
+ # Provides access to tank ID, type, model, volume, firmware version, and serial number.
5
+ module Mixergy
6
+ # Represents a Mixergy tank and its metadata.
7
+ class Tank
8
+ # @return [String] Unique tank ID
9
+ attr_reader :id
10
+ # @return [String] Tank type
11
+ attr_reader :type
12
+ # @return [String] Tank model code
13
+ attr_reader :model
14
+ # @return [Float] Tank volume in liters
15
+ attr_reader :volume
16
+ # @return [String] Firmware version
17
+ attr_reader :firmware_version
18
+ # @return [String] Serial number
19
+ attr_reader :serial_number
20
+
21
+ # Create a new Tank object from API data.
22
+ # @param data [Hash, nil] API response data for the tank
23
+ def initialize(data = nil)
24
+ if data
25
+ @id = data["id"]
26
+ @type = data["type"]
27
+ @model = data["tankModelCode"]
28
+ @volume = data["volume"]
29
+ @firmware_version = data["firmwareVersion"]
30
+ @serial_number = data["serialNumber"]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixergy gem version constant.
4
+ module Mixergy
5
+ # The current version of the Mixergy gem.
6
+ # Update this value for each gem release.
7
+ VERSION = "0.1.0"
8
+ end
data/lib/mixergy.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mixergy/version"
4
+ require_relative "mixergy/client"
5
+ require_relative "mixergy/config"
6
+ require_relative "mixergy/error"
data/sig/mixergy.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Mixergy
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mixergy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicholas J. Humfrey
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pastel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: This gem provides a simple interface for interacting with the Mixergy
56
+ API, allowing users to manage their hot water tanks programmatically.
57
+ email:
58
+ - njh@aelius.com
59
+ executables:
60
+ - mixergy
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - doc/screenshot.png
68
+ - exe/mixergy
69
+ - lib/mixergy.rb
70
+ - lib/mixergy/ascii_art.rb
71
+ - lib/mixergy/client.rb
72
+ - lib/mixergy/config.rb
73
+ - lib/mixergy/error.rb
74
+ - lib/mixergy/status.rb
75
+ - lib/mixergy/tank.rb
76
+ - lib/mixergy/version.rb
77
+ - sig/mixergy.rbs
78
+ homepage: https://github.com/njh/ruby-mixergy
79
+ licenses:
80
+ - MIT
81
+ metadata:
82
+ allowed_push_host: https://rubygems.org
83
+ homepage_uri: https://github.com/njh/ruby-mixergy
84
+ source_code_uri: https://github.com/njh/ruby-mixergy.git
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: 3.2.0
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubygems_version: 3.5.3
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Ruby gem for controlling Mixergy hot water tanks
104
+ test_files: []