ojelectronics 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|