atrea_control 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdf15229042c9284e6e7c385a94336e769d2c0118b7b9e723ed673faacfbac26
4
- data.tar.gz: 8926e926db7cfea4b5f48473bfb9a13c57584adfea15d19afbd70b70fa3b43c0
3
+ metadata.gz: 01f3c707897b8cd1626855caaa5cd4758ebeae0497881dd5164c2fb67a610327
4
+ data.tar.gz: f567bde666512960777679842366f249b0eff130e5a3fd1c487d5d652ef11687
5
5
  SHA512:
6
- metadata.gz: b774b17e3be8f214eb756839fa464f9f46bba7a2363cd1a1508c47796e6884ce16dc8172f8ceeb73b67d97bd4d9eec7d2736bcec843ce4242b73e4d457d8f3de
7
- data.tar.gz: 2af518d17ad3757473bc6b0611d3b17eeeb47d6039567dd597df19979059886d8a9225915e08c4538f72eeaf7e1065efbb2714077f4b4cae7632c7fe90511919
6
+ metadata.gz: 6f4a7ef12bbfde071d91b350e6b100d4528c3275a2642680cf26117ace31ef9ca9ed3072aaad9cb38189f336ec563d4b764db13238248295fc84ecbfe6977f65
7
+ data.tar.gz: d933b73a8afe23d567aba8f56d1d58060f53699fc5fadaa2eb897e9ff1a6f2e9aa1ed314b2f805a4923d08038c8db8924b7dfc2e5e155c6219a376e2fe05f84e
@@ -10,7 +10,7 @@ jobs:
10
10
  - name: Set up Ruby
11
11
  uses: ruby/setup-ruby@v1
12
12
  with:
13
- ruby-version: 2.7.2
13
+ ruby-version: 2.7
14
14
  bundler-cache: true
15
15
  - uses: browser-actions/setup-firefox@latest
16
16
  - name: Run tests
data/CHANGELOG.md CHANGED
@@ -1,4 +1,15 @@
1
1
  ## [Unreleased]
2
+ ### Changed
3
+ - refactored codebase to more readable classes
4
+ - selenium used only for login and then close => obtain SID (and user with unit)
5
+ - login waiting mechanism
6
+ ### Added
7
+ - found way how to change mode & power (tell unit to change)
8
+
9
+ ## [1.4.1] - 2021-12-18
10
+ ### Changed
11
+ - little refactor
12
+
2
13
  ## [1.4.0] - 2021-12-12
3
14
  ### Changed
4
15
  - founded way to get config and data from Atrea server
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atrea_control (1.4.0)
4
+ atrea_control (2.0.0)
5
5
  i18n (~> 1.8)
6
6
  nokogiri (~> 1.12)
7
7
  rest-client (~> 2.1)
@@ -18,7 +18,7 @@ GEM
18
18
  concurrent-ruby (1.1.9)
19
19
  crack (0.4.5)
20
20
  rexml
21
- diff-lcs (1.4.4)
21
+ diff-lcs (1.5.0)
22
22
  docile (1.4.0)
23
23
  domain_name (0.5.20190701)
24
24
  unf (>= 0.0.5, < 1.0.0)
@@ -35,6 +35,8 @@ GEM
35
35
  netrc (0.11.0)
36
36
  nokogiri (1.12.5-x86_64-darwin)
37
37
  racc (~> 1.4)
38
+ nokogiri (1.12.5-x86_64-linux)
39
+ racc (~> 1.4)
38
40
  parallel (1.21.0)
39
41
  parser (3.0.3.2)
40
42
  ast (~> 2.4.1)
@@ -65,16 +67,16 @@ GEM
65
67
  diff-lcs (>= 1.2.0, < 2.0)
66
68
  rspec-support (~> 3.10.0)
67
69
  rspec-support (3.10.3)
68
- rubocop (1.23.0)
70
+ rubocop (1.24.0)
69
71
  parallel (~> 1.10)
70
72
  parser (>= 3.0.0.0)
71
73
  rainbow (>= 2.2.2, < 4.0)
72
74
  regexp_parser (>= 1.8, < 3.0)
73
75
  rexml
74
- rubocop-ast (>= 1.12.0, < 2.0)
76
+ rubocop-ast (>= 1.15.0, < 2.0)
75
77
  ruby-progressbar (~> 1.7)
76
78
  unicode-display_width (>= 1.4.0, < 3.0)
77
- rubocop-ast (1.14.0)
79
+ rubocop-ast (1.15.0)
78
80
  parser (>= 3.0.1.1)
79
81
  rubocop-rspec (2.6.0)
80
82
  rubocop (~> 1.19)
data/README.md CHANGED
@@ -10,7 +10,9 @@ This gem provide simple DSL by parsing content of https://control.atrea.eu with
10
10
  * temperature
11
11
  * fan power
12
12
  * power mode
13
-
13
+ * allow change
14
+ * power
15
+ * mode
14
16
 
15
17
  ## Installation
16
18
 
@@ -30,27 +32,59 @@ Or install it yourself as:
30
32
 
31
33
  ## Usage
32
34
 
33
- UI map of sensors is required = each box have own ID contains sensor ID. Its required for correct element lookup.
34
-
35
- Default sensor map:
35
+ At the begin you need obtain `user_id`, `unit_id` and `sid` (auth token). For this use "Login"
36
36
  ```ruby
37
- def default_sensors_map
38
- {
39
- outdoor_temperature: "I10208",
40
- current_power: "H10704",
41
- current_mode: "H10705",
42
- }
43
- end
37
+ tokens = AtreaControl::Duplex::Login.user_tokens login: "myhome", password: "sup3r-S3CR3T-kocicka"
38
+ tokens # => { user_id: "1234", unit_id: "85425324672", sid: 4012 }
44
39
  ```
40
+ I recommend to store then somewhere...
41
+ Then you can call Unit for data...
42
+
45
43
  Example usage:
46
44
  ```ruby
47
- control = AtreaControl::Duplex.new login: "myhome", password: "sup3r-S3CR3T-kocicka", sensors_map: { current_power: "H10704" }
48
- control.login # => true (takes max 5.minutes)
49
- control.current_mode # => "Bathroom"
50
- control.current_power # => 37.0
45
+ control = AtreaControl::Duplex::Unit.new user_id: "1234", unit_id: "85425324672", sid: 4012
46
+ control.values # => { current_power: 88.0, current_mode: "CO2" }
47
+ control.power # => 88.0
48
+ ```
49
+ ### Dig deeper
50
+ `AtreaControl::Duplex::Unit` expect optional argument `user_ctrl` which should be object respond to
51
+
52
+ `name` (String) = Name of unit
53
+ `sensors` (Hash) = Map of sensors, for example: `{ outdoor_temperature: "HI10208", current_power: "H10704" }`
54
+ `modes` (Hash) = Is a map of "changable" modes - in unit its something like "builtin?" modes. They are translated by unit lang - `{ "0" => "Vypnuto", "1" => "Automat" }`
55
+ `user_modes` (Hash) = Is a map user specific modes, based on home switches / devices (D1, D2, D3, IN1, IN2 ...). They are translated by user texts - `{ "D1" => "Koupelna", "D2" => "CO2", "IN1" => "ovladač" }`
56
+
57
+ __Please check [lib/atrea_control/duplex/user_ctrl.rb](./lib/atrea_control/duplex/user_ctrl.rb) for more details !__
58
+
59
+ ## Development / TODO
60
+ Login is currently done by selenium - fill login form.
61
+ I found that Atre submit form to BE, generate some "empty" HTML and JS which onLoad start doing request to queue for "login".
62
+
63
+ Re-login user, add login procedure into queue:
64
+ ```bash
65
+ curl -X POST -d "comm=config%2Flogin.cgi" "https://control.atrea.eu/apps/rd5Control/handle.php?action=unitLogin&user=XXXX&unit=NNNNNNN&table=userUnits&idPwd=YYYYYYY&NFP"
66
+ ```
67
+ Response is time in seconds when login will ready:
68
+ ```xml
69
+ <root><sended time="264"/></root>
70
+ ```
71
+ Based it su shown countdown ...
72
+
73
+
74
+ Request for current queue status
75
+ ```bash
76
+ curl 'https://control.atrea.eu/apps/rd5Control/handle.php?Sync=1&action=unitQuery&query=loged&user=XXXX&unit=NNNNNNN'
77
+ ```
78
+ if queue is processed:
79
+ ```xml
80
+ <root><login uconn="16395889" sid="010101" ver="3001009"/></root>
81
+ ```
82
+ else
83
+ ```xml
84
+ <root><login uconn="16390480" sid="0"/></root>
51
85
  ```
52
86
 
53
- ## Development
87
+ Goal is to obtain "SID".
54
88
 
55
89
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
56
90
 
@@ -0,0 +1,107 @@
1
+ require "selenium-webdriver"
2
+
3
+ module AtreaControl
4
+ module Duplex
5
+ # Process login into RD5 with selenium to get `sid` ( auth_token ) for direct API communication
6
+ class Login
7
+
8
+ include AtreaControl::Logger
9
+
10
+ # @return [Hash] - user_id, unit_id, sid
11
+ def self.user_tokens(login:, password:)
12
+ i = new(login: login, password: password)
13
+ tokens = i.user
14
+
15
+ tokens
16
+ ensure
17
+ i.close
18
+ end
19
+
20
+ # @param [String] login
21
+ # @param [String] password
22
+ def initialize(login:, password:)
23
+ @login = login
24
+ @password = password
25
+ end
26
+
27
+ def user
28
+ raise AtreaControl::Error, "Must be logged in" unless login
29
+
30
+ logger.debug "refresh user data based on session"
31
+ @user_id = driver.execute_script("return window._user")
32
+ @unit_id = driver.execute_script("return window._unit")
33
+ @auth_token = driver.execute_script("return window.user")&.[]("auth") # sid
34
+
35
+ { user_id: @user_id, unit_id: @unit_id, sid: @auth_token }
36
+ end
37
+
38
+ # @return [Selenium::WebDriver::Firefox::Driver]
39
+ def driver
40
+ return @driver if defined?(@driver)
41
+
42
+ options = Selenium::WebDriver::Firefox::Options.new
43
+ options.headless! unless ENV["NO_HEADLESS"]
44
+ @driver ||= Selenium::WebDriver.for :firefox, capabilities: [options]
45
+ end
46
+
47
+ # Login into control
48
+ def login
49
+ return driver if @logged
50
+
51
+ @login_in_progress = true
52
+ logger.debug "start new login..."
53
+ driver.get "#{AtreaControl::Duplex::CONTROL_URI}?action=logout"
54
+ submit_login_form
55
+ finish_login
56
+ driver
57
+ ensure
58
+ @login_in_progress = false
59
+ end
60
+
61
+ # Submit given credentials and proceed login
62
+ def submit_login_form
63
+ form = driver.find_element(id: "loginFrm")
64
+ username = form.find_element(name: "username")
65
+ username.send_keys @login
66
+ password = form.find_element(name: "password")
67
+ password.send_keys @password
68
+ logger.debug "Submit login form..."
69
+
70
+ submit = form.find_element(css: "input[type=submit]")
71
+ submit.click
72
+ end
73
+
74
+ # Retrieve dashboard URI from object tag and open it again
75
+ def open_dashboard
76
+ uri = driver.find_element(tag_name: "object").attribute "data"
77
+ # Open "iframe" with atrea dashboard - it propagate window objects...
78
+ driver.get uri
79
+ logger.debug "login success"
80
+ @logged = true
81
+ end
82
+
83
+ # quit selenium browser
84
+ def close
85
+ driver.quit rescue nil
86
+ logger.debug "driver closed & destroyed"
87
+ ensure
88
+ remove_instance_variable :@driver
89
+ end
90
+
91
+ private
92
+
93
+ def finish_login
94
+ 30.times do |i|
95
+ return true if open_dashboard
96
+ rescue Selenium::WebDriver::Error::NoSuchElementError => e
97
+ logger.debug e.message
98
+ logger.debug "#{i + 1}/30 attempt for login..."
99
+ sleep 10
100
+ end
101
+ File.write("/tmp/failed_login-#{@login}.html", driver.page_source)
102
+ raise AtreaControl::Error, "unable to login"
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,29 @@
1
+ require "rest-client"
2
+
3
+ module AtreaControl
4
+ module Duplex
5
+ # Process request with duplex unit itself. Handle response
6
+ class Request
7
+
8
+ # @param [String, Integer] user_id
9
+ # @param [String, Integer] unit_id
10
+ # @param [String, Integer] sid
11
+ # @note `ver` is done by atrea server
12
+ def initialize(user_id:, unit_id:, sid:)
13
+ @params = {
14
+ _user: user_id.to_i,
15
+ _unit: unit_id,
16
+ auth: sid,
17
+ ver: AtreaControl::Duplex::CONTROL_VERSION,
18
+ }
19
+ end
20
+
21
+ # @param [Hash] params
22
+ # @option params [String] :_t ("config/xml.xml") file name
23
+ def call(params)
24
+ RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/comm/sw/unit.php", params: @params.merge(params)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AtreaControl
4
+ module Duplex
5
+ # Communication with RD5 unit (read/write)
6
+ class Unit
7
+ include AtreaControl::Logger
8
+
9
+ attr_reader :current_mode, :current_power, :outdoor_temperature
10
+ # @return [DateTime] store time of last update
11
+ attr_reader :valid_for
12
+ # @return [UserCtrl]
13
+ attr_reader :user_ctrl
14
+
15
+ # @param [String, Integer] user_id
16
+ # @param [String, Integer] unit_id
17
+ # @param [String, Integer] sid
18
+ # @param [AtreaControl::Duplex::UserCtrl] user_ctrl
19
+ def initialize(user_id:, unit_id:, sid:, user_ctrl: nil)
20
+ @user_id = user_id
21
+ @unit_id = unit_id
22
+ @sid = sid
23
+ @user_ctrl = user_ctrl || UserCtrl.new(user_id: user_id, unit_id: unit_id, sid: sid)
24
+ end
25
+
26
+ def name
27
+ @user_ctrl.name
28
+ end
29
+
30
+ def mode
31
+ current_mode || values[:current_mode]
32
+ end
33
+
34
+ def power
35
+ current_power || values[:current_power]
36
+ end
37
+
38
+ # @param [String] value 0 - power-off; 1 - automat
39
+ def mode=(value)
40
+ v = parser.input(@user_ctrl.sensors["mode_input"], value.to_s)
41
+ write(v)
42
+ end
43
+
44
+ def power=(value)
45
+ v = [parser.input(@user_ctrl.sensors["power_input"], value.to_s)]
46
+ v << parser.input(@user_ctrl.sensors["mode_switch"], "2")
47
+ write(v)
48
+ end
49
+
50
+ def values
51
+ parser.values(read.body).each do |name, value|
52
+ instance_variable_set :"@#{name}", value
53
+ end
54
+ as_json
55
+ end
56
+
57
+ def as_json(_options = nil)
58
+ {
59
+ current_mode: current_mode,
60
+ current_power: current_power,
61
+ outdoor_temperature: outdoor_temperature,
62
+ valid_for: valid_for,
63
+ }
64
+ end
65
+
66
+ def to_json(*args)
67
+ values.to_json(*args)
68
+ end
69
+
70
+ private
71
+
72
+ def parser
73
+ @parser ||= ::AtreaControl::SensorParser.new(@user_ctrl)
74
+ end
75
+
76
+ # Request to RD5
77
+ def request
78
+ @request ||= Request.new(user_id: @user_id, unit_id: @unit_id, sid: @sid)
79
+ end
80
+
81
+ def read
82
+ request.call(_t: "config/xml.xml")
83
+ end
84
+
85
+ # @param [Array<String>] values in format SENSOR0000VALUE
86
+ def write(*values)
87
+ inputs = values.to_h { |i| [i, nil] }
88
+ logger.debug("set RD5 #{inputs.keys}")
89
+ request.call({ _t: "config/xml.cgi", _w: 1 }.merge(inputs))
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,138 @@
1
+ require "nokogiri"
2
+
3
+ module AtreaControl
4
+ module Duplex
5
+ # Parse `userCtrl.xml`
6
+ class UserCtrl
7
+ # @param (see #initialize)
8
+ def self.data(user_id:, unit_id:, sid:)
9
+ user_ctrl = new(user_id: user_id, unit_id: unit_id, sid: sid)
10
+ {
11
+ name: user_ctrl.name,
12
+ sensors: user_ctrl.sensors,
13
+ modes: user_ctrl.modes,
14
+ user_modes: user_ctrl.user_modes,
15
+ }
16
+ end
17
+
18
+ # @param [String, Integer] user_id
19
+ # @param [String, Integer] unit_id
20
+ # @param [String, Integer] sid
21
+ def initialize(user_id:, unit_id:, sid:)
22
+ @user_id = user_id
23
+ @unit_id = unit_id
24
+ @sid = sid
25
+ end
26
+
27
+ # Request to RD5
28
+ def request
29
+ @request ||= Request.new(user_id: @user_id, unit_id: @unit_id, sid: @sid)
30
+ end
31
+
32
+ # Get and parse XML with user/unit configuration source
33
+ def user_ctrl
34
+ response = request.call(_t: "lang/userCtrl.xml")
35
+ Nokogiri::XML response.body
36
+ end
37
+
38
+ # User-defined name of home (RD5 unit)
39
+ # @return [String]
40
+ def name
41
+ @name ||= user_texts["UnitName"]
42
+ end
43
+
44
+ # Gather ID of sensors based on userCtrl config file
45
+ def sensors
46
+ return @sensors if @sensors
47
+
48
+ power = user_ctrl.xpath("//body/content/i[@title='$currentPower']")
49
+ mode = user_ctrl.xpath("//body/content/i[@title='$currentMode']")
50
+ outdoor_temp = user_ctrl.xpath("//body/content/i[@title='$outdoorTemp']")
51
+
52
+ switch_mode = mode.xpath("displayval").text[/values\.(\w+)/, 1]
53
+
54
+ power_input = power.xpath("onchange").text[/getUrlPar\('(\w+)',val\)/, 1]
55
+ m = mode.xpath("onchange").text.match(/getUrlPar\('(\w+)',val\).*getUrlPar\(.(\w+).,2\)/)
56
+
57
+ @sensors = {
58
+ "outdoor_temperature" => outdoor_temp.attribute("id").value, # "I10208"
59
+ "current_power" => power.attribute("id").value, # "H10704"
60
+ "current_mode" => mode.attribute("id").value, # "H10705"
61
+ "current_mode_switch" => switch_mode, # "H10712"
62
+ "power_input" => power_input, # "H10708"
63
+ "mode_input" => m[1], # "H10709"
64
+ "mode_switch" => m[2], # "H10701"
65
+ }
66
+ end
67
+
68
+ # Generic modes
69
+ # @return [Hash]
70
+ def modes
71
+ return @modes if @modes
72
+
73
+ @modes = {}
74
+ user_ctrl.xpath("//op[@id='Mode']/i").each do |mode|
75
+ m = translate_mode(mode)
76
+ @modes[m[:id]] = m[:value]
77
+ end
78
+ @modes
79
+ end
80
+
81
+ # Modes by inputs - switches - named by user custom texts
82
+ # @return [Hash]
83
+ def user_modes
84
+ return @user_modes if @user_modes
85
+
86
+ @user_modes = {}
87
+ user_ctrl.xpath("//op[@id='ModeText']/i").each do |mode|
88
+ m = translate_mode(mode)
89
+ @user_modes[m[:id]] = m[:value]
90
+ end
91
+ @user_modes
92
+ end
93
+
94
+ private
95
+
96
+ # User defined texts in RD5 unit
97
+ # @return [Hash]
98
+ def user_texts
99
+ return @user_texts if @user_texts
100
+
101
+ response = request.call(_t: "config/texts.xml")
102
+ xml = Nokogiri::XML response.body
103
+ @user_texts = xml.xpath("//i").to_h do |node|
104
+ value = node.attributes["value"].value
105
+ id = node.attributes["id"].value
106
+ [id, value.gsub(/%u([\dA-Z]{4})/) { |i| +"" << i[Regexp.last_match(1)].to_i(16) }]
107
+ end
108
+ end
109
+
110
+ # @param [Nokogiri::XML::Element] mode
111
+ # @return [Hash{Symbol->String}]
112
+ def translate_mode(mode)
113
+ id = mode.attributes["id"].value
114
+ title = mode.attributes["title"].value
115
+ title = if title.start_with?("$")
116
+ I18n.t(title[/\w+/])
117
+ else
118
+ user_texts[title]
119
+ end
120
+ { id: id, value: title }
121
+ end
122
+
123
+ # Re-generate copy of locale files
124
+ # @note internal use only
125
+ def update_locales_files!
126
+ { cs: "0", de: "1", en: "2" }.each do |name, atrea_id|
127
+ response = request.call(_t: "lang/texts_#{atrea_id}.xml")
128
+
129
+ xml = Nokogiri::XML response.body
130
+ # `eval` JS object (= hash)
131
+ locale = eval xml.xpath("//words/text()")[0].text
132
+ yaml = { name.to_s => JSON.parse(locale.to_json) }.to_yaml
133
+ File.write(File.expand_path("../../../config/locales/#{name}.yml", __dir__), yaml)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,278 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "logger"
4
- require "nokogiri"
5
- require "rest-client"
6
- require "selenium-webdriver"
7
-
8
3
  module AtreaControl
9
4
  # Controller for +control.atrea.eu+
10
- class Duplex
5
+ module Duplex
11
6
  CONTROL_URI = "https://control.atrea.eu/"
7
+ CONTROL_VERSION = "003001009"
12
8
 
13
- attr_reader :current_mode, :current_power, :outdoor_temperature
14
- # @return [DateTime] store time of last update
15
- attr_reader :valid_for
16
-
17
- attr_accessor :user_id, :unit_id, :auth_token
18
- attr_writer :user_texts, :user_modes, :modes
19
-
20
- # @param [String] login
21
- # @param [String] password
22
- # @param [Hash] sensors_map which box is related to sensor ID
23
- # @option sensors_map [String] :outdoor_temperature
24
- # @option sensors_map [String] :current_power
25
- # @option sensors_map [String] :current_mode
26
- def initialize(login:, password:, sensors_map: Config.default_sensors_map)
27
- @login = login
28
- @password = password
29
- @sensors = sensors_map
30
- end
31
-
32
- # @return [Selenium::WebDriver::Firefox::Driver]
33
- def driver
34
- return @driver if defined?(@driver)
35
-
36
- options = Selenium::WebDriver::Firefox::Options.new
37
- options.headless! unless ENV["NO_HEADLESS"]
38
- @driver ||= Selenium::WebDriver.for :firefox, capabilities: [options]
39
- end
40
-
41
- def logged?
42
- user&.[] "loged"
43
- end
44
-
45
- def login_in_progress?
46
- @login_in_progress
47
- end
48
-
49
- # Login into control
50
- def login
51
- @login_in_progress = true
52
- logger.debug "start new login"
53
- driver.get CONTROL_URI
54
- submit_login_form unless logged?
55
- finish_login
56
- refresh!
57
- inspect
58
- ensure
59
- @login_in_progress = false
60
- end
61
-
62
- # Submit given credentials and proceed login
63
- def submit_login_form
64
- form = driver.find_element(id: "loginFrm")
65
- username = form.find_element(name: "username")
66
- username.send_keys @login
67
- password = form.find_element(name: "password")
68
- password.send_keys @password
69
-
70
- submit = form.find_element(css: "input[type=submit]")
71
- submit.click
72
- end
73
-
74
- # Retrieve dashboard URI from object tag and open it again
75
- def open_dashboard
76
- uri = driver.find_element(tag_name: "object").attribute "data"
77
- driver.get uri
78
- logger.debug "#{name} login success"
79
- end
80
-
81
- # @return [String]
82
- def name
83
- @name ||= user_texts["UnitName"]
84
- end
85
-
86
- # Window.user object from atrea
87
- # @return [Hash, nil]
88
- def user
89
- driver.execute_script("return window.user")
90
- end
91
-
92
- # quit selenium browser
93
- def close
94
- driver.quit
95
- ensure
96
- remove_instance_variable :@driver
97
- end
98
-
99
- alias logout! close
100
-
101
- def as_json(_options = nil)
102
- {
103
- current_mode: current_mode,
104
- current_power: current_power,
105
- outdoor_temperature: outdoor_temperature,
106
- valid_for: valid_for,
107
- }
108
- end
109
-
110
- alias values as_json
111
-
112
- def to_json(*args)
113
- as_json.to_json(*args)
114
- end
115
-
116
- def call_unit!
117
- return false if @login_in_progress
118
-
119
- logger.debug "call_unit!"
120
- parse_values(response_comm_unit)
121
- @valid_for = Time.now
122
- as_json
123
- end
124
-
125
- def modes
126
- return @modes if @modes
127
-
128
- @modes = {}
129
- user_ctrl.xpath("//op[@id='Mode']/i").each do |mode|
130
- m = translate_mode(mode)
131
- @modes[m[:id]] = m[:value]
132
- end
133
- @modes
134
- end
135
-
136
- def user_modes
137
- return @user_modes if @user_modes
138
-
139
- @user_modes = {}
140
- user_ctrl.xpath("//op[@id='ModeText']/i").each do |mode|
141
- m = translate_mode(mode)
142
- @user_modes[m[:id]] = m[:value]
143
- end
144
- @user_modes
145
- end
146
-
147
- # User defined texts in RD5 unit
148
- # @return [Hash]
149
- def user_texts
150
- return @user_texts if @user_texts
151
-
152
- response = rd5_request(params_comm_unit.merge(_t: "config/texts.xml"))
153
- xml = Nokogiri::XML response.body
154
- @user_texts = xml.xpath("//i").map do |node|
155
- value = node.attributes["value"].value
156
- id = node.attributes["id"].value
157
- [id, value.gsub(/%u([\dA-Z]{4})/) { |i| +'' << i[$1].to_i(16) }]
158
- end.to_h
159
- end
160
-
161
- private
162
-
163
- # @see scripts.php -> loadRD5Values(node, init)
164
- # @note
165
- # if(values[key]>32767) values[key]-=65536;
166
- # if(params[key] && params[key].offset)
167
- # values[key]=values[key]-params[key].offset;
168
- # if(params[key] && params[key].coef)
169
- # values[key]=values[key]/params[key].coef;
170
- def parse_values(response)
171
- xml = Nokogiri::XML response.body
172
- sensors_values = @sensors.transform_values do |id|
173
- value = xml.xpath("//O[@I=\"#{id}\"]/@V").last&.value.to_i
174
- value -= 65_536 if value > 32_767
175
- # value -= 0 if "offset"
176
- # value -= 0 if "coef"
177
- value
178
- end
179
- refresh_data(sensors_values)
180
- end
181
-
182
- # @param [Hash] values
183
- # @return [Hash]
184
- def refresh_data(values)
185
- @outdoor_temperature = values[:outdoor_temperature].to_f / 10.0
186
- @current_power = values[:current_power].to_f
187
- @current_mode = if values[:current_mode_switch].positive?
188
- user_modes[values[:current_mode_switch].to_s]
189
- else
190
- modes[values[:current_mode].to_s]
191
- end
192
- # @current_mode = mode_map[values[:current_mode_name] > 0 ? values[:current_mode_name] : values[:current_mode]]
193
-
194
- as_json
195
- end
196
-
197
- def logger
198
- @logger ||= ::Logger.new($stdout)
199
- end
200
-
201
- def finish_login
202
- 13.times do |i|
203
- return true if open_dashboard
204
- rescue Selenium::WebDriver::Error::NoSuchElementError => _e
205
- t = [3 * (1 + i), 25].min
206
- logger.debug "waiting #{t}s for login..."
207
- sleep t
208
- end
209
- raise Error, "unable to login"
210
- end
211
-
212
- def sensor_element(sensor_id)
213
- driver.find_element id: "contentBox#{sensor_id}"
214
- end
215
-
216
- # @return [RestClient::Response]
217
- def response_comm_unit
218
- rd5_request(params_comm_unit.merge(_t: "config/xml.xml"))
219
- end
220
-
221
- def rd5_request(params)
222
- RestClient.get "https://control.atrea.eu/comm/sw/unit.php", { Cookie: "autoLogin=#{autologin_token}", params: params }
223
- end
224
-
225
- def user_ctrl
226
- response = rd5_request(params_comm_unit.merge(_t: "lang/userCtrl.xml"))
227
- xml = Nokogiri::XML response.body
228
- end
229
-
230
- def autologin_token
231
- @autologin_token ||= CGI.escape([@login, @password].join("\b"))
232
- end
233
-
234
- # @note `ver` is done by atrea server
235
- def params_comm_unit
236
- {
237
- _user: user_id.to_i,
238
- _unit: unit_id,
239
- auth: auth_token || "null",
240
- ver: "003001009",
241
- }
242
- end
243
-
244
- # Update tokens based on current state
245
- def refresh!
246
- @user_id = driver.execute_script("return window._user")
247
- @unit_id = driver.execute_script("return window._unit")
248
- @auth_token = user&.[]("auth")
249
- end
250
-
251
- # @param [Nokogiri::XML::Element] mode
252
- # @return [Hash{Symbol->String}]
253
- def translate_mode(mode)
254
- id = mode.attributes["id"].value
255
- title = mode.attributes["title"].value
256
- title = if title.start_with?("$")
257
- I18n.t(title[/\w+/])
258
- else
259
- user_texts[title]
260
- end
261
- { id: id, value: title }
262
- end
263
-
264
- # Re-generate copy of locale files
265
- # @note internal use only
266
- def update_locales_files!
267
- { cs: "0", de: "1", en: "2" }.each do |name, atrea_id|
268
- response = rd5_request(params_comm_unit.merge(_t: "lang/texts_#{atrea_id}.xml", auth: nil))
269
-
270
- xml = Nokogiri::XML response.body
271
- locale = eval xml.xpath("//words/text()")[0].text
272
- yaml = { name.to_s => JSON.parse(locale.to_json) }.to_yaml
273
- File.write(File.expand_path("../../config/locales/#{name}.yml", __dir__), yaml)
274
- end
275
- end
9
+ autoload :Login, "atrea_control/duplex/login"
10
+ autoload :Request, "atrea_control/duplex/request"
11
+ autoload :Unit, "atrea_control/duplex/unit"
12
+ autoload :UserCtrl, "atrea_control/duplex/user_ctrl"
276
13
 
277
14
  end
278
15
  end
@@ -0,0 +1,11 @@
1
+ require "logger"
2
+
3
+ module AtreaControl
4
+ module Logger
5
+
6
+ def logger
7
+ @logger ||= ::Logger.new($stdout)
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module AtreaControl
6
+ # Call RD5 unit ang get current sensors values
7
+ # parse it and return Hash
8
+ class SensorParser
9
+ # @param [AtreaControl::Duplex::UserCtrl] user_ctrl
10
+ def initialize(user_ctrl)
11
+ @user_ctrl = user_ctrl
12
+ end
13
+
14
+ def values(xml)
15
+ format_data(parse(xml))
16
+ end
17
+
18
+ def input(sensor, value)
19
+ sensor + value.to_s.rjust(5, "0")
20
+ end
21
+
22
+ # @see scripts.php -> loadRD5Values(node, init)
23
+ # @note
24
+ # if(values[key]>32767) values[key]-=65536;
25
+ # if(params[key] && params[key].offset)
26
+ # values[key]=values[key]-params[key].offset;
27
+ # if(params[key] && params[key].coef)
28
+ # values[key]=values[key]/params[key].coef;
29
+ def parse(xml)
30
+ xml = Nokogiri::XML xml
31
+ @user_ctrl.sensors.transform_values do |id|
32
+ value = xml.xpath("//O[@I=\"#{id}\"]/@V").last&.value.to_i
33
+ value -= 65_536 if value > 32_767
34
+ # value -= 0 if "offset"
35
+ # value -= 0 if "coef"
36
+ value
37
+ end
38
+ end
39
+
40
+ # @param [Hash] values
41
+ # @return [Hash]
42
+ def format_data(values)
43
+ {
44
+ "current_mode" => parse_current_mode(values),
45
+ "current_power" => values["current_power"].to_f,
46
+ "outdoor_temperature" => values["outdoor_temperature"].to_f / 10.0,
47
+ "valid_for" => Time.now,
48
+ }
49
+ end
50
+
51
+ private
52
+
53
+ # `current_mode_switch` = mode trigger by wall switch or something similar
54
+ # `current_mode` = is common "builtin" mode
55
+ def parse_current_mode(values)
56
+ if values['current_mode_switch'].positive?
57
+ @user_ctrl.user_modes[values['current_mode_switch'].to_s]
58
+ else
59
+ @user_ctrl.modes[values['current_mode'].to_s]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AtreaControl
4
4
 
5
- VERSION = "1.4.0"
5
+ VERSION = "2.0.0"
6
6
 
7
7
  end
data/lib/atrea_control.rb CHANGED
@@ -8,21 +8,10 @@ module AtreaControl
8
8
  class Error < StandardError; end
9
9
 
10
10
  autoload :Duplex, "atrea_control/duplex"
11
+ autoload :Logger, "atrea_control/logger"
12
+ autoload :SensorParser, "atrea_control/sensor_parser"
11
13
 
12
14
  I18n.load_path.concat Dir["#{File.expand_path("../config/locales", __dir__)}/*.yml"]
13
15
  I18n.default_locale = :cs
14
16
 
15
- # Provide map sensor to element ID
16
- module Config
17
- module_function
18
-
19
- def default_sensors_map
20
- {
21
- outdoor_temperature: "I10208",
22
- current_power: "H10704",
23
- current_mode: "H10705",
24
- current_mode_switch: "H10712",
25
- }
26
- end
27
- end
28
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atrea_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukáš Pokorný
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-12 00:00:00.000000000 Z
11
+ date: 2021-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -92,6 +92,12 @@ files:
92
92
  - config/locales/en.yml
93
93
  - lib/atrea_control.rb
94
94
  - lib/atrea_control/duplex.rb
95
+ - lib/atrea_control/duplex/login.rb
96
+ - lib/atrea_control/duplex/request.rb
97
+ - lib/atrea_control/duplex/unit.rb
98
+ - lib/atrea_control/duplex/user_ctrl.rb
99
+ - lib/atrea_control/logger.rb
100
+ - lib/atrea_control/sensor_parser.rb
95
101
  - lib/atrea_control/version.rb
96
102
  homepage: https://github.com/luk4s/atrea_control
97
103
  licenses: []