ets-to-homeassistant 0.1

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 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�