ets-to-homeassistant 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7bf61e1b960298191c6f9465cf0817b1343b5f8732a97db30b0abaacde94ccef
4
+ data.tar.gz: 8ef2e06491f0b08a9fd7511b677aa545af145c2a35ef7ad1e95d8691b93c25fc
5
+ SHA512:
6
+ metadata.gz: b97e9426c6f44520e9e709bb748d7ddbd092d249c8b8611a9d71cbe9fe80739683e736721ae8cab4a8bc918cae1b5f272bd4dcc2c0c379454a58f7da22d0353b
7
+ data.tar.gz: efb10210a466e020a8bc59172e0affa36efe19695adab19430545d83f832d6459e3cfe2c54dbb74cf02ac758dc947ed8ac3cf4beb5a14ce526ef33acc3925fc7
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,4 @@
1
+ iS�w�����J�B<��r8����g�>q��t�۾o0��|=����LƹG���\�D;�����+c4,�
2
+ �<��.F�E�t�,P�<��f���_�朳���Æ?6Lbp$;��p�Zg�V·�����@Ʊ�ꆵ��J��_|��'�q6^:�yo닌�w��,,i7�گN@�2�i��
3
+ I�0��c���_�.�
4
+ ���TaJ~��$��Ҳ��{��LQ\8*�D>A����$��VI���ݵ��;��8��8�dX
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changes (Release notes)
2
+
3
+ * 0.1
4
+
5
+ * New Features:
6
+ * initial version number
7
+ * option `--sort-by-name` to sort by object names in domains
8
+ * Issues Fixed:
9
+ * n/a
10
+ * Breaking Changes:
11
+ * Changed format for custom code and how data is passed back (`:custom` -> `:ha`, etc...)
12
+ * Custom function now access data through methods instead of hash
data/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # ETS project file to Home Assistant configuration
2
+
3
+ A Ruby tool to convert an ETS5 project file (`*.knxproj`) into:
4
+
5
+ * a YAML configuration file suitable for **Home Assistant** (requires to define building, functions and group data points in ETS)
6
+ * an XML file for `linknx` (the object list only)
7
+
8
+ [https://www.home-assistant.io/integrations/knx/](https://www.home-assistant.io/integrations/knx/)
9
+
10
+ ## Glossary
11
+
12
+ * **KNX Group Address**: a group address is a 1, 2 or 3-level address in the KNX system, e.g. `1/2/3`.
13
+ * **KNX Data Point Type**: a data point type is a type of data that can be sent on a group address, e.g. **1.001**, **5.001**.
14
+ * **ETS Building Information**: In ETS, a building is a container for rooms and **ETS Function**.
15
+ * **ETS Function** : In ETS, represents an object that has several **KNX Group Address** associated to it.
16
+ * **HA Device** : In Home Assistant, a device has a type (e.g.`light`) and has **HA Config Variable**.
17
+ * **HA Config Variable** : In Home Assistant, it's a property of a device, e.g.`name`,`address`,`state_address`.
18
+
19
+ ## Important note
20
+
21
+ In order for the tool to generate result properly read the following.
22
+
23
+ An actionable entity in KNX is a **KNX Group Address**: commands are exchanged in a group address on the KNX Bus.
24
+ For example a dimmable light has at least one group address for On/Off and another for the dimming value.
25
+
26
+ An actionable entity in **Home Assistant** is an **HA Device**.
27
+ For example, a dimmable light is a device and has properties, one of them is the group address for On/Off and another is the group address for the dimming value.
28
+
29
+ So, for this tool to work, the following pieces of information must be found:
30
+
31
+ * **KNX Group Address**
32
+ * The purpose for each **KNX Group Address** (typically, a **KNX Data Point Type**)
33
+ * Grouping of group addresses into devices : e.g. **ETS Function** in ETS mapped to **HA Device** in Home Assistant
34
+
35
+ By default, and in fact in a lot of ETS projects, only group addresses are defined as this is sufficient for an installation.
36
+ I.e. no **ETS Function** is defined, and **KNX Data Point Type**s are not defined for group addresses.
37
+ This is why this tool will not work out of the box most of the times: missing information.
38
+ The next 2 sections explain how to fix this.
39
+
40
+ ### Group address: Type
41
+
42
+ **KNX Group Address**es are defined in the ETS project file naturally, as it is the base for the system to work.
43
+ But we need the types of them (e.g. **on/off** versus **dim value**) in order to create the HA configuration.
44
+
45
+ The best, easiest and most reliable way for the tool to find the type of **KNX Group Address** is to specify the **KNX Data Point Type** in the **KNX Group Address** itself in ETS.
46
+ This requires editing the KNX project.
47
+ Refer to [Structure in ETS](#structure-in-ets).
48
+
49
+ Another possibility is to create a custom script.
50
+ Refer to [Custom method](#custom-method)
51
+
52
+ ### Group address: Grouping into devices
53
+
54
+ The second part is to regroup **KNX Group Address**es into **HA Device**.
55
+ This is best done by creating **ETS Function** in ETS.
56
+ Refer to [Structure in ETS](#structure-in-ets).
57
+
58
+ Another possibility is to create a custom script.
59
+ Refer to [Custom method](#custom-method)
60
+
61
+ ## Installation
62
+
63
+ 1. [Install Ruby for your platform](https://www.ruby-lang.org/fr/downloads/):
64
+
65
+ * Windows: [Ruby Installer](https://rubyinstaller.org/)
66
+ * macOS: builtin, or [RVM](https://rvm.io/), or [brew](https://brew.sh/), or [rbenv](https://github.com/rbenv/rbenv)
67
+ * Linux: builtin (yum, apt), or [RVM](https://rvm.io/), or [rbenv](https://github.com/rbenv/rbenv)
68
+
69
+ 2. Install this gem
70
+
71
+ ```bash
72
+ gem install ets-to-homeassistant
73
+ ```
74
+
75
+ 3. Test it works:
76
+
77
+ ```bash
78
+ ets_to_hass --help
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ General invocation syntax:
84
+
85
+ ```bash
86
+ Usage: .ets_to_hass [options] <ets project file>.knxproj
87
+
88
+ -h, --help
89
+ show help
90
+
91
+ --ha-knx
92
+ include level knx in output file
93
+
94
+ --sort-by-name
95
+ sort arrays by name
96
+
97
+ --full-name
98
+ add room name in object name
99
+
100
+ --format [format]
101
+ one of homeass|linknx
102
+
103
+ --fix [ruby file]
104
+ file with specific code to fix objects
105
+
106
+ --addr [addr]
107
+ one of Free, TwoLevel, ThreeLevel
108
+
109
+ --trace [trace]
110
+ one of debug, info, warn, error
111
+
112
+ --output [file]
113
+ add room name in object name```
114
+
115
+ For example to generate the home assistant KNX configuration from the exported ETS project: `myexport.knxproj`
116
+
117
+ ```bash
118
+ ets_to_hass --full-name --format homeass --output config_knx.yaml myexport.knxproj
119
+ ```
120
+
121
+ Option `--ha-knx` adds the dictionary key `knx` in the generated Home Assistant configuration so that it can be copy/paste in `configuration.yaml`.
122
+ Else, typically, include the generated entities in a separate file like this:
123
+
124
+ ```yaml
125
+ knx: !include config_knx.yaml
126
+ ```
127
+
128
+ If option `--fix` with a path to a non-existing file, then the tool will create a template file with the default code.
129
+ It will generate basic Objects/Functions for group addresses not part of a function.
130
+
131
+ Logs are sent to STDERR.
132
+
133
+ ## Internal logic
134
+
135
+ The tool takes the exported file from ETS with extension: `knxproj`.
136
+ The project file is a zip with several XML files in it.
137
+ Make sure that the project file is not password protected.
138
+
139
+ * The tool parses the first project file found.
140
+ * It extracts **ETS Building Information** and **KNX Group Address**es.
141
+ * A Ruby object of type: `EtsToHass` is created, with the following fields:
142
+
143
+ `@groups_addresses` contains all **KNX Group Address**es, `_gaid_` is the internal identifier of the **KNX Group Address** in ETS
144
+
145
+ `@objects` contains all **ETS Function**, `_obid_` is the internal identifier of the **ETS Function** in ETS
146
+
147
+ ```ruby
148
+ @groups_addresses = {
149
+ _gaid_ => {
150
+ name: "from ETS",
151
+ description: "from ETS",
152
+ address: group address as string. e.g. "x/y/z" depending on project style,
153
+ datapoint: datapoint type as string "x.abc", e.g. 1.001,
154
+ ha: {address_type: '...' } # set by specific code, HA parameter for address
155
+ },...
156
+ }
157
+ @objects = {
158
+ _obid_ => {
159
+ name: "from ETS, either function name or full name with room if option --full-name is used",
160
+ type: "ETS function type, see below",
161
+ floor: "from ETS",
162
+ room: "from ETS",
163
+ ha: {domain: '...', ha parameters... } # set by specific code, HA parameters
164
+ },...
165
+ }
166
+ @associations = [[_gaid_,_obid_],...]
167
+ ```
168
+
169
+ * a default mapping is proposed in methods:
170
+
171
+ * `map_ets_datapoint_to_ha_address_type` : default value for `@group_addresses[x][:ha][:address_type]`
172
+ * `map_ets_function_to_ha_object_category` : default value for `@objects[x][:ha][:domain]`
173
+
174
+ * the custom specific code is called giving an opportunity to modify this structure.
175
+
176
+ * Eventually, the HA configuration is generated
177
+
178
+ ## Structure in ETS
179
+
180
+ The **KNX Data Point Type** of **KNX Group Address** is used to find out the associated **HA Config Variable**.
181
+ In ETS, for each **KNX Group Address**, make sure to define the **KNX Data Point Type** as in the following screenshot:
182
+
183
+ ![datapoint in group address](images/ets5_datapoint.png)
184
+
185
+ **ETS Building Information** and **ETS Function** are used to generate HA devices.
186
+ If the ETS project has no **ETS Building Information**, then the tool will create one **HA Device** per **KNX Group Address**.
187
+
188
+ In the following screenshot, note that both **KNX Group Address** and **ETS Function** are created.
189
+
190
+ ![ETS 5 with building functions](images/ets5.png)
191
+
192
+ Moreover, if **ETS Function** are located properly in **ETS Building Information** levels and rooms, the tool will read this information.
193
+
194
+ When **ETS Function** are found, the tool will populate the `@objects` `Hash`.
195
+
196
+ The type of **ETS Function** is identified by a name (in ETS project file it is `FT-[n]`):
197
+
198
+ * `:custom`
199
+ * `:switchable_light`
200
+ * `:dimmable_light`
201
+ * `:sun_protection`
202
+ * `:heating_radiator`
203
+ * `:heating_floor`
204
+ * `:heating_switching_variable`
205
+ * `:heating_continuous_variable`
206
+
207
+ ## Custom method
208
+
209
+ If the **KNX Data Point Type** of a **KNX Group Address** is not defined in the ETS project, then the tool cannot guess which group address is e.g. for On/Off, or for dimming value.
210
+
211
+ If no **ETS Building Information** with **ETS Function** was created in the project, then the tool cannot guess which set of **KNX Group Address** refer to the same **HA Device**.
212
+
213
+ It is possible to add this information using option `--fix` (custom script) which can add missing information, based, for example, on the name of the **KNX Group Address**.
214
+
215
+ For example, I used to use a naming convention like: `<room>:<object>:<type>` before using ETS features, and the custom script could guess the missing data type from `<type>`, and then group addresses into devices based on `<room>` and `<object>`.
216
+
217
+ But if the convention is to place `ON/OFF` in `1/x/x` then you can use the first address identifier to guess the type of **KNX Group Address**.
218
+
219
+ The specific code can modify the analyzed structure:
220
+
221
+ * It can delete objects, or create objects.
222
+ * It can add fields in the `:ha` properties:
223
+
224
+ * in `@group_addresses`:
225
+ * `address_type` : define the use for the group address, e.g. `address`, `state_address`, etc...
226
+ * in `@objects`:
227
+ * `domain` : set the entity type in HA (`switch`, `light`, etc), this key is then removed from the `Hash`
228
+ * **Other fields** : initialize the HA object with these values, e.g. `name`
229
+
230
+ The function can use any information such as fields of the object, or description or name of group address for that.
231
+
232
+ Typically, the name of group addresses can be used if a specific naming convention was used.
233
+ Or, if group addresses were defined using a specific convention: for example in a/b/c a is the type of action, b is the identifier of device...
234
+
235
+ The specific code shall be like this:
236
+
237
+ ```ruby
238
+ def fix_objects(generator)
239
+ # use methods of generator to modify the structure
240
+ end
241
+ ```
242
+
243
+ ## Linknx
244
+
245
+ `linknx` does not have an object concept, and needs only group addresses.
246
+
247
+ ## XKNX
248
+
249
+ Support is dropped for the moment, until needed, but it is close enough to HA.
250
+
251
+ ## Reporting issues
252
+
253
+ Include the version of ETS used and logs.
254
+
255
+ ## TODO
256
+
257
+ One possibility would be to add extra information in the description of the group address and/or function in ETS, and then parse it in the tool.
258
+ For example, as YAML format.
data/bin/ets_to_hass ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # add in case we are in dev
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__dir__), 'lib'))
6
+
7
+ begin
8
+ require 'ets_to_hass/string_colors'
9
+ require 'ets_to_hass/generator'
10
+ require 'getoptlong'
11
+ rescue LoadError => e
12
+ puts(e.backtrace.join("\n"))
13
+ puts("Missing gems (#{e}): read the manual: execute:")
14
+ puts("gem install bundler\nbundle install".blink)
15
+ exit(1)
16
+ end
17
+
18
+ # prefix of generation methods
19
+ GENE_PREFIX = 'generate_'
20
+ # get list of generation methods
21
+ gene_formats = (EtsToHass::Generator.instance_methods - EtsToHass::Generator.superclass.instance_methods)
22
+ .select { |m| m.to_s.start_with?(GENE_PREFIX) }
23
+ .map { |m| m[GENE_PREFIX.length..-1] }
24
+
25
+ opts = GetoptLong.new(
26
+ ['--help', '-h', GetoptLong::NO_ARGUMENT],
27
+ ['--ha-knx', '-k', GetoptLong::NO_ARGUMENT],
28
+ ['--sort-by-name', '-r', GetoptLong::NO_ARGUMENT],
29
+ ['--full-name', '-n', GetoptLong::NO_ARGUMENT],
30
+ ['--format', '-f', GetoptLong::REQUIRED_ARGUMENT],
31
+ ['--fix', '-s', GetoptLong::REQUIRED_ARGUMENT],
32
+ ['--addr', '-a', GetoptLong::REQUIRED_ARGUMENT],
33
+ ['--trace', '-t', GetoptLong::REQUIRED_ARGUMENT],
34
+ ['--output', '-o', GetoptLong::REQUIRED_ARGUMENT]
35
+ )
36
+
37
+ options = {}
38
+ output_format = 'homeass'
39
+ opts.each do |opt, arg|
40
+ case opt
41
+ when '--help'
42
+ puts <<~END_OF_MANUAL
43
+ Usage: #{$PROGRAM_NAME} [options] <ets project file>.knxproj
44
+
45
+ -h, --help
46
+ show help
47
+
48
+ --ha-knx
49
+ include level knx in output file
50
+
51
+ --sort-by-name
52
+ sort arrays by name
53
+
54
+ --full-name
55
+ add room name in object name
56
+
57
+ --format [format]
58
+ one of #{gene_formats.join('|')}
59
+
60
+ --fix [ruby file]
61
+ file with specific code to fix objects
62
+
63
+ --addr [addr]
64
+ one of #{EtsToHass::Generator::GROUP_ADDRESS_PARSERS.keys.map(&:to_s).join(', ')}
65
+
66
+ --trace [trace]
67
+ one of debug, info, warn, error
68
+
69
+ --output [file]
70
+ add room name in object name
71
+ END_OF_MANUAL
72
+ Process.exit(1)
73
+ when '--fix'
74
+ options[:specific] = arg
75
+ when '--format'
76
+ output_format = arg
77
+ raise "Error: no such output format: #{output_format}" unless gene_formats.include?(output_format)
78
+ when '--ha-knx'
79
+ options[:ha_knx] = true
80
+ when '--sort-by-name'
81
+ options[:sort_by_name] = true
82
+ when '--full-name'
83
+ options[:full_name] = true
84
+ when '--trace'
85
+ options[:trace] = arg
86
+ when '--addr'
87
+ options[:addr] = arg
88
+ when '--output'
89
+ options[:output] = arg
90
+ else
91
+ raise "Unknown option #{opt}"
92
+ end
93
+ end
94
+
95
+ if ARGV.length != 1
96
+ puts 'Missing project file argument (try --help)'
97
+ Process.exit(1)
98
+ end
99
+
100
+ output_file =
101
+ if options[:output]
102
+ File.open(options[:output], 'w')
103
+ else
104
+ $stdout
105
+ end
106
+ project_file_path = ARGV.shift
107
+
108
+ # read and parse ETS file
109
+ generator = EtsToHass::Generator.new(project_file_path, options)
110
+ # generate result (e.g. call generate_homeass)
111
+ output_file.write(generator.send(:"#{GENE_PREFIX}#{output_format}"))
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+ require 'xmlsimple'
5
+ require 'yaml'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'fileutils'
9
+ require 'ets_to_hass/string_colors'
10
+ require 'ets_to_hass/info'
11
+
12
+ module EtsToHass
13
+ # Import ETS project file and generate configuration for Home Assistant and KNXWeb
14
+ class Generator
15
+ # extension of ETS project file
16
+ ETS_EXT = '.knxproj'
17
+ # converters of group address integer address into representation
18
+ GROUP_ADDRESS_PARSERS = {
19
+ Free: ->(a) { a.to_s },
20
+ TwoLevel: ->(a) { [(a >> 11) & 31, a & 2047].join('/') },
21
+ ThreeLevel: ->(a) { [(a >> 11) & 31, (a >> 8) & 7, a & 255].join('/') }
22
+ }.freeze
23
+ # KNX functions described in knx_master.xml in project file, in <FunctionTypes>
24
+ # map index parsed from "FT-x" to recognizable identifier
25
+ ETS_FUNCTIONS_INDEX_TO_NAME =
26
+ %i[custom switchable_light dimmable_light sun_protection heating_radiator heating_floor
27
+ dimmable_light sun_protection heating_switching_variable heating_continuous_variable].freeze
28
+ private_constant :ETS_EXT, :ETS_FUNCTIONS_INDEX_TO_NAME
29
+
30
+ # class methods
31
+ class << self
32
+ # helper function to dig through keys, knowing that we used ForceArray
33
+ def dig_xml(entry_point, path)
34
+ raise "ERROR: wrong entry point: #{entry_point.class}, expect Hash" unless entry_point.is_a?(Hash)
35
+
36
+ path.each do |n|
37
+ raise "ERROR: cannot find level #{n} in xml, have #{entry_point.keys.join(',')}" unless entry_point.key?(n)
38
+
39
+ entry_point = entry_point[n]
40
+ # because we use ForceArray
41
+ entry_point = entry_point.first
42
+ raise "ERROR: expect array with one element in #{n}" if entry_point.nil?
43
+ end
44
+ entry_point
45
+ end
46
+
47
+ def function_type_to_name(ft_type)
48
+ m = ft_type.match(/^FT-([0-9])$/)
49
+ raise "ERROR: Unknown function type: #{ft_type}" if m.nil?
50
+
51
+ ETS_FUNCTIONS_INDEX_TO_NAME[m[1].to_i]
52
+ end
53
+
54
+ def specific_defined?
55
+ defined?(fix_objects).eql?('method')
56
+ end
57
+ end
58
+
59
+ def initialize(file, options = {})
60
+ # command line options
61
+ @opts = options
62
+ # parsed data: ob: objects (ETS functions), ga: group addresses
63
+ @objects = {}
64
+ @group_addresses = {}
65
+ @associations = []
66
+ # log to stderr, so that redirecting stdout captures only generated data
67
+ @logger = Logger.new($stderr)
68
+ @logger.level = @opts.key?(:trace) ? @opts[:trace] : Logger::INFO
69
+ @logger.debug("options: #{@opts}")
70
+ # read .knxproj file xml into project variable
71
+ project = read_file(file)
72
+ # find out address style
73
+ proj_info = self.class.dig_xml(project[:info], %w[Project ProjectInformation])
74
+ @group_addr_style = (@opts[:addr] || proj_info['GroupAddressStyle']).to_sym
75
+ raise "Error: no such style #{@group_addr_style} in #{GROUP_ADDRESS_PARSERS.keys}" if GROUP_ADDRESS_PARSERS[@group_addr_style].nil?
76
+
77
+ @logger.info("Using project #{proj_info['Name']}, address style: #{@group_addr_style}")
78
+ # locate main node in xml
79
+ installation = self.class.dig_xml(project[:data], %w[Project Installations Installation])
80
+ # process group ranges: fill @group_addresses
81
+ process_group_ranges(self.class.dig_xml(installation, %w[GroupAddresses GroupRanges]))
82
+ # process group ranges: fill @objects (for 2 versions of ETS which have different tags?)
83
+ process_space(self.class.dig_xml(installation, ['Locations']), 'Space') if installation.key?('Locations')
84
+ process_space(self.class.dig_xml(installation, ['Buildings']), 'BuildingPart') if installation.key?('Buildings')
85
+ @logger.warn('No building information found.') if @objects.keys.empty?
86
+ return unless @opts[:specific]
87
+ unless File.exist?(@opts[:specific])
88
+ @logger.warn("Specific code file #{@opts[:specific]} not found, creating generic one.")
89
+ FileUtils.cp(File.join(File.dirname(__FILE__), 'specific', 'generic.rb'), @opts[:specific])
90
+ end
91
+ # load specific code
92
+ load(@opts[:specific])
93
+ raise "no method found in #{@opts[:specific]}" unless self.class.specific_defined?
94
+ end
95
+
96
+ def warning(entity, name, message)
97
+ @logger.warn("#{entity.red} #{name.green} #{message}")
98
+ end
99
+
100
+ # format the integer group address to string in desired style (e.g. 1/2/3)
101
+ def parse_group_address(group_address)
102
+ GROUP_ADDRESS_PARSERS[@group_addr_style].call(group_address.to_i).freeze
103
+ end
104
+
105
+ # Read both project.xml and 0.xml
106
+ # @return Hash {info: xml data, data: xml data}
107
+ def read_file(file)
108
+ raise "ETS file must end with #{ETS_EXT}" unless file.end_with?(ETS_EXT)
109
+
110
+ project = {}
111
+ # read ETS5 file and get project file
112
+ Zip::File.open(file) do |zip_file|
113
+ zip_file.each do |entry|
114
+ case entry.name
115
+ when %r{P-[^/]+/project\.xml$}
116
+ project[:info] = XmlSimple.xml_in(entry.get_input_stream.read, { 'ForceArray' => true })
117
+ when %r{P-[^/]+/0\.xml$}
118
+ project[:data] = XmlSimple.xml_in(entry.get_input_stream.read, { 'ForceArray' => true })
119
+ end
120
+ end
121
+ end
122
+ raise "Did not find project information or data (#{project.keys})" unless project.keys.sort.eql?(%i[data info])
123
+
124
+ project
125
+ end
126
+
127
+ # process group range recursively and find addresses
128
+ def process_group_ranges(group)
129
+ group['GroupRange'].each { |sgr| process_group_ranges(sgr) } if group.key?('GroupRange')
130
+ group['GroupAddress'].each { |group_address| process_ga(group_address) } if group.key?('GroupAddress')
131
+ end
132
+
133
+ # process a group address
134
+ def process_ga(group_address)
135
+ # build object for each group address
136
+ group = {
137
+ name: group_address['Name'].freeze, # ETS: name field
138
+ description: group_address['Description'].freeze, # ETS: description field
139
+ address: parse_group_address(group_address['Address']), # group address as string. e.g. "x/y/z" depending on project style
140
+ datapoint: nil, # datapoint type as string "x.00y"
141
+ ha: { address_type: nil } # prepared to be potentially modified by specific code
142
+ }
143
+ if group_address['DatapointType'].nil?
144
+ warning(group[:address], group[:name], 'no datapoint type for address group, to be defined in ETS, skipping')
145
+ return
146
+ end
147
+ # parse datapoint for easier use
148
+ if (m = group_address['DatapointType'].match(/^DPST-([0-9]+)-([0-9]+)$/))
149
+ # datapoint type as string x.00y
150
+ group[:datapoint] = format('%d.%03d', m[1].to_i, m[2].to_i) # no freeze
151
+ else
152
+ warning(group[:address], group[:name],
153
+ "Cannot parse data point type: #{group_address['DatapointType']} (DPST-x-x), skipping")
154
+ return
155
+ end
156
+ # Index is the internal Id in xml file
157
+ @group_addresses[group_address['Id'].freeze] = group.freeze
158
+ @logger.debug("group: #{group}")
159
+ end
160
+
161
+ # process locations recursively, and find functions
162
+ # @param space the current space
163
+ # @param space_type the type of space: Space or BuildingPart
164
+ # @param info current location information: floor, room
165
+ def process_space(space, space_type, info = nil)
166
+ info = info.nil? ? {} : info.dup
167
+ @logger.debug("space: #{space['Type']}: #{space['Name']} (#{info})")
168
+ # process building sub spaces
169
+ if space.key?(space_type)
170
+ # get floor when we have it
171
+ info[:floor] = space['Name'] if space['Type'].eql?('Floor')
172
+ # recursively process sub-spaces
173
+ space[space_type].each { |s| process_space(s, space_type, info) }
174
+ end
175
+ # Functions are objects
176
+ return unless space.key?('Function')
177
+
178
+ # we assume the object is directly in the room
179
+ info[:room] = space['Name']
180
+ # loop on group addresses
181
+ space['Function'].each do |ets_function|
182
+ # @logger.debug("function #{ets_function}")
183
+ # ignore functions without group address
184
+ next unless ets_function.key?('GroupAddressRef')
185
+
186
+ # the ETS object, created from ETS function
187
+ ets_object = {
188
+ name: ets_function['Name'].freeze,
189
+ type: self.class.function_type_to_name(ets_function['Type']),
190
+ ha: { domain: nil } # hone assistant values
191
+ }.merge(info)
192
+ add_object(ets_function['Id'], ets_object)
193
+ ets_function['GroupAddressRef'].map { |g| g['RefId'].freeze }.each do |group_address_id|
194
+ associate(ga_id: group_address_id, object_id: ets_function['Id'])
195
+ end
196
+ @logger.debug("function: #{ets_object}")
197
+ end
198
+ end
199
+
200
+ # map ETS function to home assistant object type
201
+ # see https://www.home-assistant.io/integrations/knx/
202
+ def map_ets_function_to_ha_object_category(ets_func)
203
+ # map FT-x type to home assistant type
204
+ case ets_func[:type]
205
+ when :switchable_light, :dimmable_light then 'light'
206
+ when :sun_protection then 'cover'
207
+ when :custom, :heating_continuous_variable, :heating_floor, :heating_radiator, :heating_switching_variable
208
+ @logger.warn("#{ets_func[:room].red} #{ets_func[:name].green} function type #{ets_func[:type].to_s.blue} not implemented")
209
+ nil
210
+ else @logger.error("#{ets_func[:room].red} #{ets_func[:name].green} function type #{ets_func[:type].to_s.blue} not supported, please report")
211
+ nil
212
+ end
213
+ end
214
+
215
+ # map datapoint to home assistant type
216
+ # see https://www.home-assistant.io/integrations/knx/
217
+ def map_ets_datapoint_to_ha_address_type(group_address, ha_object_domain)
218
+ case group_address[:datapoint]
219
+ when '1.001' then 'address' # switch on/off or state
220
+ when '1.008' then 'move_long_address' # up/down
221
+ when '1.010' then 'stop_address' # stop
222
+ when '1.011' then 'state_address' # switch state
223
+ when '3.007'
224
+ @logger.debug("#{group_address[:address]}(#{ha_object_domain}:#{group_address[:datapoint]}:#{group_address[:name]}): ignoring datapoint")
225
+ nil # dimming control: used by buttons
226
+ when '5.001' # percentage 0-100
227
+ # user-provided code tells what is state
228
+ case ha_object_domain
229
+ when 'light' then 'brightness_address'
230
+ when 'cover' then 'position_address'
231
+ else
232
+ warning(group_address[:address], group_address[:name], "#{group_address[:datapoint]} expects: light or cover, not #{ha_object_domain.magenta}")
233
+ nil
234
+ end
235
+ else
236
+ warning(group_address[:address], group_address[:name], "un-managed datapoint #{group_address[:datapoint].blue} (#{ha_object_domain.magenta})")
237
+ nil
238
+ end
239
+ end
240
+
241
+ # @param ga_id group address id
242
+ # @returns array of objects for this group id
243
+ def ga_object_ids(ga_id)
244
+ @associations.select { |a| a[:ga_id].eql?(ga_id) }.map { |a| a[:object_id] }
245
+ end
246
+
247
+ def all_object_ids
248
+ @objects.keys
249
+ end
250
+
251
+ # @param object_id object id
252
+ # @returns array of group addresses for this object
253
+ def object_ga_ids(object_id)
254
+ @associations.select { |a| a[:object_id].eql?(object_id) }.map { |a| a[:ga_id] }
255
+ end
256
+
257
+ def all_ga_ids
258
+ @group_addresses.keys
259
+ end
260
+
261
+ def associate(ga_id:, object_id:)
262
+ @associations.push({ ga_id: ga_id, object_id: object_id })
263
+ end
264
+
265
+ def delete_object(object_id)
266
+ @objects.delete(object_id)
267
+ @associations.delete_if { |a| a[:object_id].eql?(object_id) }
268
+ end
269
+
270
+ def add_object(object_id, object)
271
+ # objects will not be modified, this shall be what comes from ETS only, use field `ha` for specific code
272
+ @objects[object_id] = object.freeze
273
+ end
274
+
275
+ def object(object_id)
276
+ @objects[object_id]
277
+ end
278
+
279
+ def group_address_data(ga_id)
280
+ @group_addresses[ga_id]
281
+ end
282
+
283
+ # This creates the Home Assistant configuration in variable ha_config
284
+ # based on data coming from ETS
285
+ # and optionally modified by specific code in apply_specific
286
+ def generate_homeass
287
+ # First, apply user-provided specific code
288
+ if self.class.specific_defined?
289
+ @logger.info("Applying fix code from #{@opts[:specific]}")
290
+ fix_objects(self)
291
+ end
292
+ # This will be the YAML for HA
293
+ ha_config = {}
294
+ # warn of group addresses that will not be used (you can fix in specific code)
295
+ all_ga_ids.each do |ga_id|
296
+ next unless ga_object_ids(ga_id).empty?
297
+ ga_data = group_address_data(ga_id)
298
+ warning(ga_data[:address], ga_data[:name], 'Group not in object: use ETS to create functions or use specific code')
299
+ end
300
+ # Generate devices from either functions in ETS, or from specific code
301
+ all_object_ids.each do |object_id|
302
+ ets_object = object(object_id)
303
+ # compute object domain, this is the section in HA configuration (switch, light, etc...)
304
+ ha_object_domain = ets_object[:ha].delete(:domain) || map_ets_function_to_ha_object_category(ets_object)
305
+ if ha_object_domain.nil?
306
+ warning(ets_object[:name], ets_object[:room], "#{ets_object[:type].to_s.blue}: function type not detected, skipping")
307
+ next
308
+ end
309
+ # add domain to config, if necessary
310
+ ha_config[ha_object_domain] ||= []
311
+ # HA configuration object, either empty or initialized in specific code
312
+ ha_device = ets_object[:ha]
313
+ # default name
314
+ ha_device['name'] ||= @opts[:full_name] ? "#{ets_object[:name]} #{ets_object[:room]}" : ets_object[:name]
315
+ # check name is not duplicated, as name is used to identify the object
316
+ if ha_config[ha_object_domain].any? { |v| v['name'].casecmp?(ha_device['name']) }
317
+ @logger.warn("#{ha_device['name'].red} object name is duplicated (check ETS objects and rooms)")
318
+ end
319
+ # add object to configuration
320
+ ha_config[ha_object_domain].push(ha_device)
321
+ # process all group addresses in ETS function (object)
322
+ object_ga_ids(object_id).each do |group_address_id|
323
+ # get this group information
324
+ ga_data = group_address_data(group_address_id)
325
+ if ga_data.nil?
326
+ @logger.error("#{ets_object[:name].red} #{ets_object[:room].green} (#{ets_object[:type].to_s.magenta}): #{group_address_id}: group address not found, skipping")
327
+ next
328
+ end
329
+ # find HA property name based on datapoint
330
+ ha_address_type = ga_data[:ha][:address_type] || map_ets_datapoint_to_ha_address_type(ga_data, ha_object_domain)
331
+ next if ha_address_type.eql?(:ignore)
332
+ if ha_address_type.nil?
333
+ warning(ga_data[:address], ga_data[:name],
334
+ "#{ga_data[:datapoint].blue} / #{ha_object_domain.magenta}: address type not detected, skipping")
335
+ next
336
+ end
337
+ if ha_device.key?(ha_address_type)
338
+ @logger.error("#{ga_data[:address].red} #{ga_data[:name].green} (#{ha_object_domain.magenta}:#{ga_data[:datapoint]}) #{ha_address_type} already set with #{ha_device[ha_address_type]}, skipping")
339
+ # TODO: support passive addresses ?
340
+ next
341
+ end
342
+ # ok, we have the property name and group address
343
+ ha_device[ha_address_type] = ga_data[:address]
344
+ end
345
+ end
346
+ if @opts[:sort_by_name]
347
+ @logger.info('Sorting by name')
348
+ ha_config.each_value { |v| v.sort_by! { |o| o['name'] } }
349
+ end
350
+ # add knx level, if user asks for it
351
+ ha_config = { 'knx' => ha_config } if @opts[:ha_knx]
352
+ ha_config.to_yaml
353
+ end
354
+
355
+ # https://sourceforge.net/p/linknx/wiki/Object_Definition_section/
356
+ def generate_linknx
357
+ @group_addresses.values.sort { |a, b| a[:address] <=> b[:address] }.map do |ga_data|
358
+ linknx_name = ga_data[:name]
359
+ linknx_id = "id_#{ga_data[:address].gsub('/', '_')}"
360
+ %Q( <object type="#{ga_data[:datapoint]}" id="#{linknx_id}" gad="#{ga_data[:address]}" init="request">#{linknx_name}</object>)
361
+ end.join("\n")
362
+ end
363
+ end
364
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EtsToHass
4
+ NAME = 'ets-to-homeassistant'
5
+ VERSION = '0.1'
6
+ SRC_URL = 'https://github.com/laurent-martin/ets-to-homeassistant'
7
+ GEM_URL = 'https://github.com/laurent-martin/ets-to-homeassistant'
8
+ DOC_URL = 'https://github.com/laurent-martin/ets-to-homeassistant'
9
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This example of custom method creates one object per group address.
4
+ # It's not right, but it's simple.
5
+
6
+ def fix_objects(generator)
7
+ # generate new objects sequentially
8
+ new_object_id = 0
9
+ # loop on group addresses
10
+ generator.all_ga_ids.each do |ga_id|
11
+ # skip if group address already assigned to an object
12
+ next unless generator.ga_object_ids(ga_id).empty?
13
+ ga_data = generator.group_address_data(ga_id)
14
+ # generate a dummy object with a single group address
15
+ generator.add_object(
16
+ new_object_id, {
17
+ name: ga_data[:name],
18
+ type: :custom, # unknown type of object, switch ?
19
+ floor: 'unknown floor',
20
+ room: 'unknown room',
21
+ ha: { domain: 'switch' }
22
+ }
23
+ )
24
+ generator.associate(ga_id: ga_id, object_id: new_object_id)
25
+ # prepare next object identifier
26
+ new_object_id += 1
27
+ end
28
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # I can detect HA object address type based on group address
4
+ GROUP_TO_ADDR_TYPE = {
5
+ '/1/' => 'state_address',
6
+ '/4/' => 'brightness_state_address'
7
+ }.freeze
8
+ DATAPOINT_TO_ADDR_TYPE = {
9
+ '3.007' => :ignore # action "up" / "down"
10
+ }.freeze
11
+
12
+ # Laurent's specific code for KNX configuration
13
+ def fix_objects(generator)
14
+ # loop on objects to find blinds
15
+ # I have two types of blinds: normal and special
16
+ # "normal" blinds have a single GA for up/down
17
+ # "special" blinds have 2 GA: one for up, one for down, and are managed like a switch, so I declare 2 objects for them
18
+ generator.all_object_ids.each do |obj_id|
19
+ object = generator.object(obj_id)
20
+ # customs are switches
21
+ object[:ha][:domain] ||= 'switch' if object[:type].eql?(:custom)
22
+ # need only to manage covers/blinds
23
+ next unless object[:type].eql?(:sun_protection)
24
+ group_ids = object_ga_ids(obj_id)
25
+ # manage in special manner my blinds, identified by "pulse" in address group name
26
+ if group_address_data(group_ids.first)[:name].end_with?(':Pulse')
27
+ # delete current object: will be split into 2
28
+ generator.delete_object(obj_id)
29
+ # loop on GA for this object (one for up and one for down)
30
+ group_ids.each do |ga_id|
31
+ ga_data = group_address_data(ga_id)
32
+ # get direction of GA based on name
33
+ direction =
34
+ case ga_data[:name]
35
+ when /Montee/ then 'Montee'
36
+ when /Descente/ then 'Descente'
37
+ else raise "error: #{ga_data[:name]}"
38
+ end
39
+ # fix datapoint for GA (I have set up/down in ETS)
40
+ ga_data[:datapoint].replace('1.001')
41
+ new_object_id = "#{obj_id}_#{direction}"
42
+ # create new object
43
+ generator.add_object(
44
+ new_object_id,
45
+ {
46
+ name: "#{object[:name]} #{direction}",
47
+ type: :custom, # simple switch
48
+ floor: object[:floor],
49
+ room: object[:room],
50
+ ha: { domain: 'switch' }
51
+ }
52
+ )
53
+ generator.associate(ga_id: ga_id, object_id: new_object_id)
54
+ end
55
+ else
56
+ # set my specific times
57
+ object[:ha].merge!(
58
+ { 'travelling_time_down' => 59,
59
+ 'travelling_time_up' => 59 }
60
+ )
61
+ end
62
+ end
63
+ # lights: I use x/1/x for state and x/4/x for brightness state
64
+ generator.all_ga_ids.each do |ga_id|
65
+ ga_data = generator.group_address_data(ga_id)
66
+ GROUP_TO_ADDR_TYPE.each do |pattern, addr_type|
67
+ ga_data[:ha][:address_type] = addr_type if ga_data[:address].include?(pattern)
68
+ end
69
+ DATAPOINT_TO_ADDR_TYPE.each do |pattern, addr_type|
70
+ ga_data[:ha][:address_type] = addr_type if ga_data[:datapoint].eql?(pattern)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This example of custom method uses group address description to figure out devices
4
+ PREFIX = {
5
+ 'ECL_On/Off ' => { address_type: 'address', domain: 'light' },
6
+ # 'ECL_VAL ' => {address_type: 'todo1',domain: 'light'},
7
+ # 'État_ECL_VAL ' => {address_type: 'todo2',domain: 'light'},
8
+ 'État_ECL_On/Off ' => { address_type: 'state_address', domain: 'light' },
9
+ 'ECL_VAR ' => { address_type: 'brightness_address', domain: 'light' },
10
+ 'Pos._VR_% ' => { address_type: 'position_address', domain: 'cover' },
11
+ 'M/D_VR ' => { address_type: 'move_long_address', domain: 'cover' }
12
+ # 'État_Pos._VR_% ' => {address_type: 'todo',domain: 'cover'},
13
+ # 'T°_Amb. ' => {address_type: 'type4',domain: 'light'},
14
+ # 'Dét._Prés. ' => {address_type: 'type4',domain: 'light'},
15
+ }.freeze
16
+
17
+ # generate
18
+ def fix_objects(generator)
19
+ # loop on group addresses
20
+ generator.all_ga_ids.each do |ga_id|
21
+ # ignore if the group address is already in an object
22
+ next unless generator.ga_object_ids(ga_id).empty?
23
+ ga_data = generator.group_address_data(ga_id)
24
+ obj_name = nil
25
+ object_domain = nil
26
+ # try to guess an object name from group address name
27
+ PREFIX.each do |prefix, info|
28
+ next unless ga_data[:name].start_with?(prefix)
29
+ obj_name = ga_data[:name][prefix.length..]
30
+ ga_data[:ha][:address_type] = info[:address_type]
31
+ object_domain = info[:domain]
32
+ break
33
+ end
34
+ if obj_name.nil?
35
+ warn("unknown:#{ga_data}")
36
+ next
37
+ end
38
+
39
+ # use name as id, so that we can easily group GAs
40
+ obj_id = obj_name
41
+
42
+ if generator.object(obj_id).nil?
43
+ generator.add_object(
44
+ obj_id, {
45
+ name: obj_name,
46
+ type: :custom, # unknown, so assume just switch
47
+ floor: 'unknown floor',
48
+ room: 'unknown room',
49
+ ha: { domain: object_domain }
50
+ }
51
+ )
52
+ end
53
+ generator.associate(ga_id: ga_id, object_id: obj_id)
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add colors
4
+ class String
5
+ class << self
6
+ private
7
+
8
+ def vt_cmd(code)
9
+ "\e[#{code}m"
10
+ end
11
+ end
12
+ # see https://en.wikipedia.org/wiki/ANSI_escape_code
13
+ # symbol is the method name added to String
14
+ # it adds control chars to set color (and reset at the end).
15
+ VT_STYLES = {
16
+ bold: 1,
17
+ dim: 2,
18
+ italic: 3,
19
+ underline: 4,
20
+ blink: 5,
21
+ reverse_color: 7,
22
+ invisible: 8,
23
+ black: 30,
24
+ red: 31,
25
+ green: 32,
26
+ brown: 33,
27
+ blue: 34,
28
+ magenta: 35,
29
+ cyan: 36,
30
+ gray: 37,
31
+ bg_black: 40,
32
+ bg_red: 41,
33
+ bg_green: 42,
34
+ bg_brown: 43,
35
+ bg_blue: 44,
36
+ bg_magenta: 45,
37
+ bg_cyan: 46,
38
+ bg_gray: 47
39
+ }.freeze
40
+ private_constant :VT_STYLES
41
+ # defines methods to String, one per entry in VT_STYLES
42
+ VT_STYLES.each do |name, code|
43
+ if $stderr.tty?
44
+ begin_seq = vt_cmd(code)
45
+ end_code = 0 # by default reset all
46
+ if code <= 7 then code + 20
47
+ elsif code <= 37 then 39
48
+ elsif code <= 47 then 49
49
+ end
50
+ end_seq = vt_cmd(end_code)
51
+ define_method(name) { "#{begin_seq}#{self}#{end_seq}" }
52
+ else
53
+ define_method(name) { self }
54
+ end
55
+ end
56
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,253 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ets-to-homeassistant
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Laurent Martin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDkjCCAnqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBHMRkwFwYDVQQDDBBsYXVy
14
+ ZW50Lm1hcnRpbi5sMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/Is
15
+ ZAEZFgNjb20wHhcNMjMxMjMxMTc1NjA2WhcNMjcwMTA0MTc1NjA2WjBHMRkwFwYD
16
+ VQQDDBBsYXVyZW50Lm1hcnRpbi5sMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzAR
17
+ BgoJkiaJk/IsZAEZFgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
18
+ AQDEkaJ9DiTfuwZ00qWDbyb7jwZvYsuMCQ96gkoKj+97fUUq8PtZBQd88KAdCHYA
19
+ FAYQ2oeUz6viAylMkg5RUqd4CxnFU7Udozmnw3qyQksUsxLjbBnhg8Ye5icsumT8
20
+ T0O9jlKJ0xKW24INLghVrmyrh8qzH4EcwAUceuHB0BziMP2ZfuaiDvmEVruEmWvE
21
+ FS2B3m+JJYEfhLjHN80f/G2P3kvN44r1to/R5wBjgxvtvaHWv2amwBGZbOB1c3Sg
22
+ qdi7laIZMccF02+KfGBXbuc28b75NItFUK+62eLgSCi9/GJwZ4HC5lGpF7ac+S66
23
+ upj5v4RdZMx9sTIWfoNugI5TAgMBAAGjgYgwgYUwCQYDVR0TBAIwADALBgNVHQ8E
24
+ BAMCBLAwHQYDVR0OBBYEFGq+yaiaa5fcNGtNzPk64oVEJYUcMCUGA1UdEQQeMByB
25
+ GmxhdXJlbnQubWFydGluLmxAZ21haWwuY29tMCUGA1UdEgQeMByBGmxhdXJlbnQu
26
+ bWFydGluLmxAZ21haWwuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBZf2H7OVPcLrvM
27
+ Xy9CEJ72vs4fxjedp23jbUI+qLMUYPLCAdxe1Mbsbtp3/UcnwZWzcu8YZl+fA+29
28
+ f/s8LV6MccR3YiPgdnJhYUnNTQR/PuqsaAtyMbznW/+fv0/5IBsBd7Y0AJmO0JQl
29
+ 1yAvw+U0E+nMfQ2Ifbwo0P0N1qmQhMDS8c7TBi8/ODaPfX/US+Ypt5VTgghL5f12
30
+ j64llzG5eLduyc+OnBevqJ8aYtbgrV3vFEQYL2n+buCV8TvXIEjqGNrUAMnbITCD
31
+ eT4yyJQaHXUl49wQcaNxTyWN9uqhAiv5EJqm19jJeYoQD/qPKP5jEyQdIFx5R/5A
32
+ ty9+j+pZ
33
+ -----END CERTIFICATE-----
34
+ date: 2024-01-04 00:00:00.000000000 Z
35
+ dependencies:
36
+ - !ruby/object:Gem::Dependency
37
+ name: rubyzip
38
+ requirement: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '2.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '2.0'
50
+ - !ruby/object:Gem::Dependency
51
+ name: xml-simple
52
+ requirement: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '1.0'
64
+ - !ruby/object:Gem::Dependency
65
+ name: bundler
66
+ requirement: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '2.0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '13.0'
85
+ type: :development
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: '13.0'
92
+ - !ruby/object:Gem::Dependency
93
+ name: reek
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: 6.1.0
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: 6.1.0
106
+ - !ruby/object:Gem::Dependency
107
+ name: rspec
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '3.0'
113
+ type: :development
114
+ prerelease: false
115
+ version_requirements: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '3.0'
120
+ - !ruby/object:Gem::Dependency
121
+ name: rubocop
122
+ requirement: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '1.12'
127
+ type: :development
128
+ prerelease: false
129
+ version_requirements: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '1.12'
134
+ - !ruby/object:Gem::Dependency
135
+ name: rubocop-ast
136
+ requirement: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '1.4'
141
+ type: :development
142
+ prerelease: false
143
+ version_requirements: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '1.4'
148
+ - !ruby/object:Gem::Dependency
149
+ name: rubocop-performance
150
+ requirement: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - "~>"
153
+ - !ruby/object:Gem::Version
154
+ version: '1.10'
155
+ type: :development
156
+ prerelease: false
157
+ version_requirements: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '1.10'
162
+ - !ruby/object:Gem::Dependency
163
+ name: rubocop-shopify
164
+ requirement: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '2.0'
169
+ type: :development
170
+ prerelease: false
171
+ version_requirements: !ruby/object:Gem::Requirement
172
+ requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: '2.0'
176
+ - !ruby/object:Gem::Dependency
177
+ name: simplecov
178
+ requirement: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - "~>"
181
+ - !ruby/object:Gem::Version
182
+ version: '0.18'
183
+ type: :development
184
+ prerelease: false
185
+ version_requirements: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - "~>"
188
+ - !ruby/object:Gem::Version
189
+ version: '0.18'
190
+ - !ruby/object:Gem::Dependency
191
+ name: solargraph
192
+ requirement: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - "~>"
195
+ - !ruby/object:Gem::Version
196
+ version: '0.44'
197
+ type: :development
198
+ prerelease: false
199
+ version_requirements: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - "~>"
202
+ - !ruby/object:Gem::Version
203
+ version: '0.44'
204
+ description: Generate Home Assistant configuration from ETS project
205
+ email:
206
+ - laurent.martin.l@gmail.com
207
+ executables:
208
+ - ets_to_hass
209
+ extensions: []
210
+ extra_rdoc_files: []
211
+ files:
212
+ - CHANGELOG.md
213
+ - README.md
214
+ - bin/ets_to_hass
215
+ - lib/ets_to_hass/generator.rb
216
+ - lib/ets_to_hass/info.rb
217
+ - lib/ets_to_hass/specific/generic.rb
218
+ - lib/ets_to_hass/specific/laurent.rb
219
+ - lib/ets_to_hass/specific/specific2.rb
220
+ - lib/ets_to_hass/string_colors.rb
221
+ homepage: https://github.com/laurent-martin/ets-to-homeassistant
222
+ licenses:
223
+ - Apache-2.0
224
+ metadata:
225
+ allowed_push_host: https://rubygems.org
226
+ homepage_uri: https://github.com/laurent-martin/ets-to-homeassistant
227
+ source_code_uri: https://github.com/laurent-martin/ets-to-homeassistant
228
+ changelog_uri: https://github.com/laurent-martin/ets-to-homeassistant
229
+ rubygems_uri: https://github.com/laurent-martin/ets-to-homeassistant
230
+ documentation_uri: https://github.com/laurent-martin/ets-to-homeassistant
231
+ bug_tracker_uri: https://github.com/laurent-martin/ets-to-homeassistant
232
+ rubygems_mfa_required: 'true'
233
+ post_install_message:
234
+ rdoc_options: []
235
+ require_paths:
236
+ - lib
237
+ required_ruby_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: '2.7'
242
+ required_rubygems_version: !ruby/object:Gem::Requirement
243
+ requirements:
244
+ - - ">="
245
+ - !ruby/object:Gem::Version
246
+ version: '0'
247
+ requirements:
248
+ - Read the manual for any requirement
249
+ rubygems_version: 3.3.26
250
+ signing_key:
251
+ specification_version: 4
252
+ summary: Tool to generate Home Assistant configuration from ETS project
253
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ �=�����~QH���!I
2
+ �?W�v�n�ӎ�xss[Օx��곡zΐn. �3�f�l���ԍ�NS�$LUO��ׁQ��I�_ �����(7LZ')~�N���삵7��D�Yk���6�JHV�=�)�m��i��c�%�%Z�)(Y���o�\]݌��x Y-XY�6?Q�����O@�!�Ӎ���YнiU��K�{��%CK������ZA ��A���,�fx�w:�飆xb\B�^e�j��1�