diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..2113d89 --- /dev/null +++ b/.bandit @@ -0,0 +1,3 @@ +assert_used: + skips: + - src/nwp500/cli/rich_output.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a383012..0341451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,25 @@ jobs: - name: Run tox lint run: tox -e lint + security: + name: Security Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install tox + + - name: Run bandit + run: tox -e bandit + test: name: Test on Python ${{ matrix.python-version }} runs-on: ubuntu-latest diff --git a/.hypothesis/constants/05044b0e70ef42ab b/.hypothesis/constants/05044b0e70ef42ab new file mode 100644 index 0000000..813a1ee --- /dev/null +++ b/.hypothesis/constants/05044b0e70ef42ab @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/unit_system.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'MIT', 'metric', 'unit_system', 'us_customary'] \ No newline at end of file diff --git a/.hypothesis/constants/13db13a7b2178134 b/.hypothesis/constants/13db13a7b2178134 new file mode 100644 index 0000000..d1c91a4 --- /dev/null +++ b/.hypothesis/constants/13db13a7b2178134 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/utils.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'MIT'] \ No newline at end of file diff --git a/.hypothesis/constants/1b534f211836d036 b/.hypothesis/constants/1b534f211836d036 new file mode 100644 index 0000000..dfdb5a4 --- /dev/null +++ b/.hypothesis/constants/1b534f211836d036 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/enums.py +# hypothesis_version: 6.151.5 + +[257, 326, 407, 445, 480, 481, 513, 515, 517, 593, 594, 595, 596, 598, 615, 781, 798, 799, 901, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 920, 940, 990, 16777217, 16777219, 16777222, 16777225, 16777226, 33554433, 33554434, 33554437, 33554438, 33554439, 33554440, 33554441, 33554442, 33554443, 33554444, 33554445, 33554446, 33554447, 33554451, 33554455, 33554464, 33554466, 33554467, 33554468, 33554469, 33554470, 33554471, 33554472, 33554473, 33554474, 33554475, 33554476, '0.5°C', '1°C', '3 Stage', '50 gallons', '65 gallons', '80 gallons', 'Abnormal DIP Switch', 'Abnormal EEPROM', 'Abnormal Feedback', 'Abnormal Panel Key', 'Abnormal Power Meter', 'Advance Load Up', 'Always', 'Button', 'C', 'Commercial', 'Customer Peak Event', 'Dry Fire', 'Electric', 'Energy Saver', 'Heat Element', 'Heat Pump', 'Heat Pump Active', 'High Demand', 'Hybrid Boost', 'Hybrid Efficiency', 'Load Up', 'Mid Peak', 'No Error', 'Normal Operation', 'Not Applied', 'OFF', 'Off Peak', 'On Peak', 'Power Off', 'R', 'Relay Fault', 'Replacement Needed', 'Residential', 'Run Normal', 'Schedule', 'Shed', 'Standby', 'Temperature', 'Unknown', 'Vacation', 'Water Leak Detected'] \ No newline at end of file diff --git a/.hypothesis/constants/22cdb7ce598358e5 b/.hypothesis/constants/22cdb7ce598358e5 new file mode 100644 index 0000000..77e3851 --- /dev/null +++ b/.hypothesis/constants/22cdb7ce598358e5 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/diagnostics.py +# hypothesis_version: 6.151.5 + +[0.0, 1000, '-', '1', '11+', '2-5', '6-10', '=', 'Emmanuel Levijarvi', 'MIT', 'aws_error_counts', 'code', 'connected', 'inf', 'metrics', 'name', 'recent_connections', 'recent_drops', 'sample_durations', 'timestamp', 'total_sessions'] \ No newline at end of file diff --git a/.hypothesis/constants/30373286342e9c2d b/.hypothesis/constants/30373286342e9c2d new file mode 100644 index 0000000..a3db196 --- /dev/null +++ b/.hypothesis/constants/30373286342e9c2d @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt_events.py +# hypothesis_version: 6.151.5 + +['CurrentOperationMode', 'DeviceFeature', 'DeviceStatus', 'ErrorCode', '_', 'connection_resumed', 'error_cleared', 'error_detected', 'feature_received', 'heating_started', 'heating_stopped', 'mode_changed', 'power_changed', 'status_received', 'temperature_changed'] \ No newline at end of file diff --git a/.hypothesis/constants/351307d9f52e5712 b/.hypothesis/constants/351307d9f52e5712 new file mode 100644 index 0000000..4e168f8 --- /dev/null +++ b/.hypothesis/constants/351307d9f52e5712 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/client.py +# hypothesis_version: 6.151.5 + +[30.0, 300.0, 'Already connected', 'Emmanuel Levijarvi', 'MIT', 'Not connected', 'connected', 'connection failure', 'connection_resumed', 'name', 'resumed'] \ No newline at end of file diff --git a/.hypothesis/constants/38e5a0ee4c20c2fa b/.hypothesis/constants/38e5a0ee4c20c2fa new file mode 100644 index 0000000..799f638 --- /dev/null +++ b/.hypothesis/constants/38e5a0ee4c20c2fa @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/encoding.py +# hypothesis_version: 6.151.5 + +[150, 'Friday', 'Monday', 'Saturday', 'Sunday', 'Thursday', 'Tuesday', 'Wednesday', 'day_index', 'decimalPoint', 'decimal_point', 'enable', 'enabled', 'endHour', 'endMinute', 'end_hour', 'end_minute', 'hour', 'min', 'minute', 'mode', 'mode_id', 'month', 'param', 'priceMax', 'priceMin', 'season', 'startHour', 'startMinute', 'start_hour', 'start_minute', 'temperature', 'week', 'weekday'] \ No newline at end of file diff --git a/.hypothesis/constants/3cd888b2fda940ce b/.hypothesis/constants/3cd888b2fda940ce new file mode 100644 index 0000000..e49b057 --- /dev/null +++ b/.hypothesis/constants/3cd888b2fda940ce @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/reconnection.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'MIT', 'deep', 'quick', 'reconnection_failed'] \ No newline at end of file diff --git a/.hypothesis/constants/3e41f6e2dac7d711 b/.hypothesis/constants/3e41f6e2dac7d711 new file mode 100644 index 0000000..1a2b4cf --- /dev/null +++ b/.hypothesis/constants/3e41f6e2dac7d711 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/converters.py +# hypothesis_version: 6.151.5 + +[0.0, 0.264172, 10.0, 'Celsius', 'Fahrenheit', 'div_10', 'enum_validator', 'metric', 'mul_10', 'str_enum_validator', 'temp_formula_type', 'temperatureType', 'temperature_type', 'volume_to_preferred'] \ No newline at end of file diff --git a/.hypothesis/constants/43960b0ace50254c b/.hypothesis/constants/43960b0ace50254c new file mode 100644 index 0000000..06ac08d --- /dev/null +++ b/.hypothesis/constants/43960b0ace50254c @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/venv/bin/pytest +# hypothesis_version: 6.151.5 + +['__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/47db797441adc9da b/.hypothesis/constants/47db797441adc9da new file mode 100644 index 0000000..d2339be --- /dev/null +++ b/.hypothesis/constants/47db797441adc9da @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/topic_builder.py +# hypothesis_version: 6.151.5 + +['ctrl'] \ No newline at end of file diff --git a/.hypothesis/constants/523206edf9bd1235 b/.hypothesis/constants/523206edf9bd1235 new file mode 100644 index 0000000..1430a33 --- /dev/null +++ b/.hypothesis/constants/523206edf9bd1235 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/periodic.py +# hypothesis_version: 6.151.5 + +[300.0, 'DEVICE_ID_REDACTED', 'Emmanuel Levijarvi', 'name', 's'] \ No newline at end of file diff --git a/.hypothesis/constants/67b0a8ccf18bf5d2 b/.hypothesis/constants/67b0a8ccf18bf5d2 new file mode 100644 index 0000000..ae885fc --- /dev/null +++ b/.hypothesis/constants/67b0a8ccf18bf5d2 @@ -0,0 +1,4 @@ +# file: /usr/lib/python3.13/sitecustomize.py +# hypothesis_version: 6.151.5 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/74e6edbaadef1e50 b/.hypothesis/constants/74e6edbaadef1e50 new file mode 100644 index 0000000..e3ee4a3 --- /dev/null +++ b/.hypothesis/constants/74e6edbaadef1e50 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/events.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'MIT'] \ No newline at end of file diff --git a/.hypothesis/constants/767fac7418bf21ee b/.hypothesis/constants/767fac7418bf21ee new file mode 100644 index 0000000..003f527 --- /dev/null +++ b/.hypothesis/constants/767fac7418bf21ee @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/device_info_cache.py +# hypothesis_version: 6.151.5 + +['Device info cached', 'DeviceFeature', 'DeviceFeature | None', 'Emmanuel Levijarvi', 'cached_at', 'device_count', 'devices', 'expires_at', 'is_expired', 'mac'] \ No newline at end of file diff --git a/.hypothesis/constants/7b14ae8dd875b280 b/.hypothesis/constants/7b14ae8dd875b280 new file mode 100644 index 0000000..392872d --- /dev/null +++ b/.hypothesis/constants/7b14ae8dd875b280 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/temperature.py +# hypothesis_version: 6.151.5 + +[2.0, 10.0] \ No newline at end of file diff --git a/.hypothesis/constants/7d39af6d962efa3c b/.hypothesis/constants/7d39af6d962efa3c new file mode 100644 index 0000000..72de7f5 --- /dev/null +++ b/.hypothesis/constants/7d39af6d962efa3c @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/command_queue.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'MIT'] \ No newline at end of file diff --git a/.hypothesis/constants/89a580be14782398 b/.hypothesis/constants/89a580be14782398 new file mode 100644 index 0000000..0e3aa82 --- /dev/null +++ b/.hypothesis/constants/89a580be14782398 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/auth.py +# hypothesis_version: 6.151.5 + +[200, 401, 3600, '/', 'AuthTokens', 'Content-Type', 'Emmanuel Levijarvi', 'MIT', 'NavienAuthClient', 'SUCCESS', 'Token expires in: %s', 'User-Agent', 'UserInfo', 'accessKeyId', 'accessToken', 'access_key_id', 'access_token', 'application/json', 'authenticate', 'authorization', 'before', 'code', 'data', 'idToken', 'id_token', 'ignore', 'invalid', 'json', 'legal', 'metric', 'msg', 'password', 'refreshToken', 'refresh_access_token', 'refresh_token', 'secretKey', 'secret_key', 'sessionToken', 'session_token', 'token', 'tokens', 'unauthorized', 'us_customary', 'userId', 'userInfo'] \ No newline at end of file diff --git a/.hypothesis/constants/8f1254dc32667f86 b/.hypothesis/constants/8f1254dc32667f86 new file mode 100644 index 0000000..a35cac6 --- /dev/null +++ b/.hypothesis/constants/8f1254dc32667f86 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/control.py +# hypothesis_version: 6.151.5 + +[5.0, 365, '+00:00', 'Emmanuel Levijarvi', 'MqttDeviceInfoCache', 'Z', 'additionalValue', 'air-filter-reset', 'anti-leg-off', 'anti-leg-on', 'app-connection', 'clientID', 'command', 'ctrl', 'ctrl/rsv/rd', 'ctrl/tou/rd', 'days', 'deviceType', 'device_info', 'dhw-mode', 'dhw-temperature', 'dhw_use', 'dr-off', 'dr-on', 'goout-day', 'holiday_use', 'macAddress', 'mode', 'period_days', 'periods', 'power-off', 'power-on', 'power_use', 'protocolVersion', 'recirc-hotbtn', 'recirc-mode', 'recirculation_use', 'request', 'requestTopic', 'reservation-mode', 'responseTopic', 'rsv/rd', 'sessionID', 'st', 'st/did', 'st/rsv/rd', 'temperature', 'timestamp', 'tou-off', 'tou-on', 'tou/rd', 'vacation_days'] \ No newline at end of file diff --git a/.hypothesis/constants/9e0fd89dbf206808 b/.hypothesis/constants/9e0fd89dbf206808 new file mode 100644 index 0000000..8877f5f --- /dev/null +++ b/.hypothesis/constants/9e0fd89dbf206808 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/__init__.py +# hypothesis_version: 6.151.5 + +['ConnectionDropEvent', 'ConnectionEvent', 'MqttConnectionConfig', 'MqttMetrics', 'NavienMqttClient', 'PeriodicRequestType'] \ No newline at end of file diff --git a/.hypothesis/constants/9eab8fc57863e41e b/.hypothesis/constants/9eab8fc57863e41e new file mode 100644 index 0000000..c0ed3a3 --- /dev/null +++ b/.hypothesis/constants/9eab8fc57863e41e @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/subscriptions.py +# hypothesis_version: 6.151.5 + +['#', 'Emmanuel Levijarvi', 'Heating started', 'Heating stopped', 'error_cleared', 'error_detected', 'feature', 'feature_received', 'heating_started', 'heating_stopped', 'metric', 'mode_changed', 'power_changed', 'response', 'status', 'status_received', 'temperature_changed', 'utf-8', '°C', '°F'] \ No newline at end of file diff --git a/.hypothesis/constants/a3f229c898281d19 b/.hypothesis/constants/a3f229c898281d19 new file mode 100644 index 0000000..02efcd7 --- /dev/null +++ b/.hypothesis/constants/a3f229c898281d19 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/utils.py +# hypothesis_version: 6.151.5 + +[1.0, 2.0, 120.0, 100, 256, 512, 1200, '#', '([0-9A-Fa-f]{12})', '+', '......', '/', '', '', 'Emmanuel Levijarvi', 'MIT', 'REDACTED', '[0-9A-Fa-f]{8,}', '\\1REDACTED', '\\b[0-9a-fA-F]{12}\\b', 'access_key_id', 'auth', 'clientID', 'clientId', 'client_id', 'device_info', 'device_status', 'email', 'macAddress', 'mac_address', 'navilink-', 'password', 'pushToken', 'push_token', 'secret_access_key', 'secret_key', 'sessionID', 'sessionToken', 'session_token', 'token'] \ No newline at end of file diff --git a/.hypothesis/constants/b00985cd3c10a1f8 b/.hypothesis/constants/b00985cd3c10a1f8 new file mode 100644 index 0000000..7638f16 --- /dev/null +++ b/.hypothesis/constants/b00985cd3c10a1f8 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/models.py +# hypothesis_version: 6.151.5 + +[0.0, 43.0, 65.0, 100.0, ' GPM', ' L', ' LPM', ' gal', ' °C', ' °F', '%', '...', 'Current fan RPM', 'Current state number', 'Fan PWM value', 'GPM', 'RPM', 'Target fan RPM', 'Unknown', 'Vacation day setting', 'W', 'Wh', 'before', 'clientID', 'days', 'device_class', 'device_info', 'energy', 'flow_rate', 'gal', 'h', 'heTime', 'heUsage', 'hours', 'hpTime', 'hpUsage', 'ignore', 'interval', 'json_schema_extra', 'metric', 'mixingValveUse', 'mode', 'power', 'python', 'sessionID', 'temperature', 'touInfo', 'unit_of_measurement', 'water', '°F'] \ No newline at end of file diff --git a/.hypothesis/constants/b294530e090203ba b/.hypothesis/constants/b294530e090203ba new file mode 100644 index 0000000..f1e3487 --- /dev/null +++ b/.hypothesis/constants/b294530e090203ba @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/.tox/default/bin/pytest +# hypothesis_version: 6.151.5 + +['.exe', '__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/b5232deb51bddc9a b/.hypothesis/constants/b5232deb51bddc9a new file mode 100644 index 0000000..716dc25 --- /dev/null +++ b/.hypothesis/constants/b5232deb51bddc9a @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/__init__.py +# hypothesis_version: 6.151.5 + +['APIError', 'AuthTokens', 'AuthenticationError', 'CommandCode', 'ConnectionDropEvent', 'ConnectionEvent', 'CurrentOperationMode', 'DREvent', 'Device', 'DeviceError', 'DeviceFeature', 'DeviceInfo', 'DeviceNotFoundError', 'DeviceOfflineError', 'DeviceOperationError', 'DeviceStatus', 'DhwOperationSetting', 'EnergyUsageDay', 'EnergyUsageResponse', 'EnergyUsageTotal', 'ErrorCode', 'EventEmitter', 'EventListener', 'FilterChange', 'FirmwareInfo', 'HeatSource', 'InstallType', 'Location', 'MonthlyEnergyData', 'MqttClientEvents', 'MqttCommand', 'MqttConnectionConfig', 'MqttConnectionError', 'MqttCredentialsError', 'MqttDeviceInfoCache', 'MqttError', 'MqttMetrics', 'MqttPublishError', 'MqttRequest', 'NavienAPIClient', 'NavienAuthClient', 'NavienMqttClient', 'Nwp500Error', 'OnOffFlag', 'Operation', 'PeriodicRequestType', 'RangeValidationError', 'RecirculationMode', 'TOUInfo', 'TOUSchedule', 'TempFormulaType', 'TemperatureType', 'TokenExpiredError', 'TokenRefreshError', 'TouRateType', 'TouWeekType', 'UnitType', 'UserInfo', 'ValidationError', 'VolumeCode', '__version__', 'authenticate', 'build_tou_period', 'decode_price', 'decode_week_bitfield', 'encode_price', 'encode_week_bitfield', 'get_unit_system', 'log_performance', 'nwp500-python', 'refresh_access_token', 'requires_capability', 'reset_unit_system', 'set_unit_system', 'unknown'] \ No newline at end of file diff --git a/.hypothesis/constants/bd643e9276a6173b b/.hypothesis/constants/bd643e9276a6173b new file mode 100644 index 0000000..b7ddc3b --- /dev/null +++ b/.hypothesis/constants/bd643e9276a6173b @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/api_client.py +# hypothesis_version: 6.151.5 + +[200, 401, '/', '/device/info', '/device/list', '/device/tou', '1.0.0', '3.8+', 'Emmanuel Levijarvi', 'GET', 'MIT', 'NavienAPIClient', 'O', 'POST', 'Python', 'Python Client', '_session', 'additionalValue', 'appVersion', 'code', 'controllerId', 'count', 'data', 'firmwares', 'macAddress', 'metric', 'modelName', 'msg', 'offset', 'os', 'osVersion', 'pushToken', 'us_customary', 'userId', 'userType'] \ No newline at end of file diff --git a/.hypothesis/constants/c7a31c6dc0068cbd b/.hypothesis/constants/c7a31c6dc0068cbd new file mode 100644 index 0000000..9c1bad7 --- /dev/null +++ b/.hypothesis/constants/c7a31c6dc0068cbd @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/.venv/bin/pytest +# hypothesis_version: 6.151.5 + +['.exe', '__main__'] \ No newline at end of file diff --git a/.hypothesis/constants/ca0b70aa93535c1f b/.hypothesis/constants/ca0b70aa93535c1f new file mode 100644 index 0000000..8b48a82 --- /dev/null +++ b/.hypothesis/constants/ca0b70aa93535c1f @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/config.py +# hypothesis_version: 6.151.5 + +['/auth/refresh', '/user/sign-in', 'us-east-1'] \ No newline at end of file diff --git a/.hypothesis/constants/cd91fc95eade0457 b/.hypothesis/constants/cd91fc95eade0457 new file mode 100644 index 0000000..e26e9c1 --- /dev/null +++ b/.hypothesis/constants/cd91fc95eade0457 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/mqtt/connection.py +# hypothesis_version: 6.151.5 + +['Already connected', 'Emmanuel Levijarvi', 'MIT', 'MqttConnectionConfig', 'NavienAuthClient', 'Not connected', 'name', 'utf-8'] \ No newline at end of file diff --git a/.hypothesis/constants/d0ea67d716f83a90 b/.hypothesis/constants/d0ea67d716f83a90 new file mode 100644 index 0000000..e5e78f7 --- /dev/null +++ b/.hypothesis/constants/d0ea67d716f83a90 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/device_capabilities.py +# hypothesis_version: 6.151.5 + +[', ', 'DeviceFeature', 'Emmanuel Levijarvi', 'dhw_use', 'holiday_use', 'power_use', 'recirculation_use'] \ No newline at end of file diff --git a/.hypothesis/constants/dae78f4d97fdd43b b/.hypothesis/constants/dae78f4d97fdd43b new file mode 100644 index 0000000..66ceb5c --- /dev/null +++ b/.hypothesis/constants/dae78f4d97fdd43b @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/field_factory.py +# hypothesis_version: 6.151.5 + +['W', 'dBm', 'device_class', 'energy', 'energy_field', 'json_schema_extra', 'kWh', 'power', 'power_field', 'signal_strength', 'temperature', 'temperature_field', 'unit_of_measurement', '°F'] \ No newline at end of file diff --git a/.hypothesis/constants/dcfcf6e03af72a1c b/.hypothesis/constants/dcfcf6e03af72a1c new file mode 100644 index 0000000..208b51b --- /dev/null +++ b/.hypothesis/constants/dcfcf6e03af72a1c @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/factory.py +# hypothesis_version: 6.151.5 + +[] \ No newline at end of file diff --git a/.hypothesis/constants/dfc00db7cf73d63c b/.hypothesis/constants/dfc00db7cf73d63c new file mode 100644 index 0000000..7445afd --- /dev/null +++ b/.hypothesis/constants/dfc00db7cf73d63c @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/exceptions.py +# hypothesis_version: 6.151.5 + +['(retriable)', 'Emmanuel Levijarvi', 'MIT', 'details', 'error_code', 'error_type', 'message', 'retriable'] \ No newline at end of file diff --git a/.hypothesis/constants/e4b0f73ee49546d6 b/.hypothesis/constants/e4b0f73ee49546d6 new file mode 100644 index 0000000..d63695f --- /dev/null +++ b/.hypothesis/constants/e4b0f73ee49546d6 @@ -0,0 +1,4 @@ +# file: /home/emmanuel/Projects/nwp500-python/src/nwp500/command_decorators.py +# hypothesis_version: 6.151.5 + +['Emmanuel Levijarvi', 'F'] \ No newline at end of file diff --git a/.hypothesis/examples/04e6b3400353b141/b04e33f1dccbdab0 b/.hypothesis/examples/04e6b3400353b141/b04e33f1dccbdab0 new file mode 100644 index 0000000..1a20869 --- /dev/null +++ b/.hypothesis/examples/04e6b3400353b141/b04e33f1dccbdab0 @@ -0,0 +1,2 @@ +[#¾…©sÒ^Þ·HÜü`ÊŒv2l¬°úŒ¼Õ4Šä‰ +¤>4›¢»¾¨œ»Ù/Á \ No newline at end of file diff --git a/.hypothesis/examples/04e6b3400353b141/e2ebc17191359b9b b/.hypothesis/examples/04e6b3400353b141/e2ebc17191359b9b new file mode 100644 index 0000000..a632899 --- /dev/null +++ b/.hypothesis/examples/04e6b3400353b141/e2ebc17191359b9b @@ -0,0 +1,2 @@ +[#¾…©sÒ^Þ·HÜü`ÊŒv2l¬°úŒ¼Õ4Šä‰ +¤>4›¢»¾¨œ»Ù/Á.secondary \ No newline at end of file diff --git a/.hypothesis/examples/b04e33f1dccbdab0/3e1706876c4d4240 b/.hypothesis/examples/b04e33f1dccbdab0/3e1706876c4d4240 new file mode 100644 index 0000000..b275563 Binary files /dev/null and b/.hypothesis/examples/b04e33f1dccbdab0/3e1706876c4d4240 differ diff --git a/.hypothesis/examples/e2ebc17191359b9b/06e8e5454ebe80d4 b/.hypothesis/examples/e2ebc17191359b9b/06e8e5454ebe80d4 new file mode 100644 index 0000000..015dfb7 Binary files /dev/null and b/.hypothesis/examples/e2ebc17191359b9b/06e8e5454ebe80d4 differ diff --git a/.hypothesis/examples/e2ebc17191359b9b/6fe30320955145d3 b/.hypothesis/examples/e2ebc17191359b9b/6fe30320955145d3 new file mode 100644 index 0000000..d58cb21 Binary files /dev/null and b/.hypothesis/examples/e2ebc17191359b9b/6fe30320955145d3 differ diff --git a/.hypothesis/examples/e2ebc17191359b9b/97091e146329faa6 b/.hypothesis/examples/e2ebc17191359b9b/97091e146329faa6 new file mode 100644 index 0000000..7def1f7 Binary files /dev/null and b/.hypothesis/examples/e2ebc17191359b9b/97091e146329faa6 differ diff --git a/setup.cfg b/setup.cfg index 256c2f1..89dee02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,6 +77,7 @@ testing = pytest pytest-cov pytest-asyncio + hypothesis # Development tools dev = diff --git a/tests/test_mqtt_hypothesis.py b/tests/test_mqtt_hypothesis.py new file mode 100644 index 0000000..f660781 --- /dev/null +++ b/tests/test_mqtt_hypothesis.py @@ -0,0 +1,180 @@ +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from nwp500.enums import TemperatureType +from nwp500.models import DeviceStatus + +# Base payload matching required fields in DeviceStatus +BASE_PAYLOAD = { + "command": 0, + "outsideTemperature": 0.0, + "specialFunctionStatus": 0, + "errorCode": 0, + "subErrorCode": 0, + "smartDiagnostic": 0, + "faultStatus1": 0, + "faultStatus2": 0, + "wifiRssi": 0, + "dhwChargePer": 0.0, + "drEventStatus": 0, + "vacationDaySetting": 0, + "vacationDayElapsed": 0, + "antiLegionellaPeriod": 0, + "programReservationType": 0, + "tempFormulaType": 0, + "currentStatenum": 0, + "targetFanRpm": 0, + "currentFanRpm": 0, + "fanPwm": 0, + "mixingRate": 0.0, + "eevStep": 0, + "airFilterAlarmPeriod": 0, + "airFilterAlarmElapsed": 0, + "cumulatedOpTimeEvaFan": 0, + "cumulatedDhwFlowRate": 0.0, + "touStatus": 0, + "drOverrideStatus": 0, + "touOverrideStatus": 0, + "totalEnergyCapacity": 0.0, + "availableEnergyCapacity": 0.0, + "recircOperationMode": 0, + "recircPumpOperationStatus": 0, + "recircHotBtnReady": 0, + "recircOperationReason": 0, + "recircErrorStatus": 0, + "currentInstPower": 0.0, + "didReload": 0, + "operationBusy": 0, + "freezeProtectionUse": 0, + "dhwUse": 0, + "dhwUseSustained": 0, + "dhwOperationBusy": 0, + "programReservationUse": 0, + "ecoUse": 0, + "compUse": 0, + "eevUse": 0, + "evaFanUse": 0, + "shutOffValveUse": 0, + "conOvrSensorUse": 0, + "wtrOvrSensorUse": 0, + "antiLegionellaUse": 0, + "antiLegionellaOperationBusy": 0, + "errorBuzzerUse": 0, + "currentHeatUse": 0, + "heatUpperUse": 0, + "heatLowerUse": 0, + "scaldUse": 0, + "airFilterAlarmUse": 0, + "recircOperationBusy": 0, + "recircReservationUse": 0, + "dhwTemperature": 0, + "dhwTemperatureSetting": 0, + "dhwTargetTemperatureSetting": 0, + "freezeProtectionTemperature": 0, + "dhwTemperature2": 0, + "hpUpperOnTempSetting": 0, + "hpUpperOffTempSetting": 0, + "hpLowerOnTempSetting": 0, + "hpLowerOffTempSetting": 0, + "heUpperOnTempSetting": 0, + "heUpperOffTempSetting": 0, + "heLowerOnTempSetting": 0, + "heLowerOffTempSetting": 0, + "heatMinOpTemperature": 0, + "recircTempSetting": 0, + "recircTemperature": 0, + "recircFaucetTemperature": 0, + "currentInletTemperature": 0, + "currentDhwFlowRate": 0, + "hpUpperOnDiffTempSetting": 0, + "hpUpperOffDiffTempSetting": 0, + "hpLowerOnDiffTempSetting": 0, + "hpLowerOffDiffTempSetting": 0, + "heUpperOnDiffTempSetting": 0, + "heUpperOffDiffTempSetting": 0, + "heLowerOnTDiffempSetting": 0, + "heLowerOffDiffTempSetting": 0, + "recircDhwFlowRate": 0, + "tankUpperTemperature": 0, + "tankLowerTemperature": 0, + "dischargeTemperature": 0, + "suctionTemperature": 0, + "evaporatorTemperature": 0, + "ambientTemperature": 0, + "targetSuperHeat": 0, + "currentSuperHeat": 0, + "operationMode": 0, + "dhwOperationSetting": 3, + "temperatureType": 2, + "freezeProtectionTempMin": 43.0, + "freezeProtectionTempMax": 65.0, +} + + +@given( + temperature_type_int=st.sampled_from([1, 2]), + dhw_temp_raw=st.integers(min_value=0, max_value=200), # 0 to 100°C + tank_temp_raw=st.integers(min_value=0, max_value=1000), # 0 to 100°C (deci) + flow_rate_raw=st.integers(min_value=0, max_value=500), # 0 to 50 LPM +) +def test_device_status_fuzzing( + temperature_type_int, dhw_temp_raw, tank_temp_raw, flow_rate_raw +): + """ + Fuzz test parsing of DeviceStatus with varying temperature types and values. + """ + # Create a copy of the base payload + payload = BASE_PAYLOAD.copy() + + # Update with fuzzed values + payload["temperatureType"] = temperature_type_int + payload["dhwTemperature"] = dhw_temp_raw + payload["tankUpperTemperature"] = tank_temp_raw + payload["currentDhwFlowRate"] = flow_rate_raw + + # Parse the model + status = DeviceStatus.model_validate(payload) + + # Assertions based on temperature type + is_celsius = temperature_type_int == 1 # 1=Celsius, 2=Fahrenheit + + # 1. Check Temperature Type parsing + if is_celsius: + assert status.temperature_type == TemperatureType.CELSIUS + else: + assert status.temperature_type == TemperatureType.FAHRENHEIT + + # 2. Check DHW Temperature (HalfCelsius) + # Raw value is half-celsius. + celsius_val = dhw_temp_raw / 2.0 + if is_celsius: + assert status.dhw_temperature == pytest.approx(celsius_val) + else: + fahrenheit_val = (celsius_val * 9 / 5) + 32 + assert status.dhw_temperature == pytest.approx(fahrenheit_val) + + # 3. Check Tank Temperature (DeciCelsius) + # Raw value is deci-celsius. + tank_celsius_val = tank_temp_raw / 10.0 + if is_celsius: + assert status.tank_upper_temperature == pytest.approx(tank_celsius_val) + else: + tank_fahrenheit_val = (tank_celsius_val * 9 / 5) + 32 + # Note: DeciCelsiusToPreferred calls DeciCelsius(raw).to_preferred( + # is_celsius) + # to_preferred -> to_fahrenheit -> standard conversion + # We need to match the exact logic if there's rounding involved, but + # simple math should match approx. + assert status.tank_upper_temperature == pytest.approx( + tank_fahrenheit_val + ) + + # 4. Check Flow Rate (LPM * 10) + lpm_val = flow_rate_raw / 10.0 + if is_celsius: + assert status.current_dhw_flow_rate == pytest.approx(lpm_val) + else: + # Imperial: LPM to GPM (rounded to 2 decimals in the converter) + gpm_val = lpm_val * 0.264172 + assert status.current_dhw_flow_rate == pytest.approx(gpm_val, abs=0.01) diff --git a/tox.ini b/tox.ini index 9047447..550546e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ [tox] minversion = 3.24 -envlist = default,lint +envlist = default,lint,bandit isolated_build = True @@ -41,6 +41,13 @@ commands = pyright src/nwp500 {posargs} +[testenv:bandit] +description = Run bandit to check for security issues +skip_install = True +deps = bandit +commands = bandit -c .bandit -r src/ {posargs} + + [testenv:format] description = Format code with ruff skip_install = True