ojelectronics 0.0.2
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 +7 -0
- data/exe/oj_electronics_mqtt_bridge +66 -0
- data/lib/oj_electronics/client.rb +113 -0
- data/lib/oj_electronics/thermostat.rb +29 -0
- data/lib/oj_electronics/version.rb +5 -0
- data/lib/oj_electronics.rb +7 -0
- data/lib/ojelectronics.rb +3 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4efc56c71825ce41000181e47bc73ba623038635d937247421189b39ba909de4
|
4
|
+
data.tar.gz: 9aaa6318abc98289c21a657f82edaea20a65c6e9c695d73bbe28c6ddeb923041
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: aee4126a865ff834204d5e861da08989cc95388464c33e078a7ce940553496d783ef8a844b88a8471c7e6f5c82959f78570fe48749ebb544c27e6f36abeef9d5
|
7
|
+
data.tar.gz: 724a221232dd8a1dcc4a1623f1247920c488901dcfedddb946fc9a7f0ff080e68ca4d795295ab1b3441c82d44fd22ca5052cc57e6bfee817459e641277c2ef8e
|
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "ojelectronics"
|
5
|
+
require "homie-mqtt"
|
6
|
+
require "optparse"
|
7
|
+
|
8
|
+
brand = nil
|
9
|
+
|
10
|
+
options = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: oj_electronics_mqtt_bridge USERNAME PASSWORD MQTT_URI [options]"
|
12
|
+
|
13
|
+
opts.on("--brand=BRAND", "Provide the brand. Either oj_electronics or schluter") do |v|
|
14
|
+
brand = v.to_sym
|
15
|
+
end
|
16
|
+
opts.on("-h", "--help", "Prints this help") do
|
17
|
+
puts opts
|
18
|
+
exit
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
options.parse!
|
23
|
+
|
24
|
+
unless ARGV.length == 3
|
25
|
+
puts options
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
client = OJElectronics::Client.new(ARGV[0], ARGV[1], brand: brand)
|
30
|
+
|
31
|
+
homie = MQTT::Homie::Device.new(MQTT::Homie.escape_id(brand&.to_s || "oj_electronics"),
|
32
|
+
"OJ Electronics Thermostats",
|
33
|
+
mqtt: ARGV[2])
|
34
|
+
|
35
|
+
client.thermostats.each_value do |t|
|
36
|
+
homie.node(t.serial_number, t.room, "Thermostat") do |n|
|
37
|
+
n.property("temperature", "Current Temperature", :float, t.temperature, unit: "°C")
|
38
|
+
n.property("set-point-temperature",
|
39
|
+
"Set Point Temperature",
|
40
|
+
:float,
|
41
|
+
t.set_point_temperature,
|
42
|
+
unit: "°C",
|
43
|
+
format: 5..40) do |v|
|
44
|
+
t.set_point_temperature = v
|
45
|
+
end
|
46
|
+
n.property("online", "Online", :boolean, t.online)
|
47
|
+
n.property("heating", "Heating", :boolean, t.heating)
|
48
|
+
n.property("regulation-mode", "Regulation Mode", :enum, t.regulation_mode,
|
49
|
+
format: OJElectronics::Thermostat::REGULATION_MODES)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
homie.publish
|
53
|
+
|
54
|
+
PROPERTIES = %i[temperature set_point_temperature online heating regulation_mode].freeze
|
55
|
+
|
56
|
+
loop do
|
57
|
+
t = client.long_poll
|
58
|
+
next unless t
|
59
|
+
|
60
|
+
homie.mqtt.batch_publish do
|
61
|
+
n = homie[t.serial_number]
|
62
|
+
PROPERTIES.each do |prop|
|
63
|
+
n[prop.to_s.tr("_", "-")].value = t.send(prop)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/core_ext/enumerable"
|
5
|
+
require "faraday_middleware"
|
6
|
+
|
7
|
+
module OJElectronics
|
8
|
+
class Client
|
9
|
+
class AuthenticationError < RuntimeError
|
10
|
+
class << self
|
11
|
+
def create(error_code)
|
12
|
+
klass, message = case error_code
|
13
|
+
when 1 then [InvalidUsernameError, "Invalid Username"]
|
14
|
+
when 2 then [IncorrectPasswordError, "Incorrect password"]
|
15
|
+
else
|
16
|
+
[self, nil]
|
17
|
+
end
|
18
|
+
klass.new(error_code, message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :error_code
|
23
|
+
|
24
|
+
def initialize(error_code, message = nil)
|
25
|
+
@error_code = error_code
|
26
|
+
super(message || "Unable to authenticate: ErrorCode #{error_code}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class IncorrectPasswordError < AuthenticationError; end
|
31
|
+
class InvalidUsernameError < AuthenticationError; end
|
32
|
+
|
33
|
+
BRANDS = {
|
34
|
+
oj_electronics: 0,
|
35
|
+
schluter: 8
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
attr_reader(*%i[username session_id brand expires thermostats])
|
39
|
+
|
40
|
+
def initialize(username, password, session_id: nil, expires: nil, brand: :oj_electronics)
|
41
|
+
raise ArgumentError, "unrecognized brand #{brand.inspect}" unless BRANDS.key?(brand)
|
42
|
+
|
43
|
+
@brand = brand
|
44
|
+
@username = username
|
45
|
+
@password = password
|
46
|
+
@session_id = session_id
|
47
|
+
@expires = expires
|
48
|
+
@api = Faraday.new(url: "https://mythermostat.info/api/") do |f|
|
49
|
+
f.request :json
|
50
|
+
f.request :retry
|
51
|
+
f.response :raise_error
|
52
|
+
f.response :json
|
53
|
+
f.adapter :net_http_persistent
|
54
|
+
end
|
55
|
+
@thermostats = {}
|
56
|
+
|
57
|
+
refresh
|
58
|
+
end
|
59
|
+
|
60
|
+
def expired?
|
61
|
+
@session_id.nil? || @expires.nil? || @expires < Time.now
|
62
|
+
end
|
63
|
+
|
64
|
+
# refresh all thermostats
|
65
|
+
def refresh
|
66
|
+
thermostats = api.get("thermostats", sessionid: session_id)
|
67
|
+
.body["Groups"]
|
68
|
+
.flat_map { |g| g["Thermostats"] }
|
69
|
+
.index_by { |t| t["SerialNumber"] }
|
70
|
+
missing = @thermostats.keys - thermostats.keys
|
71
|
+
missing.each { |sn| @thermostats.delete(sn) }
|
72
|
+
|
73
|
+
additional = thermostats.keys - @thermostats.keys
|
74
|
+
additional.each do |sn|
|
75
|
+
@thermostats[sn] = Thermostat.new(self, sn)
|
76
|
+
end
|
77
|
+
|
78
|
+
thermostats.each do |sn, json|
|
79
|
+
@thermostats[sn].refresh(json)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# returns nil if nothing changed; otherwise the thermostat that changed
|
84
|
+
def long_poll
|
85
|
+
response = api.get("notification", sessionid: session_id).body
|
86
|
+
json = response["Thermostat"]
|
87
|
+
return if json.nil?
|
88
|
+
|
89
|
+
@thermostats[json["SerialNumber"]].tap { |t| t.refresh(json) }
|
90
|
+
end
|
91
|
+
|
92
|
+
# !@visibility private
|
93
|
+
def api
|
94
|
+
reauth
|
95
|
+
@api
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def reauth
|
101
|
+
return unless expired?
|
102
|
+
|
103
|
+
auth = @api.post("authenticate/user",
|
104
|
+
Email: username,
|
105
|
+
Password: @password,
|
106
|
+
Application: BRANDS[brand]).body
|
107
|
+
raise AuthenticationError.create(auth["ErrorCode"]) unless auth["ErrorCode"] == 0 # rubocop:disable Style/NumericPredicate
|
108
|
+
|
109
|
+
@session_id = auth["SessionId"]
|
110
|
+
@expires = Time.now + 3600
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OJElectronics
|
4
|
+
class Thermostat
|
5
|
+
REGULATION_MODES = %i[schedule temporary_hold permanent_hold vacation_hold].freeze
|
6
|
+
attr_reader(*%i[serial_number temperature set_point_temperature room online heating regulation_mode])
|
7
|
+
|
8
|
+
def initialize(client, serial_number)
|
9
|
+
@client = client
|
10
|
+
@serial_number = serial_number
|
11
|
+
end
|
12
|
+
|
13
|
+
def refresh(json)
|
14
|
+
@temperature = json["Temperature"].to_f / 100
|
15
|
+
@set_point_temperature = json["SetPointTemp"].to_f / 100
|
16
|
+
@room = json["Room"]
|
17
|
+
@online = json["Online"]
|
18
|
+
@heating = json["Heating"]
|
19
|
+
@regulation_mode = REGULATION_MODES[json["RegulationMode"] - 1]
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_point_temperature=(value) # rubocop:disable Naming/AccessorMethodName
|
23
|
+
@client.api.post("thermostat?sessionid=#{@client.session_id}&serialnumber=#{serial_number}",
|
24
|
+
RegulationMode: 3,
|
25
|
+
VacationEnabled: false,
|
26
|
+
ManualTemperature: (value * 100).to_i)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ojelectronics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Cutrer
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday_middleware
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: homie-mqtt
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.6'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: net-http-persistent
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '9.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '9.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '13.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '13.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.23'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.23'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-performance
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.12'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.12'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rake
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.6'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.6'
|
139
|
+
description:
|
140
|
+
email: cody@cutrer.com'
|
141
|
+
executables:
|
142
|
+
- oj_electronics_mqtt_bridge
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- exe/oj_electronics_mqtt_bridge
|
147
|
+
- lib/oj_electronics.rb
|
148
|
+
- lib/oj_electronics/client.rb
|
149
|
+
- lib/oj_electronics/thermostat.rb
|
150
|
+
- lib/oj_electronics/version.rb
|
151
|
+
- lib/ojelectronics.rb
|
152
|
+
homepage: https://github.com/ccutrer/ruby-ojelectronics
|
153
|
+
licenses:
|
154
|
+
- MIT
|
155
|
+
metadata:
|
156
|
+
rubygems_mfa_required: 'true'
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '2.5'
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubygems_version: 3.3.5
|
173
|
+
signing_key:
|
174
|
+
specification_version: 4
|
175
|
+
summary: Interact with OJ Electronics/DITRA HEAT floor thermostats via MQTT
|
176
|
+
test_files: []
|