atrea_control 1.4.0 → 2.0.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 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: []