Add required battery_size to devices, filter install panels by size

This commit is contained in:
2026-04-19 14:48:55 -05:00
parent aabe273172
commit 3e75bb3ab4
8 changed files with 238 additions and 77 deletions
+58 -22
View File
@@ -634,8 +634,10 @@ def create_app(config_object="config"):
devices = db.query(Device).order_by(Device.name).all() devices = db.query(Device).order_by(Device.name).all()
device_types = sorted({d.device_type for d in devices if d.device_type}) device_types = sorted({d.device_type for d in devices if d.device_type})
device_locations = sorted({d.location for d in devices if d.location}) device_locations = sorted({d.location for d in devices if d.location})
device_battery_sizes = sorted({d.battery_size for d in devices if d.battery_size})
return render_template("device_list.html", devices=devices, return render_template("device_list.html", devices=devices,
device_types=device_types, device_locations=device_locations) device_types=device_types, device_locations=device_locations,
device_battery_sizes=device_battery_sizes)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Devices — add # Devices — add
@@ -646,19 +648,31 @@ def create_app(config_object="config"):
all_devices = db.query(Device).all() all_devices = db.query(Device).all()
device_types = sorted({d.device_type for d in all_devices if d.device_type}) device_types = sorted({d.device_type for d in all_devices if d.device_type})
device_locations = sorted({d.location for d in all_devices if d.location}) device_locations = sorted({d.location for d in all_devices if d.location})
device_battery_sizes = sorted({d.battery_size for d in all_devices if d.battery_size})
if request.method == "POST": if request.method == "POST":
name = request.form.get("name", "").strip() name = request.form.get("name", "").strip()
slots_raw = request.form.get("battery_slots", "1").strip() slots_raw = request.form.get("battery_slots", "1").strip()
notes = request.form.get("notes", "").strip() or None notes = request.form.get("notes", "").strip() or None
device_type = request.form.get("device_type", "").strip() or None device_type = request.form.get("device_type", "").strip() or None
battery_size = request.form.get("battery_size", "").strip() or None
location = request.form.get("location", "").strip() or None location = request.form.get("location", "").strip() or None
if not name: if not name:
flash("Device name is required.", "error") flash("Device name is required.", "error")
return render_template("device_add.html", return render_template("device_add.html",
device_types=device_types, device_types=device_types,
device_locations=device_locations), 400 device_locations=device_locations,
device_battery_sizes=device_battery_sizes), 400
if not battery_size:
flash("Battery size is required.", "error")
return render_template("device_add.html",
device_types=device_types,
device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
form_name=name, form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400
try: try:
slots = int(slots_raw) slots = int(slots_raw)
@@ -669,6 +683,7 @@ def create_app(config_object="config"):
return render_template("device_add.html", return render_template("device_add.html",
device_types=device_types, device_types=device_types,
device_locations=device_locations, device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
form_name=name, form_notes=notes or "", form_name=name, form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400 form_device_type=request.form.get("device_type", "")), 400
@@ -677,19 +692,22 @@ def create_app(config_object="config"):
return render_template("device_add.html", return render_template("device_add.html",
device_types=device_types, device_types=device_types,
device_locations=device_locations, device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
form_name=name, form_slots=slots, form_name=name, form_slots=slots,
form_notes=notes or "", form_notes=notes or "",
form_device_type=request.form.get("device_type", "")), 400 form_device_type=request.form.get("device_type", "")), 400
device = Device(name=name, battery_slots=slots, notes=notes, device = Device(name=name, battery_slots=slots, notes=notes,
device_type=device_type, location=location) device_type=device_type, battery_size=battery_size,
location=location)
db.add(device) db.add(device)
db.commit() db.commit()
flash(f"Device '{name}' added.", "success") flash(f"Device '{name}' added.", "success")
return redirect(url_for("device_list")) return redirect(url_for("device_list"))
return render_template("device_add.html", device_types=device_types, return render_template("device_add.html", device_types=device_types,
device_locations=device_locations) device_locations=device_locations,
device_battery_sizes=device_battery_sizes)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Devices — detail # Devices — detail
@@ -700,12 +718,22 @@ def create_app(config_object="config"):
device = db.get(Device, device_id) device = db.get(Device, device_id)
if device is None: if device is None:
abort(404) abort(404)
brands = [r[0] for r in db.query(Battery.brand).distinct().order_by(Battery.brand).all()] all_devices = db.query(Device).all()
available_batteries = (db.query(Battery) brands_q = db.query(Battery.brand).filter(Battery.status == "available")
.filter_by(status="available") if device.battery_size:
.order_by(Battery.label).all()) brands_q = brands_q.filter(
device_types = sorted({d.device_type for d in db.query(Device).all() if d.device_type}) (Battery.size == device.battery_size) | (Battery.size == None)
device_locations = sorted({d.location for d in db.query(Device).all() if d.location}) )
brands = [r[0] for r in brands_q.distinct().order_by(Battery.brand).all()]
avail_q = db.query(Battery).filter_by(status="available")
if device.battery_size:
avail_q = avail_q.filter(
(Battery.size == device.battery_size) | (Battery.size == None)
)
available_batteries = avail_q.order_by(Battery.label).all()
device_types = sorted({d.device_type for d in all_devices if d.device_type})
device_locations = sorted({d.location for d in all_devices if d.location})
device_battery_sizes = sorted({d.battery_size for d in all_devices if d.battery_size})
ha_live_pct = None ha_live_pct = None
if ha_client.enabled and device.ha_entity_id: if ha_client.enabled and device.ha_entity_id:
ha_live_pct = ha_client.get_state(device.ha_entity_id, timeout=1) ha_live_pct = ha_client.get_state(device.ha_entity_id, timeout=1)
@@ -727,6 +755,7 @@ def create_app(config_object="config"):
available_batteries=available_batteries, available_batteries=available_batteries,
device_types=device_types, device_types=device_types,
device_locations=device_locations, device_locations=device_locations,
device_battery_sizes=device_battery_sizes,
ha_enabled=ha_client.enabled, ha_enabled=ha_client.enabled,
ha_live_pct=ha_live_pct) ha_live_pct=ha_live_pct)
@@ -763,6 +792,7 @@ def create_app(config_object="config"):
device.battery_slots = slots device.battery_slots = slots
device.notes = notes device.notes = notes
device.device_type = device_type device.device_type = device_type
device.battery_size = request.form.get("battery_size", "").strip() or None
device.location = request.form.get("location", "").strip() or None device.location = request.form.get("location", "").strip() or None
device.ha_entity_id = request.form.get("ha_entity_id", "").strip() or None device.ha_entity_id = request.form.get("ha_entity_id", "").strip() or None
db.commit() db.commit()
@@ -807,11 +837,12 @@ def create_app(config_object="config"):
# Validate availability before writing anything # Validate availability before writing anything
for brand, qty in pairs: for brand, qty in pairs:
available_count = ( avail_q = db.query(func.count(Battery.id)).filter_by(brand=brand, status="available")
db.query(func.count(Battery.id)) if device.battery_size:
.filter_by(brand=brand, status="available") avail_q = avail_q.filter(
.scalar() (Battery.size == device.battery_size) | (Battery.size == None)
) )
available_count = avail_q.scalar()
if available_count < qty: if available_count < qty:
flash( flash(
f"Need {qty} {brand}, but only {available_count} available.", f"Need {qty} {brand}, but only {available_count} available.",
@@ -822,13 +853,12 @@ def create_app(config_object="config"):
# All checks passed — perform installs # All checks passed — perform installs
total_installed = 0 total_installed = 0
for brand, qty in pairs: for brand, qty in pairs:
batch = ( batch_q = db.query(Battery).filter_by(brand=brand, status="available")
db.query(Battery) if device.battery_size:
.filter_by(brand=brand, status="available") batch_q = batch_q.filter(
.order_by(Battery.id) (Battery.size == device.battery_size) | (Battery.size == None)
.limit(qty) )
.all() batch = batch_q.order_by(Battery.id).limit(qty).all()
)
for b in batch: for b in batch:
b.status = "installed" b.status = "installed"
b.device_id = device.id b.device_id = device.id
@@ -870,6 +900,12 @@ def create_app(config_object="config"):
f"{', '.join(existing_brands)}. Mixing brands is not recommended.", f"{', '.join(existing_brands)}. Mixing brands is not recommended.",
"warning", "warning",
) )
if device.battery_size and battery.size and battery.size != device.battery_size:
flash(
f"Warning: {device.name} requires {device.battery_size} batteries, "
f"but {battery.label} is {battery.size}.",
"warning",
)
battery.status = "installed" battery.status = "installed"
battery.device_id = device.id battery.device_id = device.id
db.commit() db.commit()
+1
View File
@@ -13,6 +13,7 @@ class Device(Base):
name = Column(String(100), nullable=False, unique=True) name = Column(String(100), nullable=False, unique=True)
battery_slots = Column(Integer, nullable=False, default=1) battery_slots = Column(Integer, nullable=False, default=1)
device_type = Column(String(50), nullable=True) device_type = Column(String(50), nullable=True)
battery_size = Column(String(20), nullable=True) # AA, AAA, 9V, CR2032 …
location = Column(String(100), nullable=True) location = Column(String(100), nullable=True)
notes = Column(Text, nullable=True) notes = Column(Text, nullable=True)
ha_entity_id = Column(String(100), nullable=True) # e.g. "sensor.tv_remote_battery" ha_entity_id = Column(String(100), nullable=True) # e.g. "sensor.tv_remote_battery"
+21
View File
@@ -18,6 +18,27 @@
value="{{ form_slots|default(1) }}" min="1" required> value="{{ form_slots|default(1) }}" min="1" required>
</div> </div>
<div class="form-group">
<label for="battery-size-select">Battery Size <span class="text-danger">*</span></label>
{% set _preset_sizes = ['AA','AAA','C','D','9V','CR2032','CR2025','CR2016','18650','14500','16340','26650','LR44/AG13'] %}
{% set _cur_size = form_battery_size|default('') %}
<select id="battery-size-select" onchange="metaSelectChanged(this,'battery_size')">
<option value="">— select —</option>
{% for s in _preset_sizes %}
<option value="{{ s }}" {% if _cur_size == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
{% for s in device_battery_sizes|default([]) %}
{% if s not in _preset_sizes %}
<option value="{{ s }}" {% if _cur_size == s %}selected{% endif %}>{{ s }}</option>
{% endif %}
{% endfor %}
<option value="__new__" {% if _cur_size and _cur_size not in _preset_sizes %}selected{% endif %}>Other…</option>
</select>
<input type="text" id="battery_size" name="battery_size" value="{{ _cur_size }}"
placeholder="e.g. CR123A"
style="display:{% if _cur_size and _cur_size not in _preset_sizes %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div>
<div class="form-group"> <div class="form-group">
<label>Type</label> <label>Type</label>
{% set _preset_types = ['Remote Control','Game Controller','Flashlight','Lock','Sensor','Toy','Clock','Smoke Detector'] %} {% set _preset_types = ['Remote Control','Game Controller','Flashlight','Lock','Sensor','Toy','Clock','Smoke Detector'] %}
+39 -2
View File
@@ -39,6 +39,12 @@
<td style="border:none;">{{ device.device_type }}</td> <td style="border:none;">{{ device.device_type }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if device.battery_size %}
<tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Battery Size</td>
<td style="border:none;">{{ device.battery_size }}</td>
</tr>
{% endif %}
{% if device.has_mixed_brands() %} {% if device.has_mixed_brands() %}
<tr> <tr>
<td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Warning</td> <td style="padding:0.3rem 1rem 0.3rem 0;font-weight:600;color:#64748b;border:none;">Warning</td>
@@ -79,7 +85,7 @@
<div class="card"> <div class="card">
<h2>Install Batteries</h2> <h2>Install Batteries</h2>
{% set free_slots = device.battery_slots - device.installed_count() %} {% set free_slots = device.battery_slots - device.installed_count() %}
<p class="text-muted" style="margin-bottom:0.75rem;">{{ free_slots }} slot(s) free</p> <p class="text-muted" style="margin-bottom:0.75rem;">{{ free_slots }} slot(s) free{% if device.battery_size %} &mdash; showing {{ device.battery_size }} batteries only{% endif %}</p>
<form method="post" action="{{ url_for('device_install', device_id=device.id) }}"> <form method="post" action="{{ url_for('device_install', device_id=device.id) }}">
<div id="install-grid" style="display:grid;grid-template-columns:1fr auto;gap:0.4rem;max-width:400px;align-items:start;margin-bottom:0.5rem;"> <div id="install-grid" style="display:grid;grid-template-columns:1fr auto;gap:0.4rem;max-width:400px;align-items:start;margin-bottom:0.5rem;">
<span style="font-weight:600;font-size:0.85rem;color:#64748b;">Brand</span> <span style="font-weight:600;font-size:0.85rem;color:#64748b;">Brand</span>
@@ -115,6 +121,14 @@ function editTypeSelectChanged(sel) {
input.style.display = 'none'; input.value = sel.value; input.style.display = 'none'; input.value = sel.value;
} }
} }
function editSizeSelectChanged(sel) {
var input = document.getElementById('edit-battery-size');
if (sel.value === '__new__') {
input.style.display = ''; input.value = ''; input.focus();
} else {
input.style.display = 'none'; input.value = sel.value;
}
}
function editLocationSelectChanged(sel) { function editLocationSelectChanged(sel) {
var input = document.getElementById('edit-location'); var input = document.getElementById('edit-location');
if (sel.value === '__new__') { if (sel.value === '__new__') {
@@ -186,7 +200,7 @@ function addInstallRow() {
</div> </div>
<div class="card"> <div class="card">
<h2>Install Specific Battery</h2> <h2>Install Specific Battery{% if device.battery_size %} <small class="text-muted" style="font-weight:normal;font-size:0.8rem;">({{ device.battery_size }} only)</small>{% endif %}</h2>
{% if available_batteries %} {% if available_batteries %}
<form method="post" action="{{ url_for('device_install_one', device_id=device.id) }}"> <form method="post" action="{{ url_for('device_install_one', device_id=device.id) }}">
<div class="form-group"> <div class="form-group">
@@ -238,6 +252,29 @@ function addInstallRow() {
placeholder="Enter device type" placeholder="Enter device type"
style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;"> style="display:{% if device.device_type and device.device_type not in _preset_types %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div> </div>
<div class="form-group">
<label>Battery Size</label>
{% set _preset_sizes = ['AA','AAA','C','D','9V','CR2032','CR2025','CR2016','18650','14500','16340','26650','LR44/AG13'] %}
<select id="edit-battery-size-select" onchange="editSizeSelectChanged(this)">
<option value="">— none —</option>
{% for s in _preset_sizes %}
<option value="{{ s }}" {% if device.battery_size == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
{% for s in device_battery_sizes|default([]) %}
{% if s not in _preset_sizes %}
<option value="{{ s }}" {% if device.battery_size == s %}selected{% endif %}>{{ s }}</option>
{% endif %}
{% endfor %}
{% if device.battery_size and device.battery_size not in _preset_sizes and device.battery_size not in device_battery_sizes|default([]) %}
<option value="{{ device.battery_size }}" selected>{{ device.battery_size }}</option>
{% endif %}
<option value="__new__">Other…</option>
</select>
<input type="text" id="edit-battery-size" name="battery_size"
value="{{ device.battery_size or '' }}"
placeholder="e.g. CR123A"
style="display:{% if device.battery_size and device.battery_size not in _preset_sizes %}''{% else %}none{% endif %};margin-top:0.4rem;">
</div>
<div class="form-group"> <div class="form-group">
<label>Location</label> <label>Location</label>
<select id="edit-location-select" onchange="editLocationSelectChanged(this)"> <select id="edit-location-select" onchange="editLocationSelectChanged(this)">
+33 -18
View File
@@ -20,6 +20,13 @@
<option value="{{ loc }}">{{ loc }}</option> <option value="{{ loc }}">{{ loc }}</option>
{% endfor %} {% endfor %}
</select> </select>
<select id="filter-battery-size" onchange="applyDeviceFilters()"
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
<option value="">Any Size</option>
{% for sz in device_battery_sizes|default([]) %}
<option value="{{ sz }}">{{ sz }}</option>
{% endfor %}
</select>
<select id="filter-fill" onchange="applyDeviceFilters()" <select id="filter-fill" onchange="applyDeviceFilters()"
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;"> style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
<option value="">Any Fill</option> <option value="">Any Fill</option>
@@ -40,6 +47,7 @@
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Size</th>
<th>Location</th> <th>Location</th>
<th>Slots</th> <th>Slots</th>
<th>Installed</th> <th>Installed</th>
@@ -54,11 +62,13 @@
{% elif installed >= d.battery_slots %}{% set fill_state = 'full' %} {% elif installed >= d.battery_slots %}{% set fill_state = 'full' %}
{% else %}{% set fill_state = 'partial' %}{% endif %} {% else %}{% set fill_state = 'partial' %}{% endif %}
<tr data-type="{{ d.device_type or '' }}" <tr data-type="{{ d.device_type or '' }}"
data-battery-size="{{ d.battery_size or '' }}"
data-location="{{ d.location or '' }}" data-location="{{ d.location or '' }}"
data-fill="{{ fill_state }}" data-fill="{{ fill_state }}"
data-name="{{ d.name|lower }}"> data-name="{{ d.name|lower }}">
<td data-label="Device"><a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a></td> <td data-label="Device"><a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a></td>
<td data-label="Type">{{ d.device_type or '—' }}</td> <td data-label="Type">{{ d.device_type or '—' }}</td>
<td data-label="Size">{{ d.battery_size or '—' }}</td>
<td data-label="Location">{{ d.location or '—' }}</td> <td data-label="Location">{{ d.location or '—' }}</td>
<td data-label="Slots">{{ d.battery_slots }}</td> <td data-label="Slots">{{ d.battery_slots }}</td>
<td data-label="Installed"> <td data-label="Installed">
@@ -95,7 +105,7 @@
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="7" class="text-muted" style="text-align:center;padding:1rem;">No devices yet. <a href="{{ url_for('device_add') }}">Add one.</a></td></tr> <tr><td colspan="8" class="text-muted" style="text-align:center;padding:1rem;">No devices yet. <a href="{{ url_for('device_add') }}">Add one.</a></td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -106,27 +116,31 @@
<script> <script>
function applyDeviceFilters() { function applyDeviceFilters() {
var typeVal = document.getElementById('filter-type').value; var typeVal = document.getElementById('filter-type').value;
var locationVal = document.getElementById('filter-location').value; var batterySizeVal = document.getElementById('filter-battery-size').value;
var fillVal = document.getElementById('filter-fill').value; var locationVal = document.getElementById('filter-location').value;
var textVal = document.getElementById('filter-device-text').value.toLowerCase(); var fillVal = document.getElementById('filter-fill').value;
var rows = document.querySelectorAll('tbody tr[data-name]'); var textVal = document.getElementById('filter-device-text').value.toLowerCase();
var visible = 0; var rows = document.querySelectorAll('tbody tr[data-name]');
var visible = 0;
rows.forEach(function(row) { rows.forEach(function(row) {
var rowType = row.dataset.type || ''; var rowType = row.dataset.type || '';
var rowLocation = row.dataset.location || ''; var rowBatterySize = row.dataset.batterySize || '';
var rowFill = row.dataset.fill || ''; var rowLocation = row.dataset.location || '';
var rowName = row.dataset.name || ''; var rowFill = row.dataset.fill || '';
var show = (!typeVal || rowType === typeVal) && var rowName = row.dataset.name || '';
(!locationVal || rowLocation === locationVal) && var show = (!typeVal || rowType === typeVal) &&
(!fillVal || rowFill === fillVal) && (!batterySizeVal || rowBatterySize === batterySizeVal) &&
(!textVal || rowName.includes(textVal) || (!locationVal || rowLocation === locationVal) &&
rowType.toLowerCase().includes(textVal) || (!fillVal || rowFill === fillVal) &&
rowLocation.toLowerCase().includes(textVal)); (!textVal || rowName.includes(textVal) ||
rowType.toLowerCase().includes(textVal) ||
rowBatterySize.toLowerCase().includes(textVal) ||
rowLocation.toLowerCase().includes(textVal));
row.style.display = show ? '' : 'none'; row.style.display = show ? '' : 'none';
if (show) visible++; if (show) visible++;
}); });
var active = typeVal || locationVal || fillVal || textVal; var active = typeVal || batterySizeVal || locationVal || fillVal || textVal;
document.getElementById('device-filter-reset').style.display = active ? '' : 'none'; document.getElementById('device-filter-reset').style.display = active ? '' : 'none';
document.getElementById('device-filter-count').textContent = document.getElementById('device-filter-count').textContent =
active ? (visible + ' of ' + rows.length + ' shown') : ''; active ? (visible + ' of ' + rows.length + ' shown') : '';
@@ -134,6 +148,7 @@ function applyDeviceFilters() {
function resetDeviceFilters() { function resetDeviceFilters() {
document.getElementById('filter-type').value = ''; document.getElementById('filter-type').value = '';
document.getElementById('filter-battery-size').value = '';
document.getElementById('filter-location').value = ''; document.getElementById('filter-location').value = '';
document.getElementById('filter-fill').value = ''; document.getElementById('filter-fill').value = '';
document.getElementById('filter-device-text').value = ''; document.getElementById('filter-device-text').value = '';
+2 -2
View File
@@ -42,8 +42,8 @@ def seeded_client(app):
id=3 BrandX 002 (BrandX, retired) id=3 BrandX 002 (BrandX, retired)
""" """
with app.test_client() as c: with app.test_client() as c:
c.post("/device/add", data={"name": "Device A", "battery_slots": "2"}) c.post("/device/add", data={"name": "Device A", "battery_slots": "2", "battery_size": "AA"})
c.post("/device/add", data={"name": "Device B", "battery_slots": "1"}) c.post("/device/add", data={"name": "Device B", "battery_slots": "1", "battery_size": "AA"})
c.post("/battery/add", data={"brand": "BrandX", "count": "1"}) # id=1 c.post("/battery/add", data={"brand": "BrandX", "count": "1"}) # id=1
c.post("/battery/add", data={"brand": "BrandY", "count": "1"}) # id=2 c.post("/battery/add", data={"brand": "BrandY", "count": "1"}) # id=2
c.post("/battery/add", data={"brand": "BrandX", "count": "1"}) # id=3 c.post("/battery/add", data={"brand": "BrandX", "count": "1"}) # id=3
+72 -21
View File
@@ -235,7 +235,7 @@ def test_bulk_delete(client):
def test_bulk_unassign(client): def test_bulk_unassign(client):
client.post("/device/add", data={"name": "Gadget", "battery_slots": "2"}) client.post("/device/add", data={"name": "Gadget", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "TestBrand", "count": "2"}) client.post("/battery/add", data={"brand": "TestBrand", "count": "2"})
client.post("/device/1/install", data={"brand[]": "TestBrand", "qty[]": "2"}) client.post("/device/1/install", data={"brand[]": "TestBrand", "qty[]": "2"})
resp = client.post("/battery/bulk-action", resp = client.post("/battery/bulk-action",
@@ -269,7 +269,7 @@ def test_bulk_action_no_selection(client):
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_device_install_autoselects(client): def test_device_install_autoselects(client):
client.post("/device/add", data={"name": "Gadget", "battery_slots": "2"}) client.post("/device/add", data={"name": "Gadget", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "3"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "3"})
resp = client.post("/device/1/install", resp = client.post("/device/1/install",
data={"brand[]": "Eneloop", "qty[]": "2"}, data={"brand[]": "Eneloop", "qty[]": "2"},
@@ -281,7 +281,7 @@ def test_device_install_autoselects(client):
def test_device_install_mixed_brands(client): def test_device_install_mixed_brands(client):
client.post("/device/add", data={"name": "Remote", "battery_slots": "4"}) client.post("/device/add", data={"name": "Remote", "battery_slots": "4", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
client.post("/battery/add", data={"brand": "Energizer", "count": "2"}) client.post("/battery/add", data={"brand": "Energizer", "count": "2"})
resp = client.post("/device/1/install", resp = client.post("/device/1/install",
@@ -293,7 +293,7 @@ def test_device_install_mixed_brands(client):
def test_device_install_insufficient_batteries(client): def test_device_install_insufficient_batteries(client):
client.post("/device/add", data={"name": "Gadget", "battery_slots": "4"}) client.post("/device/add", data={"name": "Gadget", "battery_slots": "4", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/device/1/install", resp = client.post("/device/1/install",
data={"brand[]": "Eneloop", "qty[]": "2"}, data={"brand[]": "Eneloop", "qty[]": "2"},
@@ -303,7 +303,7 @@ def test_device_install_insufficient_batteries(client):
def test_device_install_over_capacity(client): def test_device_install_over_capacity(client):
client.post("/device/add", data={"name": "Gadget", "battery_slots": "1"}) client.post("/device/add", data={"name": "Gadget", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "3"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "3"})
resp = client.post("/device/1/install", resp = client.post("/device/1/install",
data={"brand[]": "Eneloop", "qty[]": "3"}, data={"brand[]": "Eneloop", "qty[]": "3"},
@@ -314,7 +314,7 @@ def test_device_install_over_capacity(client):
def test_bulk_install_device(client): def test_bulk_install_device(client):
"""Select multiple available batteries and install them into a device.""" """Select multiple available batteries and install them into a device."""
client.post("/device/add", data={"name": "Box", "battery_slots": "3"}) client.post("/device/add", data={"name": "Box", "battery_slots": "3", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
resp = client.post("/battery/bulk-action", resp = client.post("/battery/bulk-action",
data={"battery_ids": ["1", "2"], "action": "install_device", data={"battery_ids": ["1", "2"], "action": "install_device",
@@ -326,7 +326,7 @@ def test_bulk_install_device(client):
def test_bulk_install_device_over_capacity(client): def test_bulk_install_device_over_capacity(client):
"""Bulk install is blocked when device lacks free slots.""" """Bulk install is blocked when device lacks free slots."""
client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
resp = client.post("/battery/bulk-action", resp = client.post("/battery/bulk-action",
data={"battery_ids": ["1", "2"], "action": "install_device", data={"battery_ids": ["1", "2"], "action": "install_device",
@@ -338,8 +338,8 @@ def test_bulk_install_device_over_capacity(client):
def test_bulk_install_device_moves_installed_battery(client): def test_bulk_install_device_moves_installed_battery(client):
"""Bulk install moves a battery already installed in another device.""" """Bulk install moves a battery already installed in another device."""
client.post("/device/add", data={"name": "Box A", "battery_slots": "2"}) client.post("/device/add", data={"name": "Box A", "battery_slots": "2", "battery_size": "AA"})
client.post("/device/add", data={"name": "Box B", "battery_slots": "2"}) client.post("/device/add", data={"name": "Box B", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
client.post("/battery/1/assign", data={"device_id": "1"}) client.post("/battery/1/assign", data={"device_id": "1"})
resp = client.post("/battery/bulk-action", resp = client.post("/battery/bulk-action",
@@ -357,7 +357,7 @@ def test_bulk_install_device_moves_installed_battery(client):
def test_dashboard_quick_assign(client): def test_dashboard_quick_assign(client):
"""Quick-assign from dashboard (battery_assign POST) succeeds.""" """Quick-assign from dashboard (battery_assign POST) succeeds."""
client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/battery/1/assign", data={"device_id": "1"}, resp = client.post("/battery/1/assign", data={"device_id": "1"},
follow_redirects=True) follow_redirects=True)
@@ -367,7 +367,7 @@ def test_dashboard_quick_assign(client):
def test_dashboard_quick_assign_full_device_blocked(client): def test_dashboard_quick_assign_full_device_blocked(client):
"""Quick-assign from dashboard to a full device is blocked.""" """Quick-assign from dashboard to a full device is blocked."""
client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
client.post("/battery/1/assign", data={"device_id": "1"}) # fills the slot client.post("/battery/1/assign", data={"device_id": "1"}) # fills the slot
resp = client.post("/battery/2/assign", data={"device_id": "1"}, resp = client.post("/battery/2/assign", data={"device_id": "1"},
@@ -382,7 +382,7 @@ def test_dashboard_quick_assign_full_device_blocked(client):
def test_install_one_specific_battery(client): def test_install_one_specific_battery(client):
"""device_install_one installs a chosen battery.""" """device_install_one installs a chosen battery."""
client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) client.post("/device/add", data={"name": "Box", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "1"})
resp = client.post("/device/1/install-one", data={"battery_id": "1"}, resp = client.post("/device/1/install-one", data={"battery_id": "1"},
follow_redirects=True) follow_redirects=True)
@@ -392,7 +392,7 @@ def test_install_one_specific_battery(client):
def test_install_one_over_capacity_blocked(client): def test_install_one_over_capacity_blocked(client):
"""device_install_one to a full device is blocked.""" """device_install_one to a full device is blocked."""
client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) client.post("/device/add", data={"name": "Box", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) client.post("/battery/add", data={"brand": "Eneloop", "count": "2"})
client.post("/device/1/install-one", data={"battery_id": "1"}) # fills slot client.post("/device/1/install-one", data={"battery_id": "1"}) # fills slot
resp = client.post("/device/1/install-one", data={"battery_id": "2"}, resp = client.post("/device/1/install-one", data={"battery_id": "2"},
@@ -418,7 +418,7 @@ def test_device_list(seeded_client):
def test_add_device(client): def test_add_device(client):
resp = client.post("/device/add", resp = client.post("/device/add",
data={"name": "My Gadget", "battery_slots": "3"}, data={"name": "My Gadget", "battery_slots": "3", "battery_size": "AA"},
follow_redirects=True) follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
assert b"My Gadget" in resp.data assert b"My Gadget" in resp.data
@@ -426,7 +426,7 @@ def test_add_device(client):
def test_add_device_duplicate_name(seeded_client): def test_add_device_duplicate_name(seeded_client):
resp = seeded_client.post("/device/add", resp = seeded_client.post("/device/add",
data={"name": "Device A", "battery_slots": "2"}) data={"name": "Device A", "battery_slots": "2", "battery_size": "AA"})
assert resp.status_code == 400 assert resp.status_code == 400
assert b"already exists" in resp.data assert b"already exists" in resp.data
@@ -476,21 +476,22 @@ def test_delete_device_removed(seeded_client):
def test_add_device_with_type(client): def test_add_device_with_type(client):
resp = client.post("/device/add", resp = client.post("/device/add",
data={"name": "TV Remote", "battery_slots": "2", "device_type": "Remote Control"}, data={"name": "TV Remote", "battery_slots": "2", "battery_size": "AA",
"device_type": "Remote Control"},
follow_redirects=True) follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
assert b"TV Remote" in resp.data assert b"TV Remote" in resp.data
def test_device_detail_shows_type(client): def test_device_detail_shows_type(client):
client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"}) client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "battery_size": "AA", "device_type": "Flashlight"})
resp = client.get("/device/1") resp = client.get("/device/1")
assert resp.status_code == 200 assert resp.status_code == 200
assert b"Flashlight" in resp.data assert b"Flashlight" in resp.data
def test_edit_device_type(client): def test_edit_device_type(client):
client.post("/device/add", data={"name": "Torch", "battery_slots": "1"}) client.post("/device/add", data={"name": "Torch", "battery_slots": "1", "battery_size": "AA"})
resp = client.post("/device/1/edit", resp = client.post("/device/1/edit",
data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"}, data={"name": "Torch", "battery_slots": "1", "device_type": "Flashlight"},
follow_redirects=True) follow_redirects=True)
@@ -500,7 +501,8 @@ def test_edit_device_type(client):
def test_add_device_with_location(client): def test_add_device_with_location(client):
resp = client.post("/device/add", resp = client.post("/device/add",
data={"name": "Bedroom Remote", "battery_slots": "2", "location": "Bedroom"}, data={"name": "Bedroom Remote", "battery_slots": "2", "battery_size": "AA",
"location": "Bedroom"},
follow_redirects=True) follow_redirects=True)
assert resp.status_code == 200 assert resp.status_code == 200
assert b"Bedroom Remote" in resp.data assert b"Bedroom Remote" in resp.data
@@ -509,7 +511,7 @@ def test_add_device_with_location(client):
def test_edit_device_location(client): def test_edit_device_location(client):
client.post("/device/add", data={"name": "Living Room Clock", "battery_slots": "1"}) client.post("/device/add", data={"name": "Living Room Clock", "battery_slots": "1", "battery_size": "AA"})
resp = client.post("/device/1/edit", resp = client.post("/device/1/edit",
data={"name": "Living Room Clock", "battery_slots": "1", "location": "Living Room"}, data={"name": "Living Room Clock", "battery_slots": "1", "location": "Living Room"},
follow_redirects=True) follow_redirects=True)
@@ -545,7 +547,7 @@ def test_delete_capacity_test(client):
def test_add_install_delete_battery(client): def test_add_install_delete_battery(client):
client.post("/device/add", data={"name": "Gadget", "battery_slots": "1"}) client.post("/device/add", data={"name": "Gadget", "battery_slots": "1", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "AcmeBrand", "count": "1"}) client.post("/battery/add", data={"brand": "AcmeBrand", "count": "1"})
resp = client.post("/device/1/install", resp = client.post("/device/1/install",
data={"brand[]": "AcmeBrand", "qty[]": "1"}, data={"brand[]": "AcmeBrand", "qty[]": "1"},
@@ -553,3 +555,52 @@ def test_add_install_delete_battery(client):
assert b"Gadget" in resp.data assert b"Gadget" in resp.data
client.post("/battery/1/delete") client.post("/battery/1/delete")
assert client.get("/battery/1").status_code == 404 assert client.get("/battery/1").status_code == 404
# ------------------------------------------------------------------ #
# Device — battery_size
# ------------------------------------------------------------------ #
def test_add_device_requires_battery_size(client):
"""POST without battery_size returns 400."""
resp = client.post("/device/add", data={"name": "No Size Device", "battery_slots": "2"})
assert resp.status_code == 400
assert b"Battery size is required" in resp.data
def test_add_device_with_battery_size(client):
"""Device with battery_size shows it on detail page."""
resp = client.post("/device/add",
data={"name": "AA Remote", "battery_slots": "2", "battery_size": "AA"},
follow_redirects=True)
assert resp.status_code == 200
resp2 = client.get("/device/1")
assert b"AA" in resp2.data
def test_install_one_filters_by_size(client):
"""Single-battery install dropdown excludes batteries with wrong size."""
client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) # id=1
client.post("/battery/1/edit-details", data={"size": "AA"})
client.post("/battery/add", data={"brand": "Energizer", "count": "1"}) # id=2
client.post("/battery/2/edit-details", data={"size": "AAA"})
resp = client.get("/device/1")
assert resp.status_code == 200
# AA battery should appear in the dropdown, AAA should not
assert b"Eneloop 001" in resp.data
assert b"Energizer 001" not in resp.data
def test_bulk_install_filters_by_size(client):
"""Bulk install with a brand that only has wrong-size batteries fails with 0 available."""
client.post("/device/add", data={"name": "AA Device", "battery_slots": "2", "battery_size": "AA"})
client.post("/battery/add", data={"brand": "Energizer", "count": "2"})
# Set both batteries to AAA (explicitly incompatible)
client.post("/battery/1/edit-details", data={"size": "AAA"})
client.post("/battery/2/edit-details", data={"size": "AAA"})
resp = client.post("/device/1/install",
data={"brand[]": "Energizer", "qty[]": "1"},
follow_redirects=True)
assert resp.status_code == 200
assert b"only 0 available" in resp.data
+12 -12
View File
@@ -62,7 +62,7 @@ def test_ha_disabled_dashboard_no_ha_column(client):
def test_ha_disabled_device_no_ha_field(client): def test_ha_disabled_device_no_ha_field(client):
client.post("/device/add", data={"name": "Dev A", "battery_slots": "1"}) client.post("/device/add", data={"name": "Dev A", "battery_slots": "1", "battery_size": "AA"})
resp = client.get("/device/1") resp = client.get("/device/1")
assert resp.status_code == 200 assert resp.status_code == 200
assert b"ha_entity_id" not in resp.data assert b"ha_entity_id" not in resp.data
@@ -153,7 +153,7 @@ def _make_session_factory(ha_app):
def test_poll_updates_installed_batteries(ha_app, ha_client_f): def test_poll_updates_installed_batteries(ha_app, ha_client_f):
"""Installed battery in a device with ha_entity_id gets battery_percentage updated.""" """Installed battery in a device with ha_entity_id gets battery_percentage updated."""
ha_client_f.post("/device/add", data={"name": "Dev A", "battery_slots": "2"}) ha_client_f.post("/device/add", data={"name": "Dev A", "battery_slots": "2", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
# Assign battery to device # Assign battery to device
ha_client_f.post("/battery/1/assign", data={"device_id": "1"}) ha_client_f.post("/battery/1/assign", data={"device_id": "1"})
@@ -184,7 +184,7 @@ def test_poll_updates_installed_batteries(ha_app, ha_client_f):
def test_poll_skips_uninstalled_batteries(ha_app, ha_client_f): def test_poll_skips_uninstalled_batteries(ha_app, ha_client_f):
"""Batteries that are available (not installed) are not updated by the poller.""" """Batteries that are available (not installed) are not updated by the poller."""
ha_client_f.post("/device/add", data={"name": "Dev B", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev B", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
# Set entity on device but do NOT install the battery # Set entity on device but do NOT install the battery
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
@@ -211,7 +211,7 @@ def test_poll_skips_uninstalled_batteries(ha_app, ha_client_f):
def test_poll_skips_devices_without_entity_id(ha_app, ha_client_f): def test_poll_skips_devices_without_entity_id(ha_app, ha_client_f):
"""Devices with no ha_entity_id must never reach get_state.""" """Devices with no ha_entity_id must never reach get_state."""
ha_client_f.post("/device/add", data={"name": "Dev C", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev C", "battery_slots": "1", "battery_size": "AA"})
from ha_client import HomeAssistantClient from ha_client import HomeAssistantClient
from ha_poller import HaPoller from ha_poller import HaPoller
@@ -228,7 +228,7 @@ def test_poll_skips_devices_without_entity_id(ha_app, ha_client_f):
def test_poll_handles_api_error_gracefully(ha_app, ha_client_f): def test_poll_handles_api_error_gracefully(ha_app, ha_client_f):
"""When get_state returns None, no exception is raised and percentage stays None.""" """When get_state returns None, no exception is raised and percentage stays None."""
ha_client_f.post("/device/add", data={"name": "Dev D", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev D", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
ha_client_f.post("/battery/1/assign", data={"device_id": "1"}) ha_client_f.post("/battery/1/assign", data={"device_id": "1"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
@@ -343,14 +343,14 @@ def test_retired_battery_excluded_from_needs_attention_section(ha_app, ha_client
def test_device_detail_shows_ha_field(ha_client_f): def test_device_detail_shows_ha_field(ha_client_f):
ha_client_f.post("/device/add", data={"name": "Dev E", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev E", "battery_slots": "1", "battery_size": "AA"})
resp = ha_client_f.get("/device/1") resp = ha_client_f.get("/device/1")
assert resp.status_code == 200 assert resp.status_code == 200
assert b"ha_entity_id" in resp.data assert b"ha_entity_id" in resp.data
def test_edit_device_ha_entity_id_saves(ha_client_f): def test_edit_device_ha_entity_id_saves(ha_client_f):
ha_client_f.post("/device/add", data={"name": "Dev F", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev F", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
"name": "Dev F", "battery_slots": "1", "ha_entity_id": "sensor.my_remote" "name": "Dev F", "battery_slots": "1", "ha_entity_id": "sensor.my_remote"
}) })
@@ -359,7 +359,7 @@ def test_edit_device_ha_entity_id_saves(ha_client_f):
def test_edit_device_ha_entity_id_clear(ha_client_f): def test_edit_device_ha_entity_id_clear(ha_client_f):
ha_client_f.post("/device/add", data={"name": "Dev G", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev G", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
"name": "Dev G", "battery_slots": "1", "ha_entity_id": "sensor.foo" "name": "Dev G", "battery_slots": "1", "ha_entity_id": "sensor.foo"
}) })
@@ -415,7 +415,7 @@ def test_manual_battery_percentage_edit(ha_client_f):
def test_poll_skips_update_when_percentage_unchanged(ha_app, ha_client_f): def test_poll_skips_update_when_percentage_unchanged(ha_app, ha_client_f):
"""No write and no pct_log entry when the polled value matches the stored value.""" """No write and no pct_log entry when the polled value matches the stored value."""
ha_client_f.post("/device/add", data={"name": "Dev NoCh", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev NoCh", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
ha_client_f.post("/battery/1/assign", data={"device_id": "1"}) ha_client_f.post("/battery/1/assign", data={"device_id": "1"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
@@ -446,7 +446,7 @@ def test_poll_skips_update_when_percentage_unchanged(ha_app, ha_client_f):
def test_poll_creates_pct_log_on_change(ha_app, ha_client_f): def test_poll_creates_pct_log_on_change(ha_app, ha_client_f):
"""A pct_log entry with source='poll' is written when the value changes.""" """A pct_log entry with source='poll' is written when the value changes."""
ha_client_f.post("/device/add", data={"name": "Dev Chg", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev Chg", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
ha_client_f.post("/battery/1/assign", data={"device_id": "1"}) ha_client_f.post("/battery/1/assign", data={"device_id": "1"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
@@ -519,7 +519,7 @@ def test_manual_edit_no_log_when_unchanged(ha_client_f):
def test_device_detail_shows_live_pct(ha_app, ha_client_f): def test_device_detail_shows_live_pct(ha_app, ha_client_f):
"""Opening a device page fetches live % from HA and displays it.""" """Opening a device page fetches live % from HA and displays it."""
ha_client_f.post("/device/add", data={"name": "Dev Live", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev Live", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={
"name": "Dev Live", "battery_slots": "1", "ha_entity_id": "sensor.live_test" "name": "Dev Live", "battery_slots": "1", "ha_entity_id": "sensor.live_test"
}) })
@@ -537,7 +537,7 @@ def test_device_detail_shows_live_pct(ha_app, ha_client_f):
def test_device_detail_updates_battery_on_load(ha_app, ha_client_f): def test_device_detail_updates_battery_on_load(ha_app, ha_client_f):
"""Battery percentage is updated (with pct_log) when device page is loaded and value changed.""" """Battery percentage is updated (with pct_log) when device page is loaded and value changed."""
ha_client_f.post("/device/add", data={"name": "Dev Update", "battery_slots": "1"}) ha_client_f.post("/device/add", data={"name": "Dev Update", "battery_slots": "1", "battery_size": "AA"})
ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"}) ha_client_f.post("/battery/add", data={"brand": "X", "count": "1"})
ha_client_f.post("/battery/1/assign", data={"device_id": "1"}) ha_client_f.post("/battery/1/assign", data={"device_id": "1"})
ha_client_f.post("/device/1/edit", data={ ha_client_f.post("/device/1/edit", data={