matterbridge-bthome 0.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.
- package/CHANGELOG.md +48 -0
- package/LICENSE +202 -0
- package/README.md +276 -0
- package/bin/bthome.js +2 -0
- package/bmc-button.svg +22 -0
- package/dist/BTHome.js +349 -0
- package/dist/BTHomeDecoder.js +52 -0
- package/dist/BTHomeShellyMdDecoder.js +70 -0
- package/dist/BTHomeSpec.js +154 -0
- package/dist/index.js +4 -0
- package/dist/platform.js +197 -0
- package/matterbridge-bthome.schema.json +71 -0
- package/matterbridge.svg +50 -0
- package/npm-shrinkwrap.json +1698 -0
- package/package.json +65 -0
package/dist/platform.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { bridgedNode, contactSensor, genericSwitch, humiditySensor, lightSensor, MatterbridgeDynamicPlatform, MatterbridgeEndpoint, NumberTag, occupancySensor, powerSource, pressureSensor, temperatureSensor, } from 'matterbridge';
|
|
2
|
+
import { db, debugStringify, idn, rs, BLUE } from 'matterbridge/logger';
|
|
3
|
+
import { isValidArray } from 'matterbridge/utils';
|
|
4
|
+
import { BTHome } from './BTHome.js';
|
|
5
|
+
export class Platform extends MatterbridgeDynamicPlatform {
|
|
6
|
+
btHome = new BTHome();
|
|
7
|
+
bridgedDevices = new Map();
|
|
8
|
+
constructor(matterbridge, log, config) {
|
|
9
|
+
super(matterbridge, log, config);
|
|
10
|
+
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.0.0')) {
|
|
11
|
+
throw new Error(`This plugin requires Matterbridge version >= "3.0.0". Please update Matterbridge to the latest version in the frontend.`);
|
|
12
|
+
}
|
|
13
|
+
this.log.info('Initializing platform:', this.config.name);
|
|
14
|
+
if (!isValidArray(config.whiteList))
|
|
15
|
+
config.whiteList = [];
|
|
16
|
+
if (!isValidArray(config.blackList))
|
|
17
|
+
config.blackList = [];
|
|
18
|
+
this.btHome.on('discovered', async (device) => {
|
|
19
|
+
this.log.notice(`Discovered new BTHome device: ${device.mac}`);
|
|
20
|
+
this.log.info('- name:', device.localName);
|
|
21
|
+
this.log.info('- rssi:', device.rssi);
|
|
22
|
+
this.log.info('- version:', device.version);
|
|
23
|
+
this.log.info('- encrypted:', device.encrypted);
|
|
24
|
+
this.log.info('- trigger:', device.trigger);
|
|
25
|
+
this.log.info('- data:', debugStringify(device.data));
|
|
26
|
+
this.addDevice(device);
|
|
27
|
+
await this.savePeripherals();
|
|
28
|
+
});
|
|
29
|
+
this.btHome.on('update', async (device) => {
|
|
30
|
+
this.log.info(`${db}BTHome message from ${idn}${device.mac}${rs}${db} rssi ${BLUE}${device.rssi}${db} name ${BLUE}${device.localName}${db} version ${BLUE}${device.version}${db} ${BLUE}${device.encrypted ? 'encrypted ' : ''}${device.trigger ? 'trigger ' : ''}${db}data ${debugStringify(device.data)}`);
|
|
31
|
+
await this.updateDevice(device);
|
|
32
|
+
});
|
|
33
|
+
this.log.info('Finished initializing platform:', this.config.name);
|
|
34
|
+
}
|
|
35
|
+
async onStart(reason) {
|
|
36
|
+
this.log.info('onStart called with reason:', reason ?? 'none');
|
|
37
|
+
await this.ready;
|
|
38
|
+
await this.clearSelect();
|
|
39
|
+
await this.loadPeripherals();
|
|
40
|
+
await this.btHome.start();
|
|
41
|
+
}
|
|
42
|
+
async onConfigure() {
|
|
43
|
+
await super.onConfigure();
|
|
44
|
+
this.log.info('onConfigure called');
|
|
45
|
+
this.btHome.bthomePeripherals.forEach(async (device) => {
|
|
46
|
+
this.updateDevice(device);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async onAction(action, value, id) {
|
|
50
|
+
this.log.info('onAction called with action:', action, 'and value:', value ?? 'none', 'and id:', id ?? 'none');
|
|
51
|
+
if (action === 'delete' && value) {
|
|
52
|
+
value = value.toLowerCase().trimStart().trimEnd();
|
|
53
|
+
if (!this.btHome.bthomePeripherals.has(value)) {
|
|
54
|
+
this.log.error(`The device ${value} is not registered. Please check the MAC address.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await this.btHome.stop();
|
|
58
|
+
this.btHome.bthomePeripherals.delete(value);
|
|
59
|
+
await this.savePeripherals();
|
|
60
|
+
const device = this.bridgedDevices.get(value);
|
|
61
|
+
if (device)
|
|
62
|
+
this.unregisterDevice(device);
|
|
63
|
+
this.bridgedDevices.delete(value);
|
|
64
|
+
this.log.notice(`The device ${value} has been deleted. Please restart the plugin.`);
|
|
65
|
+
}
|
|
66
|
+
if (action === 'reset') {
|
|
67
|
+
await this.btHome.stop();
|
|
68
|
+
this.btHome.bthomePeripherals.clear();
|
|
69
|
+
await this.savePeripherals();
|
|
70
|
+
this.unregisterAllDevices();
|
|
71
|
+
this.bridgedDevices.clear();
|
|
72
|
+
this.log.notice('The storage has been reset');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async onShutdown(reason) {
|
|
76
|
+
this.log.info('onShutdown called with reason:', reason ?? 'none');
|
|
77
|
+
await this.savePeripherals();
|
|
78
|
+
this.btHome.logDevices();
|
|
79
|
+
await this.btHome.stop();
|
|
80
|
+
await super.onShutdown(reason);
|
|
81
|
+
if (this.config.unregisterOnShutdown === true)
|
|
82
|
+
await this.unregisterAllDevices();
|
|
83
|
+
this.bridgedDevices.clear();
|
|
84
|
+
this.log.info('onShutdown finished');
|
|
85
|
+
}
|
|
86
|
+
converter = [
|
|
87
|
+
{ reading: 'battery', deviceType: powerSource, cluster: 'PowerSource', attribute: 'batPercentRemaining', factor: 2 },
|
|
88
|
+
{ reading: 'temperature', deviceType: temperatureSensor, cluster: 'TemperatureMeasurement', attribute: 'measuredValue', factor: 100 },
|
|
89
|
+
{ reading: 'humidity', deviceType: humiditySensor, cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue', factor: 100 },
|
|
90
|
+
{ reading: 'pressure', deviceType: pressureSensor, cluster: 'PressureMeasurement', attribute: 'measuredValue', factor: 100 },
|
|
91
|
+
{ reading: 'motionState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
|
|
92
|
+
{ reading: 'movingState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
|
|
93
|
+
{ reading: 'occupancyState', deviceType: occupancySensor, cluster: 'OccupancySensing', attribute: 'occupancy', property: 'occupied', type: 'boolean' },
|
|
94
|
+
{ reading: 'illuminance', deviceType: lightSensor, cluster: 'IlluminanceMeasurement', attribute: 'measuredValue', type: 'lux' },
|
|
95
|
+
{ reading: 'doorState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
|
|
96
|
+
{ reading: 'garageDoorState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
|
|
97
|
+
{ reading: 'windowState', deviceType: contactSensor, cluster: 'BooleanState', attribute: 'stateValue', type: 'boolean_inverted' },
|
|
98
|
+
{ reading: 'button', deviceType: genericSwitch, cluster: 'Switch' },
|
|
99
|
+
{ reading: 'rotation_deg' },
|
|
100
|
+
{ reading: 'packetId' },
|
|
101
|
+
{ reading: 'deviceTypeId' },
|
|
102
|
+
{ reading: 'firmwareVersion' },
|
|
103
|
+
{ reading: 'firmwareVersionShort' },
|
|
104
|
+
{ reading: 'text' },
|
|
105
|
+
{ reading: 'raw' },
|
|
106
|
+
];
|
|
107
|
+
async addDevice(device) {
|
|
108
|
+
this.setSelectDevice(device.mac, device.localName, undefined, 'ble');
|
|
109
|
+
if (!this.validateDevice(device.mac, true))
|
|
110
|
+
return;
|
|
111
|
+
const matterbridgeDevice = new MatterbridgeEndpoint([bridgedNode], { uniqueStorageKey: 'BTHome ' + device.mac }, this.config.debug).createDefaultBridgedDeviceBasicInformationClusterServer('BTHome ' + device.mac, device.mac, this.matterbridge.aggregatorVendorId, this.matterbridge.aggregatorVendorName, 'BTHomeDevice');
|
|
112
|
+
for (const property in device.data) {
|
|
113
|
+
const [name, index] = property.split(':');
|
|
114
|
+
const converter = this.converter.find((converter) => converter.reading === name);
|
|
115
|
+
if (converter && converter.deviceType) {
|
|
116
|
+
this.setSelectDeviceEntity(device.mac, property, `${name}${index ? ' n. ' + index : ''}`, 'ble');
|
|
117
|
+
const child = matterbridgeDevice.addChildDeviceType(property, converter.deviceType, index
|
|
118
|
+
? {
|
|
119
|
+
uniqueStorageKey: property,
|
|
120
|
+
tagList: [{ mfgCode: null, namespaceId: NumberTag.Zero.namespaceId, tag: parseInt(index), label: null }],
|
|
121
|
+
}
|
|
122
|
+
: {
|
|
123
|
+
uniqueStorageKey: property,
|
|
124
|
+
});
|
|
125
|
+
if (converter.cluster === 'PowerSource')
|
|
126
|
+
child.createDefaultPowerSourceReplaceableBatteryClusterServer();
|
|
127
|
+
child.addRequiredClusterServers();
|
|
128
|
+
}
|
|
129
|
+
else if (converter && !converter.deviceType) {
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.log.warn(`No converter found for property ${name} in device ${device.mac}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
await this.registerDevice(matterbridgeDevice);
|
|
136
|
+
this.bridgedDevices.set(device.mac, matterbridgeDevice);
|
|
137
|
+
await this.updateDevice(device);
|
|
138
|
+
}
|
|
139
|
+
async updateDevice(device) {
|
|
140
|
+
if (!this.validateDevice(device.mac, false))
|
|
141
|
+
return;
|
|
142
|
+
const matterbridgeDevice = this.bridgedDevices.get(device.mac);
|
|
143
|
+
if (!matterbridgeDevice)
|
|
144
|
+
return;
|
|
145
|
+
for (const property in device.data) {
|
|
146
|
+
const converter = this.converter.find((converter) => converter.reading === property);
|
|
147
|
+
if (converter && converter.deviceType && converter.cluster && converter.attribute) {
|
|
148
|
+
const child = matterbridgeDevice.getChildEndpointByName(property);
|
|
149
|
+
let value = device.data[property];
|
|
150
|
+
if (converter.factor && typeof value === 'number')
|
|
151
|
+
value = value * converter.factor;
|
|
152
|
+
if (converter.type === 'boolean' && typeof value === 'number')
|
|
153
|
+
value = device.data[property] !== 0;
|
|
154
|
+
if (converter.type === 'boolean_inverted' && typeof value === 'number')
|
|
155
|
+
value = device.data[property] === 0;
|
|
156
|
+
if (converter.type === 'lux' && typeof value === 'number')
|
|
157
|
+
value = Math.round(Math.max(Math.min(10000 * Math.log10(value), 0xfffe), 0));
|
|
158
|
+
if (converter.property) {
|
|
159
|
+
await child?.updateAttribute(converter.cluster, converter.attribute, { [converter.property]: value }, child.log);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
await child?.updateAttribute(converter.cluster, converter.attribute, value, child.log);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (converter && converter.deviceType && converter.cluster === 'Switch') {
|
|
166
|
+
const child = matterbridgeDevice.getChildEndpointByName(property);
|
|
167
|
+
const value = device.data[property];
|
|
168
|
+
if (child) {
|
|
169
|
+
if (value === 'single_press')
|
|
170
|
+
await child.triggerSwitchEvent('Single', child.log);
|
|
171
|
+
else if (value === 'double_press')
|
|
172
|
+
await child.triggerSwitchEvent('Double', child.log);
|
|
173
|
+
else if (value === 'long_press')
|
|
174
|
+
await child.triggerSwitchEvent('Long', child.log);
|
|
175
|
+
}
|
|
176
|
+
device.data[property] = 'none';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async loadPeripherals() {
|
|
181
|
+
if (!this.context)
|
|
182
|
+
throw new Error('Plugin context is not available');
|
|
183
|
+
const bthomePeripherals = await this.context.get('bthomePeripherals', []);
|
|
184
|
+
this.log.info(`Loading ${bthomePeripherals.length} BTHome devices from the storage...`);
|
|
185
|
+
for (const peripheral of bthomePeripherals) {
|
|
186
|
+
await this.addDevice(peripheral);
|
|
187
|
+
this.btHome.bthomePeripherals.set(peripheral.mac, peripheral);
|
|
188
|
+
this.log.debug(`Loaded BTHome device ${idn}${peripheral.mac}${rs}${db} ${peripheral.localName} from the storage`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async savePeripherals() {
|
|
192
|
+
if (!this.context)
|
|
193
|
+
throw new Error('Plugin context is not available');
|
|
194
|
+
await this.context.set('bthomePeripherals', Array.from(this.btHome.bthomePeripherals.values()));
|
|
195
|
+
this.log.info(`Saved ${this.btHome.bthomePeripherals.size} BTHome devices in the storage`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Matterbridge BTHome plugin",
|
|
3
|
+
"description": "matterbridge-bthome v. 0.0.1 by https://github.com/Luligu",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"description": "Plugin name",
|
|
8
|
+
"type": "string",
|
|
9
|
+
"readOnly": true,
|
|
10
|
+
"ui:widget": "hidden"
|
|
11
|
+
},
|
|
12
|
+
"type": {
|
|
13
|
+
"description": "Plugin type",
|
|
14
|
+
"type": "string",
|
|
15
|
+
"readOnly": true,
|
|
16
|
+
"ui:widget": "hidden"
|
|
17
|
+
},
|
|
18
|
+
"version": {
|
|
19
|
+
"description": "Plugin version",
|
|
20
|
+
"type": "string",
|
|
21
|
+
"readOnly": true,
|
|
22
|
+
"default": "0.0.1",
|
|
23
|
+
"ui:widget": "hidden"
|
|
24
|
+
},
|
|
25
|
+
"whiteList": {
|
|
26
|
+
"description": "Only the BTHome devices in the list will be exposed. If the list is empty, all the BTHome devices will be exposed.",
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"uniqueItems": true,
|
|
32
|
+
"selectFrom": "serial"
|
|
33
|
+
},
|
|
34
|
+
"blackList": {
|
|
35
|
+
"description": "The BTHome devices in the list will not be exposed. If the list is empty, no BTHome devices will be excluded.",
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"uniqueItems": true,
|
|
41
|
+
"selectFrom": "serial"
|
|
42
|
+
},
|
|
43
|
+
"delete": {
|
|
44
|
+
"description": "Delete a device from the storage. This will remove the devices from the storage and from the controller(s).",
|
|
45
|
+
"type": "boolean",
|
|
46
|
+
"buttonField": "Delete",
|
|
47
|
+
"textPlaceholder": "Enter the device mac address",
|
|
48
|
+
"buttonClose": false,
|
|
49
|
+
"buttonSave": false,
|
|
50
|
+
"default": false
|
|
51
|
+
},
|
|
52
|
+
"reset": {
|
|
53
|
+
"description": "Reset the storage. This will remove all the devices from the storage and from the controller(s).",
|
|
54
|
+
"type": "boolean",
|
|
55
|
+
"buttonText": "Reset",
|
|
56
|
+
"buttonClose": true,
|
|
57
|
+
"buttonSave": true,
|
|
58
|
+
"default": false
|
|
59
|
+
},
|
|
60
|
+
"debug": {
|
|
61
|
+
"description": "Enable the debug for the plugin.",
|
|
62
|
+
"type": "boolean",
|
|
63
|
+
"default": false
|
|
64
|
+
},
|
|
65
|
+
"unregisterOnShutdown": {
|
|
66
|
+
"description": "Unregister all devices on shutdown. This will remove all devices from the controller when the plugin is stopped.",
|
|
67
|
+
"type": "boolean",
|
|
68
|
+
"default": false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
package/matterbridge.svg
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 296.2 296.2">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="lg1" x1="16.6" y1="16.6" x2="279.6" y2="279.6" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#00b48d" />
|
|
5
|
+
<stop offset=".1" stop-color="#3faa77" />
|
|
6
|
+
<stop offset=".3" stop-color="#234148" />
|
|
7
|
+
<stop offset=".7" stop-color="#203b44" />
|
|
8
|
+
<stop offset=".9" stop-color="#ad2e6e" />
|
|
9
|
+
<stop offset="1" stop-color="#c81b74" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="lg2" x1="31.1" y1="31.1" x2="265.1" y2="265.1" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop offset="0" stop-color="#00b48d" />
|
|
13
|
+
<stop offset=".2" stop-color="#285251" />
|
|
14
|
+
<stop offset=".4" stop-color="#234148" />
|
|
15
|
+
<stop offset=".8" stop-color="#203b44" />
|
|
16
|
+
<stop offset=".9" stop-color="#a8316c" />
|
|
17
|
+
<stop offset="1" stop-color="#c81b74" />
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<linearGradient id="lg3" x1="116.2" y1="143.9" x2="139.8" y2="143.9"
|
|
20
|
+
gradientUnits="userSpaceOnUse">
|
|
21
|
+
<stop offset="0" stop-color="#8bc751" />
|
|
22
|
+
<stop offset="1" stop-color="#0db14b" />
|
|
23
|
+
</linearGradient>
|
|
24
|
+
<linearGradient id="lg4" x1="136.1" y1="100.8" x2="159.6" y2="100.8"
|
|
25
|
+
xlink:href="#lg3" />
|
|
26
|
+
<linearGradient id="lg5" x1="155.3" y1="143.9" x2="178.9" y2="143.9"
|
|
27
|
+
xlink:href="#lg3" />
|
|
28
|
+
<linearGradient id="lg6" x1="46.8" y1="25.7" x2="89.6" y2="74.8" gradientUnits="userSpaceOnUse">
|
|
29
|
+
<stop offset="0" stop-color="#b1d34a" />
|
|
30
|
+
<stop offset="1" stop-color="#50b848" />
|
|
31
|
+
</linearGradient>
|
|
32
|
+
</defs>
|
|
33
|
+
<rect width="296.2" height="296.2" rx="56.7" ry="56.7" style="fill:url(#lg1)" />
|
|
34
|
+
<rect x="16.3" y="16.3" width="263.6" height="263.6" rx="50.5" ry="50.5" style="fill:url(#lg2)" />
|
|
35
|
+
<circle cx="128" cy="143.9" r="11.8" style="fill:url(#lg3)" />
|
|
36
|
+
<circle cx="147.8" cy="100.8" r="11.8" style="fill:url(#lg4)" />
|
|
37
|
+
<path
|
|
38
|
+
d="m244.6 114.5.4-.5L160 33a17 17 0 0 0-24.7-.5l-86.4 83.3a15 15 0 0 0 9.2 26.9h19.3v-4.7l-13.7-12.7v-.1l83.7-80.8 84.2 81-13.9 12.8v4.5h19.5a15 15 0 0 0 7.4-28.1Z"
|
|
39
|
+
style="fill:url(#lg3)" />
|
|
40
|
+
<circle cx="167.1" cy="143.9" r="11.8" style="fill:url(#lg5)" />
|
|
41
|
+
<path fill="#fff" d="M219 89.3V35.5a10.5 10.5 0 1 0-21 0v33.7l21 20Z" />
|
|
42
|
+
<path
|
|
43
|
+
d="M91.4 73.3H83a37 37 0 0 0-14.5-28.4L65 50.2c.1 0 12.6 9 11.7 25.4-5.3-.4-11.2-1.9-16.3-5.3-11.8-7.8-16-23.7-11.9-46 8.7 1.5 34 7 43 22.8 4.1 7.3 4.1 16.1 0 26.2Z"
|
|
44
|
+
style="fill:url(#lg6)" />
|
|
45
|
+
<path
|
|
46
|
+
d="M65.9 80a49.6 49.6 0 0 0 17.8 2.2l16.6-16c1.6-8.3.5-15.7-3.3-22.4C84.6 22 47.8 17.5 46.2 17.4l-3-.4-.6 3c-3.8 18.4-5.9 50.6 23.2 60ZM48.4 24.4c8.7 1.5 34 7 43 22.8 4.1 7.3 4.1 16.1 0 26.2H83a37 37 0 0 0-14.5-28.4l-3.7 5.3c.1 0 12.6 9 11.7 25.4-5.3-.4-11.2-1.9-16.3-5.3-11.9-7.8-16-23.7-11.9-46Z"
|
|
47
|
+
fill="#1e5857" />
|
|
48
|
+
<path fill="#fff"
|
|
49
|
+
d="M250.5 90.5a17.4 17.4 0 1 1 0-34.8 17.4 17.4 0 0 1 0 34.8Zm0-22.7a5.4 5.4 0 0 0 0 10.7 5.3 5.3 0 0 0 0-10.7ZM258.8 148.2a15.9 15.9 0 0 0-9.6 28.5c-.8 4.2-5.4 4.6-5.4 4.6h-26v-43l13.6-13-1.8-2-82.2-79-81.2 78.3-2.5 2.6 13.7 13v42.9H53a21.5 21.5 0 1 0 11.7 15h12.6v18.8c0 7.8 6.4 14.1 14.1 14.1h29.3v14.8H64a10.6 10.6 0 0 0-17.7 8 10.6 10.6 0 0 0 17.6 8h157.6a16.3 16.3 0 1 0 0-16h-84.8V229h66.8c7.8 0 14.2-6.3 14.2-14.1v-19.2h27.6c14.3 0 17.8-12.8 18.5-16.6a15.9 15.9 0 0 0-5-30.9ZM43.7 210.8a10.3 10.3 0 1 1 0-20.6 10.3 10.3 0 0 1 0 20.6Zm192 36a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm-77-34.8h-22v-34h22v34Zm8.4-79.8c2.7 0 5.2 1 7.2 2.5v-10.4L188 137s2.6 1.3 4.6 1.3h6.7v68c0 3.2-2.6 5.7-5.7 5.7h-19v-34h1.4a7.5 7.5 0 0 0 0-15H120a7.5 7.5 0 0 0 0 15h.7v34h-19.3a5.7 5.7 0 0 1-5.7-5.6v-68.1h6.7c2 0 4.6-1.3 4.6-1.3l13.7-12.7v10.4a11.7 11.7 0 0 1 16 1.6v-13a14.9 14.9 0 0 0-25-10.8s-.1.2-.1.2l-.5.5-6.9 7H92.5l55-53.2 55.1 53.2h-11.8l-7-7c0-.2-.2-.3-.4-.5l-.2-.2a14.8 14.8 0 0 0-25 10.9v12.9c2.2-2.5 5.3-4.1 8.9-4.1Zm91.7 36.7a4.9 4.9 0 1 1 0-9.7 4.9 4.9 0 0 1 0 9.7Z" />
|
|
50
|
+
</svg>
|