Skip to content

Commit 3384c59

Browse files
committed
update 0.0.1
1 parent 8036b81 commit 3384c59

File tree

7 files changed

+227
-51
lines changed

7 files changed

+227
-51
lines changed

custom_components/bituopmd/button.py

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,20 @@ async def async_setup_entry(hass, entry, async_add_entities):
3030
device_info = await coordinator.fetch_device_info()
3131
except UpdateFailed:
3232
_LOGGER.error("Failed to fetch device info for %s", host_ip)
33-
device_info = {"model": "Unknown Model", "fw_version": "Unknown"}
33+
device_info = {"model": "Unknown Model", "fw_version": "Unknown", "manufacturer": "Unknown", "MCUVersion": "Unknown", "manufacturer": "Unknown", "mcu_version": "Unknown"}
3434

3535
buttons = []
36+
37+
sensor_coordinator = hass.data[DOMAIN][entry.entry_id]['sensor_coordinator']
38+
buttons.append(DataRefreshButton(sensor_coordinator, host_ip, device_info["model"], device_info["fw_version"], device_info["manufacturer"], device_info["mcu_version"]))
39+
3640
if coordinator.data:
3741
for field, action in coordinator.data.items():
3842
if "switch" in field.lower():
3943
continue # Skip buttons with 'switch' in the name
4044
if field == "zero":
4145
field = "zero_Energy" # Rename 'zero' to 'zeroenergy'
42-
buttons.append(BituoButton(coordinator, host_ip, field, action, device_info["model"], device_info["fw_version"]))
46+
buttons.append(BituoButton(coordinator, host_ip, field, action, device_info["model"], device_info["fw_version"], device_info["manufacturer"], device_info["mcu_version"]))
4347

4448
async_add_entities(buttons, True)
4549

@@ -71,26 +75,30 @@ async def fetch_device_info(self):
7175
return {
7276
"model": data.get("productModel") or data.get("ProductModel", "Unknown Model"),
7377
"fw_version": data.get("FWVersion") or data.get("fwVersion", "Unknown"),
78+
"manufacturer": data.get("Manufactor", "Unknown"),
79+
"mcu_version": data.get("MCUVersion", "Unknown"),
7480
}
7581
except Exception as err:
7682
raise UpdateFailed(f"Error fetching device info: {err}")
7783

7884
class BituoButton(CoordinatorEntity, ButtonEntity):
7985
"""Representation of a Button."""
8086

81-
def __init__(self, coordinator, host_ip, field, action, model, fw_version):
87+
def __init__(self, coordinator, host_ip, field, action, model, fw_version, manufacturer, mcu_version):
8288
"""Initialize the button."""
8389
super().__init__(coordinator)
8490
self._field = field
8591
self._action = action
8692
self._attr_name = f"{field.replace('_', ' ').title()}"
8793
self._attr_unique_id = f"{host_ip}_{field}"
94+
self.entity_id = f"button.{host_ip.replace('.', '_')}_{self.format_field_entity_id(field)}"
8895
self._attr_device_info = DeviceInfo(
8996
identifiers={(DOMAIN, host_ip)},
9097
name=f"{model} - {host_ip}",
91-
manufacturer="BituoTechnik",
98+
manufacturer=manufacturer,
9299
model=model,
93-
sw_version=fw_version,
100+
sw_version=f"S{fw_version}_M{self.format_version(mcu_version)}",
101+
configuration_url=f"http://{host_ip}" # embed URL
94102
)
95103
self._host_ip = host_ip
96104

@@ -99,5 +107,71 @@ async def async_press(self):
99107
await self.hass.async_add_executor_job(
100108
requests.get, f"http://{self._host_ip}/{self._action}"
101109
)
110+
111+
@staticmethod
112+
def format_field_entity_id(field):
113+
"""Format field name to be more suitable for unique_id."""
114+
formatted_name = ''.join(['_' + char.lower() if char.isupper() else char for char in field])
115+
return formatted_name.strip('_')
116+
117+
@staticmethod
118+
def format_version(version):
119+
if version.lower() == "unknown":
120+
return version
121+
parts = version.split('.')
122+
formatted_parts = []
123+
for part in parts:
124+
if part.strip(): # 检查部分是否为空
125+
try:
126+
formatted_parts.append(str(int(part)))
127+
except ValueError:
128+
formatted_parts.append('unknown')
129+
else:
130+
formatted_parts.append('unknown') # 如果部分为空,设置为 'unknown'
131+
132+
formatted_version = '.'.join(formatted_parts)
133+
return formatted_version
134+
135+
class DataRefreshButton(CoordinatorEntity, ButtonEntity):
136+
"""Representation of a Data Refresh Button."""
137+
138+
def __init__(self, coordinator, host_ip, model, fw_version, manufacturer, mcu_version):
139+
"""Initialize the button."""
140+
super().__init__(coordinator)
141+
self._attr_name = "Data Refresh"
142+
self._attr_unique_id = f"{host_ip}_data_refresh"
143+
self.entity_id = f"button.{host_ip.replace('.', '_')}_data_refresh"
144+
self._attr_device_info = DeviceInfo(
145+
identifiers={(DOMAIN, host_ip)},
146+
name=f"{model} - {host_ip}",
147+
manufacturer=manufacturer,
148+
model=model,
149+
sw_version=f"S{fw_version}_M{self.format_version(mcu_version)}",
150+
configuration_url=f"http://{host_ip}" # embed URL
151+
)
152+
self._host_ip = host_ip
153+
self._attr_icon = "mdi:refresh"
154+
155+
async def async_press(self):
156+
"""Handle the button press to refresh data."""
157+
await self.coordinator.async_request_refresh()
158+
159+
@staticmethod
160+
def format_version(version):
161+
if version.lower() == "unknown":
162+
return version
163+
parts = version.split('.')
164+
formatted_parts = []
165+
for part in parts:
166+
if part.strip(): # 检查部分是否为空
167+
try:
168+
formatted_parts.append(str(int(part)))
169+
except ValueError:
170+
formatted_parts.append('unknown')
171+
else:
172+
formatted_parts.append('unknown') # 如果部分为空,设置为 'unknown'
173+
174+
formatted_version = '.'.join(formatted_parts)
175+
return formatted_version
102176

103177
# by Script0803

custom_components/bituopmd/config_flow.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self):
1818
self.host = None
1919
self.name = None
2020
self.devices = []
21+
self._manual_scan_active = False
2122

2223
async def async_step_user(self, user_input=None):
2324
errors = {}
@@ -45,6 +46,11 @@ async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo):
4546

4647
if "bituotechnik" not in self.name.lower():
4748
return self.async_abort(reason="not_bituotechnik_device")
49+
50+
existing_entries = self._async_current_entries()
51+
if any(entry.data.get(CONF_HOST_IP) == self.host for entry in existing_entries):
52+
_LOGGER.info(f"Device {self.name} at {self.host} already configured, ignoring discovery.")
53+
return self.async_abort(reason="already_configured")
4854

4955
await self.async_set_unique_id(self.host)
5056
self._abort_if_unique_id_configured()
@@ -61,12 +67,22 @@ async def async_step_zeroconf(self, discovery_info: ZeroconfServiceInfo):
6167
async def async_step_zeroconf_confirm(self, user_input=None):
6268
"""Handle zeroconf discovery confirmation."""
6369
if user_input is not None:
70+
# 检查设备是否已经存在于当前配置的设备列表中
71+
existing_entries = self._async_current_entries()
72+
if any(entry.data.get(CONF_HOST_IP) == self.host for entry in existing_entries):
73+
_LOGGER.info(f"Device {self.name} at {self.host} is already configured, aborting configuration.")
74+
return self.async_abort(reason="already_configured")
75+
76+
# 如果设备没有配置,继续创建配置条目
6477
entry = self.async_create_entry(
6578
title=self.name,
6679
data={CONF_HOST_IP: self.host}
6780
)
68-
# remove paired device
81+
82+
# 在设备配对成功后,移除已配对的设备
6983
self.devices = [device for device in self.devices if device["ip"] != self.host]
84+
await self.async_remove_discovered_device(self.host)
85+
7086
return entry
7187

7288
data_schema = vol.Schema({
@@ -116,9 +132,32 @@ async def async_step_zeroconf_discovery_selected(self, user_input=None):
116132
if user_input is not None:
117133
selected_device_ip = user_input["device"]
118134
selected_device_name = next(device["name"] for device in self.devices if device["ip"] == selected_device_ip)
119-
return self.async_create_entry(title=f"{selected_device_name} - {selected_device_ip}", data={CONF_HOST_IP: selected_device_ip})
135+
136+
# 检查设备是否已经配置
137+
existing_entries = self._async_current_entries()
138+
if any(entry.data.get(CONF_HOST_IP) == selected_device_ip for entry in existing_entries):
139+
_LOGGER.info(f"Device {selected_device_name} at {selected_device_ip} already configured, ignoring.")
140+
return self.async_abort(reason="already_configured") # 终止重复配置
141+
142+
# 创建配置条目
143+
entry = self.async_create_entry(title=f"{selected_device_name} - {selected_device_ip}", data={CONF_HOST_IP: selected_device_ip})
144+
145+
# 成功配对后移除设备
146+
await self.async_remove_discovered_device(selected_device_ip)
147+
148+
return entry
120149

121150
return self.async_abort(reason="no_device_selected")
151+
152+
async def async_remove_discovered_device(self, host_ip):
153+
"""Remove the discovered device from the discovered list."""
154+
for flow in self.hass.config_entries.flow.async_progress():
155+
if flow["context"]["source"] == 'zeroconf':
156+
if flow["context"]["unique_id"] == host_ip:
157+
self.hass.config_entries.flow.async_abort(flow["flow_id"])
158+
break
159+
else:
160+
_LOGGER.info(f"No matching flow found for IP: {host_ip}.")
122161

123162
async def async_step_manual(self, user_input=None):
124163
errors = {}
@@ -197,7 +236,9 @@ def _on_service_state_change(self, zeroconf, service_type, name, state_change):
197236
address = socket.inet_ntoa(info.addresses[0])
198237
if "bituotechnik" in name.lower(): # Ensure the device name contains 'bituotechnik'
199238
# Check if the device is already in the list or already configured
200-
if not any(device["ip"] == address for device in self.devices):
201-
existing_entries = self._async_current_entries()
202-
if not any(entry.data.get(CONF_HOST_IP) == address for entry in existing_entries):
239+
existing_entries = self._async_current_entries()
240+
241+
# 过滤掉已存在的设备
242+
if not any(entry.data.get(CONF_HOST_IP) == address for entry in existing_entries):
243+
if not any(device["ip"] == address for device in self.devices):
203244
self.devices.append({"ip": address, "name": name.split(".")[0]})
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"SPM01": "1.1.5",
3-
"SPM02": "1.1.5",
4-
"SDM01": "3.3.2",
2+
"SPM01": "1.1.6",
3+
"SPM02": "1.1.6",
4+
"SDM01": "3.3.3",
55
"SDM02": "1.1.1"
66
}

custom_components/bituopmd/sensor.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def load_ota_versions():
6161
settings = load_settings()
6262
ota_versions = load_ota_versions()
6363

64+
65+
6466
UNIT_MAPPING = {
6567
"unbalancelinecurrents": PERCENTAGE,
6668
"powerfactor": None,
@@ -83,7 +85,7 @@ def load_ota_versions():
8385
"rssi": SensorStateClass.MEASUREMENT,
8486
}
8587

86-
EXCLUDE_FIELDS = {"post", "Post", "Config485", "MqttStatus", "productModel", "ProductModel", "Serialnumber", "SerialNumber", "devicetype", "DeviceType", "FWVersion"}
88+
EXCLUDE_FIELDS = {"Post", "Config485", "MqttStatus", "ProductModel", "SerialNumber", "DeviceType", "FWVersion", "MCUVersion", "Manufactor"}
8789

8890
async def async_setup_entry(hass, entry, async_add_entities):
8991
"""Set up sensor platform."""
@@ -92,6 +94,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
9294
coordinator = BituoDataUpdateCoordinator(hass, host_ip, current_scan_interval)
9395
await coordinator.async_config_entry_first_refresh()
9496

97+
# Store the coordinator so it can be accessed by other platforms like button
98+
hass.data.setdefault(DOMAIN, {})
99+
hass.data[DOMAIN][entry.entry_id] = {
100+
'sensor_coordinator': coordinator
101+
}
102+
95103
# Fetch device model and firmware version
96104
try:
97105
device_info = await coordinator.fetch_device_info()
@@ -101,11 +109,11 @@ async def async_setup_entry(hass, entry, async_add_entities):
101109

102110
# Create sensor entities for each data field
103111
sensors = [
104-
BituoSensor(coordinator, host_ip, field, device_info.get("model", "Unknown Model"), device_info.get("fw_version", "Unknown"))
112+
BituoSensor(coordinator, host_ip, field, device_info.get("model", "Unknown Model"), device_info.get("fw_version", "Unknown"), device_info.get("manufacturer", "Unknown"), device_info.get("mcu_version", "Unknown"))
105113
for field in coordinator.data.keys()
106114
if field not in EXCLUDE_FIELDS
107115
]
108-
ota_sensor = BituoOTASensor(coordinator, host_ip, device_info.get("model", "Unknown Model"), device_info.get("fw_version", "Unknown"))
116+
ota_sensor = BituoOTASensor(coordinator, host_ip, device_info.get("model", "Unknown Model"), device_info.get("fw_version", "Unknown"), device_info.get("manufacturer", "Unknown"), device_info.get("mcu_version", "Unknown"))
109117
sensors.append(ota_sensor)
110118

111119
# Assign the OTA sensor to the coordinator
@@ -195,8 +203,10 @@ async def fetch_device_info(self):
195203
response.raise_for_status()
196204
data = response.json()
197205
return {
198-
"model": data.get("productModel") or data.get("ProductModel", "Unknown Model"),
199-
"fw_version": data.get("FWVersion") or data.get("fwVersion", "Unknown"),
206+
"model": data.get("ProductModel", "Unknown Model"),
207+
"fw_version": data.get("FWVersion", "Unknown"),
208+
"manufacturer": data.get("Manufactor", "Unknown"),
209+
"mcu_version": data.get("MCUVersion", "Unknown"),
200210
}
201211
except Exception as err:
202212
raise UpdateFailed(f"Error fetching device info: {err}")
@@ -229,19 +239,20 @@ async def _schedule_ota_update_checks(self):
229239
class BituoSensor(CoordinatorEntity, SensorEntity):
230240
"""Representation of a Sensor."""
231241

232-
def __init__(self, coordinator, host_ip, field, model, fw_version):
242+
def __init__(self, coordinator, host_ip, field, model, fw_version, manufacturer, mcu_version):
233243
"""Initialize the sensor."""
234244
super().__init__(coordinator)
235245
self._field = field
236246
self._attr_name = self.format_field_name(field)
237247
self._attr_unique_id = f"{host_ip}_{field}"
248+
self.entity_id = f"sensor.{host_ip.replace('.', '_')}_{self.format_field_entity_id(field)}"
238249
self._attr_device_info = DeviceInfo(
239250
identifiers={(DOMAIN, host_ip)},
240251
name=f"{model} - {host_ip}",
241-
manufacturer="BituoTechnik",
252+
manufacturer=manufacturer,
242253
model=model,
243-
sw_version=fw_version,
244-
configuration_url=f"http://{host_ip}" # 新增的配置 URL
254+
sw_version=f"S{fw_version}_M{self.format_version(mcu_version)}",
255+
configuration_url=f"http://{host_ip}" # embed URL
245256
)
246257
self._host_ip = host_ip
247258
self._native_unit_of_measurement = self.get_initial_unit_of_measurement()
@@ -299,6 +310,31 @@ def format_field_name(field):
299310
formatted_name = ''.join([' ' + char if char.isupper() else char for char in field]).title().strip()
300311
formatted_name = formatted_name.replace("X", " X").replace("Y", " Y").replace("Z", " Z")
301312
return formatted_name
313+
314+
@staticmethod
315+
def format_field_entity_id(field):
316+
"""Format field name to be more suitable for unique_id."""
317+
formatted_name = ''.join(['_' + char.lower() if char.isupper() else char for char in field])
318+
formatted_name = formatted_name.replace("x", "_x").replace("y", "_y").replace("z", "_z")
319+
return formatted_name.strip('_')
320+
321+
@staticmethod
322+
def format_version(version):
323+
if version.lower() == "unknown":
324+
return version
325+
parts = version.split('.')
326+
formatted_parts = []
327+
for part in parts:
328+
if part.strip(): # 检查部分是否为空
329+
try:
330+
formatted_parts.append(str(int(part)))
331+
except ValueError:
332+
formatted_parts.append('unknown')
333+
else:
334+
formatted_parts.append('unknown') # 如果部分为空,设置为 'unknown'
335+
336+
formatted_version = '.'.join(formatted_parts)
337+
return formatted_version
302338

303339
@property
304340
def native_value(self):
@@ -318,23 +354,42 @@ def device_class(self):
318354
class BituoOTASensor(CoordinatorEntity, SensorEntity):
319355
"""Representation of an OTA status sensor."""
320356

321-
def __init__(self, coordinator, host_ip, model, fw_version):
357+
def __init__(self, coordinator, host_ip, model, fw_version, manufacturer, mcu_version):
322358
"""Initialize the OTA status sensor."""
323359
super().__init__(coordinator)
324360
self._attr_name = "OTA Status"
325361
self._attr_unique_id = f"{host_ip}_ota_status"
326362
self._attr_device_info = DeviceInfo(
327363
identifiers={(DOMAIN, host_ip)},
328364
name=f"{model} - {host_ip}",
329-
manufacturer="BituoTechnik",
365+
manufacturer=manufacturer,
330366
model=model,
331-
sw_version=fw_version,
367+
sw_version=f"S{fw_version}_M{self.format_version(mcu_version)}",
368+
configuration_url=f"http://{host_ip}" # embed URL
332369
)
333370
self._host_ip = host_ip
334-
self._attr_state = "Unknown"
371+
self._attr_state = "unknown"
335372
coordinator.ota_entity = self # 存储自身的引用到协调器中
336373
self._attr_icon = "mdi:update"
337374
self.entity_id = f"sensor.{self._attr_unique_id.replace('.', '_')}"
375+
376+
@staticmethod
377+
def format_version(version):
378+
if version.lower() == "unknown":
379+
return version
380+
parts = version.split('.')
381+
formatted_parts = []
382+
for part in parts:
383+
if part.strip(): # 检查部分是否为空
384+
try:
385+
formatted_parts.append(str(int(part)))
386+
except ValueError:
387+
formatted_parts.append('unknown')
388+
else:
389+
formatted_parts.append('unknown') # 如果部分为空,设置为 'unknown'
390+
391+
formatted_version = '.'.join(formatted_parts)
392+
return formatted_version
338393

339394
@property
340395
def native_value(self):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"devices": {"192.168.3.76": {"scan_interval": 5}, "192.168.3.143": {"scan_interval": 10}, "192.168.3.58": {"scan_interval": 10}}}
1+
{"devices": {}}

0 commit comments

Comments
 (0)