|
2 | 2 |
|
3 | 3 | import pytest |
4 | 4 | from homeassistant.core import HomeAssistant |
| 5 | +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError |
5 | 6 |
|
6 | | -from custom_components.solisconnect.const import DOMAIN |
| 7 | +from custom_components.solisconnect.const import CONTROLLER, DOMAIN |
| 8 | +from custom_components.solisconnect.helpers import get_controller |
7 | 9 |
|
8 | 10 |
|
9 | 11 | @pytest.fixture |
10 | 12 | def mock_controller(): |
11 | 13 | controller = MagicMock() |
12 | 14 | controller.host = "1.2.3.4" |
13 | 15 | controller.device_id = 1 |
| 16 | + controller.modbus = None # simulates a cloud-only/bare controller; hubs set this to their sub-controller |
14 | 17 | controller.async_write_holding_register = AsyncMock(return_value=None) |
15 | 18 | return controller |
16 | 19 |
|
@@ -53,6 +56,105 @@ async def test_service_write_holding_register_no_host(hass: HomeAssistant, mock_ |
53 | 56 | mock_controller.async_write_holding_register.assert_called_with(123, 456) |
54 | 57 |
|
55 | 58 |
|
| 59 | +@pytest.mark.asyncio |
| 60 | +async def test_service_write_holding_register_no_target_with_multiple_controllers_raises(hass: HomeAssistant, mock_controller): |
| 61 | + """Bug #12: silently broadcasting a write to every configured inverter is dangerous; require a target.""" |
| 62 | + other_controller = MagicMock() |
| 63 | + other_controller.host = "5.6.7.8" |
| 64 | + other_controller.device_id = 1 |
| 65 | + other_controller.async_write_holding_register = AsyncMock(return_value=None) |
| 66 | + |
| 67 | + hass.data.setdefault(DOMAIN, {}) |
| 68 | + hass.data[DOMAIN][CONTROLLER] = {"1.2.3.4_1": mock_controller, "5.6.7.8_1": other_controller} |
| 69 | + |
| 70 | + from custom_components.solisconnect import async_setup |
| 71 | + |
| 72 | + await async_setup(hass, {}) |
| 73 | + |
| 74 | + with pytest.raises(ServiceValidationError, match="Multiple"): |
| 75 | + await hass.services.async_call(DOMAIN, "solis_write_holding_register", {"address": 123, "value": 456}, blocking=True) |
| 76 | + |
| 77 | + mock_controller.async_write_holding_register.assert_not_called() |
| 78 | + other_controller.async_write_holding_register.assert_not_called() |
| 79 | + |
| 80 | + |
| 81 | +@pytest.mark.asyncio |
| 82 | +async def test_service_write_holding_register_unmatched_target_raises(hass: HomeAssistant, mock_controller): |
| 83 | + hass.data.setdefault(DOMAIN, {}) |
| 84 | + hass.data[DOMAIN][CONTROLLER] = {"1.2.3.4_1": mock_controller} |
| 85 | + |
| 86 | + from custom_components.solisconnect import async_setup |
| 87 | + |
| 88 | + await async_setup(hass, {}) |
| 89 | + |
| 90 | + with pytest.raises(ServiceValidationError, match="No configured"): |
| 91 | + await hass.services.async_call(DOMAIN, "solis_write_holding_register", {"address": 123, "value": 456, "host": "9.9.9.9"}, blocking=True) |
| 92 | + |
| 93 | + mock_controller.async_write_holding_register.assert_not_called() |
| 94 | + |
| 95 | + |
| 96 | +@pytest.mark.asyncio |
| 97 | +async def test_service_write_holding_register_matches_by_serial(hass: HomeAssistant, mock_controller): |
| 98 | + """Serial matching must work even while the hub's own `host` property is protocol-dependent.""" |
| 99 | + mock_controller.host = "soliscloud" # simulates a dual-protocol hub currently active on cloud |
| 100 | + mock_controller.serial_number = "SN12345" |
| 101 | + |
| 102 | + hass.data.setdefault(DOMAIN, {}) |
| 103 | + hass.data[DOMAIN][CONTROLLER] = {"1.2.3.4_1": mock_controller} |
| 104 | + |
| 105 | + from custom_components.solisconnect import async_setup |
| 106 | + |
| 107 | + await async_setup(hass, {}) |
| 108 | + |
| 109 | + await hass.services.async_call(DOMAIN, "solis_write_holding_register", {"address": 123, "value": 456, "serial": "SN12345"}, blocking=True) |
| 110 | + |
| 111 | + mock_controller.async_write_holding_register.assert_called_with(123, 456) |
| 112 | + |
| 113 | + |
| 114 | +@pytest.mark.asyncio |
| 115 | +async def test_service_write_holding_register_write_failure_raises(hass: HomeAssistant, mock_controller): |
| 116 | + """Bug #12: a write failure must surface as a service error, not vanish into a fire-and-forget task.""" |
| 117 | + mock_controller.async_write_holding_register = AsyncMock(side_effect=RuntimeError("boom")) |
| 118 | + |
| 119 | + hass.data.setdefault(DOMAIN, {}) |
| 120 | + hass.data[DOMAIN][CONTROLLER] = {"1.2.3.4_1": mock_controller} |
| 121 | + |
| 122 | + from custom_components.solisconnect import async_setup |
| 123 | + |
| 124 | + await async_setup(hass, {}) |
| 125 | + |
| 126 | + with pytest.raises(HomeAssistantError, match="boom"): |
| 127 | + await hass.services.async_call(DOMAIN, "solis_write_holding_register", {"address": 123, "value": 456, "host": "1.2.3.4"}, blocking=True) |
| 128 | + |
| 129 | + |
| 130 | +def test_get_controller_matches_host_via_modbus_sub_controller_when_hub_is_cloud_active(): |
| 131 | + """Bug #12: a hub's `.host` delegates to the active protocol (e.g. "soliscloud"); host matching |
| 132 | + must read the hub's `.modbus` sub-controller instead, so it still works after failover.""" |
| 133 | + hub = MagicMock() |
| 134 | + hub.host = "soliscloud" # currently active protocol is cloud |
| 135 | + hub.device_id = 1 # cloud controller's hardcoded device_id, not the real Modbus slave |
| 136 | + hub.modbus.host = "1.2.3.4" # the hub's real, protocol-independent Modbus link |
| 137 | + hub.modbus.device_id = 3 |
| 138 | + |
| 139 | + hass = MagicMock() |
| 140 | + hass.data = {DOMAIN: {CONTROLLER: {"entry1": hub}}} |
| 141 | + |
| 142 | + assert get_controller(hass, host="1.2.3.4", slave=3) is hub |
| 143 | + assert get_controller(hass, host="soliscloud", slave=1) is None # the active-protocol host must not match |
| 144 | + |
| 145 | + |
| 146 | +def test_get_controller_matches_serial_regardless_of_host(): |
| 147 | + hub = MagicMock() |
| 148 | + hub.serial_number = "SN999" |
| 149 | + hub.host = "soliscloud" |
| 150 | + |
| 151 | + hass = MagicMock() |
| 152 | + hass.data = {DOMAIN: {CONTROLLER: {"entry1": hub}}} |
| 153 | + |
| 154 | + assert get_controller(hass, serial="SN999") is hub |
| 155 | + assert get_controller(hass, serial="SNOTHER") is None |
| 156 | + |
| 157 | + |
56 | 158 | @pytest.mark.asyncio |
57 | 159 | async def test_service_set_time(hass: HomeAssistant): |
58 | 160 | """Test solis_write_time service.""" |
|
0 commit comments