├── test └── .gitkeep ├── CHANGELOG.md ├── .pubignore ├── README.md ├── doc ├── AHA-HTTP-Interface.pdf └── AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf ├── .vscode ├── settings.json ├── tasks.json └── dart.code-snippets ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── analysis_options.yaml ├── lib ├── src │ └── api │ │ ├── aha │ │ ├── models │ │ │ ├── blind_state.dart │ │ │ ├── switch_duration.dart │ │ │ ├── switch_action.dart │ │ │ ├── switch_state.dart │ │ │ ├── function_bit_mask.dart │ │ │ ├── temp.dart │ │ │ ├── name.dart │ │ │ ├── avm_button.dart │ │ │ ├── hs_defaults.dart │ │ │ ├── simple_on_off.dart │ │ │ ├── button.dart │ │ │ ├── hs_color.dart │ │ │ ├── temperature_defaults.dart │ │ │ ├── level_control.dart │ │ │ ├── next_change.dart │ │ │ ├── power_meter.dart │ │ │ ├── group_info.dart │ │ │ ├── stats_group.dart │ │ │ ├── power.dart │ │ │ ├── energy.dart │ │ │ ├── voltage.dart │ │ │ ├── switch.dart │ │ │ ├── color_defaults.dart │ │ │ ├── device_stats.dart │ │ │ ├── subscription_state.dart │ │ │ ├── color.dart │ │ │ ├── temperature.dart │ │ │ ├── device_list.dart │ │ │ ├── level.dart │ │ │ ├── alert.dart │ │ │ ├── percentage.dart │ │ │ ├── color_control.dart │ │ │ ├── hkr_temperature.dart │ │ │ ├── hkr.dart │ │ │ ├── timestamp.dart │ │ │ ├── stats.dart │ │ │ └── device.dart │ │ └── aha_service.dart │ │ ├── login │ │ ├── user_credentials.dart │ │ ├── login_service.dart │ │ ├── models │ │ │ ├── session_info.dart │ │ │ ├── user.dart │ │ │ ├── sid.dart │ │ │ └── right.dart │ │ ├── authentication_exception.dart │ │ ├── login_info.dart │ │ └── login_manager.dart │ │ ├── util │ │ ├── xml_serializable.dart │ │ ├── combined_converter.dart │ │ ├── text_converter.dart │ │ └── xml_converter.dart │ │ └── aha_client.dart └── aha_client.dart ├── tool └── setup_git_hooks.dart ├── pubspec.yaml ├── .gitignore ├── .devcontainer └── devcontainer.json ├── LICENSE └── example └── aha_client_example.dart /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | !*.chopper.dart 3 | !*.freezed.dart 4 | !*.g.dart 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aha-dart 2 | Dart client for the AVM Home Automation HTTP Interface + Login. 3 | -------------------------------------------------------------------------------- /doc/AHA-HTTP-Interface.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skycoder42/aha-dart/main/doc/AHA-HTTP-Interface.pdf -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "absenk", 4 | "celsius", 5 | "komfort" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pub" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /doc/AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skycoder42/aha-dart/main/doc/AVM_Technical_Note_-_Session_ID_deutsch_2021-05-03.pdf -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:dart_test_tools/package.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**/*.g.dart" 6 | - "**/*.chopper.dart" 7 | - "**/*.freezed.dart" 8 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/blind_state.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | /// The state of a blinds device. 4 | /// 5 | /// {@macro aha_reference} 6 | enum BlindState { 7 | open, 8 | 9 | close, 10 | 11 | stop; 12 | 13 | @override 14 | String toString() => name; 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | 7 | jobs: 8 | ci: 9 | name: CI 10 | uses: Skycoder42/dart_test_tools/.github/workflows/dart.yml@main 11 | with: 12 | buildRunner: true 13 | panaScoreThreshold: 20 14 | unitTestPaths: "" 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "dart", 6 | "command": "dart", 7 | "cwd": "", 8 | "args": [ 9 | "run", 10 | "build_runner", 11 | "watch" 12 | ], 13 | "problemMatcher": [], 14 | "label": "dart: dart run build_runner watch", 15 | "detail": "", 16 | "group": { 17 | "kind": "build", 18 | "isDefault": true 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tool/setup_git_hooks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | Future main() async { 4 | final preCommitHook = File('.git/hooks/pre-commit'); 5 | await preCommitHook.parent.create(recursive: true); 6 | await preCommitHook.writeAsString( 7 | ''' 8 | #!/bin/sh 9 | exec dart run dart_pre_commit 10 | ''', 11 | ); 12 | 13 | if (!Platform.isWindows) { 14 | final result = await Process.run('chmod', ['a+x', preCommitHook.path]); 15 | stdout.write(result.stdout); 16 | stderr.write(result.stderr); 17 | exitCode = result.exitCode; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/switch_duration.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'switch_duration.freezed.dart'; 4 | 5 | /// A wrapper for a [Duration] to pass to the API 6 | @freezed 7 | class SwitchDuration with _$SwitchDuration { 8 | /// Create a switch duration from the given [duration]. 9 | const factory SwitchDuration.create( 10 | /// The duration to be wrapped. 11 | Duration duration, 12 | ) = _SwitchDuration; 13 | 14 | @override 15 | String toString() => (duration.inMilliseconds / 100).toString(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/switch_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | /// The possible actions to perform on a switch device 4 | enum SwitchAction { 5 | /// Turn the device off. 6 | off(0), 7 | 8 | /// Turn the device on. 9 | on(1), 10 | 11 | /// Toggle the device state. 12 | toggle(2); 13 | 14 | /// @nodoc 15 | @internal 16 | final int value; 17 | 18 | /// @nodoc 19 | @internal 20 | const SwitchAction(this.value); 21 | 22 | @override 23 | String toString({bool pretty = false}) => 24 | pretty ? super.toString() : value.toString(); 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/switch_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:xml_annotation/xml_annotation.dart' as xml; 3 | 4 | part 'switch_state.g.dart'; 5 | 6 | /// The state of a switch device. 7 | @xml.XmlEnum() 8 | enum SwitchState { 9 | @xml.XmlValue('0') 10 | // ignore: public_member_api_docs 11 | off(false), 12 | @xml.XmlValue('1') 13 | // ignore: public_member_api_docs 14 | on(true), 15 | @xml.XmlValue('') 16 | // ignore: public_member_api_docs 17 | invalid(null); 18 | 19 | /// @nodoc 20 | @internal 21 | final bool? value; 22 | 23 | /// @nodoc 24 | @internal 25 | // ignore: avoid_positional_boolean_parameters 26 | const SwitchState(this.value); 27 | } 28 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: aha_client 2 | description: Dart client for the AVM Home Automation HTTP Interface + Login. 3 | version: 1.0.0 4 | homepage: https://github.com/Skycoder42/aha-dart 5 | 6 | environment: 7 | sdk: "^3.0.0" 8 | 9 | dependencies: 10 | chopper: ^6.1.4 11 | color: ^3.0.0 12 | convert: ^3.1.1 13 | cryptography: ^2.5.0 14 | enum_flag: ^1.0.2 15 | freezed_annotation: ^2.2.0 16 | http: ^1.0.0 17 | meta: ^1.9.1 18 | xml: ^6.3.0 19 | xml_annotation: ^2.2.0 20 | 21 | dev_dependencies: 22 | build_runner: ^2.4.5 23 | chopper_generator: ^6.0.1 24 | dart_pre_commit: ^5.1.0+1 25 | dart_test_tools: ^5.1.1 26 | freezed: ^2.3.5 27 | test: ^1.24.3 28 | xml_serializable: ^2.2.2 29 | -------------------------------------------------------------------------------- /lib/src/api/login/user_credentials.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'user_credentials.freezed.dart'; 4 | 5 | /// User credentials to log into the fritz.box 6 | @freezed 7 | class UserCredentials with _$UserCredentials { 8 | /// Default constructor. 9 | const factory UserCredentials({ 10 | /// The user to login with. 11 | required String username, 12 | 13 | /// The password of this user. 14 | /// 15 | /// Note: The password is never sent to the remote directly. Instead, a 16 | /// challenge response algorithm is used that uses this password to prove 17 | /// it's possession to the remote. 18 | required String password, 19 | }) = _UserCredentials; 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | # If you're building an application, you may want to check-in your pubspec.lock 8 | pubspec.lock 9 | 10 | # Directory created by dartdoc 11 | # If you don't generate documentation locally you can remove this line. 12 | doc/api/ 13 | 14 | # Avoid committing generated Javascript files: 15 | *.dart.js 16 | *.info.json # Produced by the --dump-info flag. 17 | *.js # When generated by dart2js. Don't specify *.js if your 18 | # project includes source files written in JavaScript. 19 | *.js_ 20 | *.js.deps 21 | *.js.map 22 | 23 | # Generated files 24 | *.chopper.dart 25 | *.freezed.dart 26 | *.g.dart 27 | 28 | *.log 29 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/function_bit_mask.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:enum_flag/enum_flag.dart'; 4 | 5 | /// A collection of bit masks that define the capabilities of a device. 6 | /// 7 | /// {@macro aha_reference} 8 | // ignore: prefer_mixin 9 | enum FunctionBitMaskFlag with EnumFlag { 10 | hanFunDevice, 11 | // ignore: unused_field 12 | _reserved1, 13 | lightOrLamp, 14 | // ignore: unused_field 15 | _reserved3, 16 | alarmSensor, 17 | avmButton, 18 | thermostat, 19 | energyMeasurementDevice, 20 | temperatureSensor, 21 | switchSocket, 22 | avmDECTRepeater, 23 | microphone, 24 | // ignore: unused_field 25 | _reserved12, 26 | hanFunUnit, 27 | // ignore: unused_field 28 | _reserved14, 29 | switchableDevice, 30 | dimmable, 31 | colorAdjustable, 32 | blinds; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/api/util/xml_serializable.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:xml/xml.dart'; 3 | 4 | /// @nodoc 5 | @internal 6 | abstract class IXmlSerializable { 7 | IXmlSerializable._(); 8 | 9 | /// @nodoc 10 | void buildXmlChildren( 11 | XmlBuilder builder, { 12 | Map namespaces = const {}, 13 | }); 14 | 15 | /// @nodoc 16 | List toXmlAttributes({ 17 | Map namespaces = const {}, 18 | }); 19 | 20 | /// @nodoc 21 | List toXmlChildren({Map namespaces = const {}}); 22 | } 23 | 24 | /// @nodoc 25 | @internal 26 | abstract class IXmlConvertible implements IXmlSerializable { 27 | IXmlConvertible._(); 28 | 29 | /// @nodoc 30 | void buildXmlElement( 31 | XmlBuilder builder, { 32 | Map namespaces = const {}, 33 | }); 34 | 35 | /// @nodoc 36 | XmlElement toXmlElement({Map namespaces = const {}}); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/temp.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'temp.freezed.dart'; 8 | part 'temp.g.dart'; 9 | 10 | /// A predefined color temperature. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Temp with _$Temp implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Temp(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_TempXmlSerializableMixin') 24 | const factory Temp({ 25 | @xml.XmlAttribute() @Default(0) int value, 26 | }) = _Temp; 27 | 28 | /// @nodoc 29 | @internal 30 | factory Temp.fromXmlElement(XmlElement element) => 31 | _$_$_TempFromXmlElement(element); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/name.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'name.freezed.dart'; 8 | part 'name.g.dart'; 9 | 10 | /// The name of a specific color group. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Name with _$Name implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Name(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_NameXmlSerializableMixin') 24 | const factory Name({ 25 | @xml.XmlAttribute(name: 'enum') int? enum_, 26 | @xml.XmlText() @Default('') String name, 27 | }) = _Name; 28 | 29 | /// @nodoc 30 | @internal 31 | factory Name.fromXmlElement(XmlElement element) => 32 | _$_$_NameFromXmlElement(element); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/avm_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'percentage.dart'; 7 | 8 | part 'avm_button.freezed.dart'; 9 | part 'avm_button.g.dart'; 10 | 11 | /// Status information about an avm button device 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class AvmButton with _$AvmButton implements IXmlSerializable { 17 | /// @nodoc 18 | @internal 19 | static const invalid = AvmButton(); 20 | 21 | /// @nodoc 22 | @internal 23 | @xml.XmlSerializable(createMixin: true) 24 | @With.fromString(r'_$_$_AvmButtonXmlSerializableMixin') 25 | const factory AvmButton({ 26 | @xml.XmlElement() @Default(Percentage.invalid) Percentage humidity, 27 | }) = _AvmButton; 28 | 29 | /// @nodoc 30 | @internal 31 | factory AvmButton.fromXmlElement(XmlElement element) => 32 | _$_$_AvmButtonFromXmlElement(element); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/hs_defaults.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | import 'hs_color.dart'; 8 | 9 | part 'hs_defaults.freezed.dart'; 10 | part 'hs_defaults.g.dart'; 11 | 12 | /// A collection of default HSV based colors. 13 | /// 14 | /// {@macro aha_reference} 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class HsDefaults with _$HsDefaults implements IXmlSerializable { 18 | /// @nodoc 19 | @internal 20 | static const invalid = HsDefaults(); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_HsDefaultsXmlSerializableMixin') 26 | const factory HsDefaults({ 27 | @xml.XmlElement(name: 'hs') @Default([]) List hsColors, 28 | }) = _HsDefaults; 29 | 30 | /// @nodoc 31 | @internal 32 | factory HsDefaults.fromXmlElement(XmlElement element) => 33 | _$_$_HsDefaultsFromXmlElement(element); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/simple_on_off.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'simple_on_off.freezed.dart'; 8 | part 'simple_on_off.g.dart'; 9 | 10 | /// The configuration of an on off device. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class SimpleOnOff with _$SimpleOnOff implements IXmlSerializable { 16 | /// The device is on. 17 | static const off = SimpleOnOff(); 18 | 19 | /// The device is off. 20 | static const on = SimpleOnOff(state: true); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_SimpleOnOffXmlSerializableMixin') 26 | const factory SimpleOnOff({ 27 | @xml.XmlElement() @Default(false) bool state, 28 | }) = _SimpleOnOff; 29 | 30 | /// @nodoc 31 | @internal 32 | factory SimpleOnOff.fromXmlElement(XmlElement element) => 33 | _$_$_SimpleOnOffFromXmlElement(element); 34 | } 35 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/alpine 3 | { 4 | "name": "Flutter (Stable)", 5 | "image": "skycoder42/devcontainers-flutter:latest", 6 | "customizations": { 7 | "vscode": { 8 | "extensions": [ 9 | "blaxou.freezed", 10 | "dart-code.dart-code", 11 | "Gruntfuggly.todo-tree", 12 | "me-dutour-mathieu.vscode-github-actions", 13 | "mhutchie.git-graph", 14 | "ryanluker.vscode-coverage-gutters", 15 | "streetsidesoftware.code-spell-checker", 16 | "streetsidesoftware.code-spell-checker-german", 17 | "timonwong.shellcheck" 18 | ], 19 | "settings": { 20 | "terminal.integrated.defaultProfile.linux": "zsh" 21 | } 22 | } 23 | }, 24 | "features": { 25 | "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { 26 | "plugins": "git colorize vscode", 27 | "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions" 28 | }, 29 | "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {} 30 | }, 31 | "postCreateCommand": "dart run tool/setup_git_hooks.dart" 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'timestamp.dart'; 7 | 8 | part 'button.freezed.dart'; 9 | part 'button.g.dart'; 10 | 11 | /// Status information about a button device 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class Button with _$Button implements IXmlSerializable { 17 | /// @nodoc 18 | @internal 19 | static const invalid = Button(); 20 | 21 | /// @nodoc 22 | @internal 23 | @xml.XmlSerializable(createMixin: true) 24 | @With.fromString(r'_$_$_ButtonXmlSerializableMixin') 25 | const factory Button({ 26 | @xml.XmlAttribute() int? id, 27 | @xml.XmlAttribute() String? identifier, 28 | @xml.XmlElement(name: 'lastpressedtimestamp') 29 | @Default(Timestamp.deactivated) 30 | Timestamp lastPressedTimestamp, 31 | @xml.XmlElement() String? name, 32 | }) = _Button; 33 | 34 | /// @nodoc 35 | @internal 36 | factory Button.fromXmlElement(XmlElement element) => 37 | _$_$_ButtonFromXmlElement(element); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/hs_color.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'color.dart'; 7 | import 'name.dart'; 8 | 9 | part 'hs_color.freezed.dart'; 10 | part 'hs_color.g.dart'; 11 | 12 | /// A predefined HSV color. 13 | /// 14 | /// {@macro aha_reference} 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class HsColor with _$HsColor implements IXmlSerializable { 18 | /// @nodoc 19 | @internal 20 | static const invalid = HsColor(); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_HsColorXmlSerializableMixin') 26 | const factory HsColor({ 27 | @xml.XmlAttribute(name: 'hue_index') @Default(0) int hueIndex, 28 | @xml.XmlElement() @Default(Name.invalid) Name name, 29 | @xml.XmlElement(name: 'color', isSelfClosing: true) 30 | @Default([]) 31 | List colors, 32 | }) = _HsColor; 33 | 34 | /// @nodoc 35 | @internal 36 | factory HsColor.fromXmlElement(XmlElement element) => 37 | _$_$_HsColorFromXmlElement(element); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/temperature_defaults.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | import 'temp.dart'; 8 | 9 | part 'temperature_defaults.freezed.dart'; 10 | part 'temperature_defaults.g.dart'; 11 | 12 | /// A collection of default light color temperatures. 13 | /// 14 | /// {@macro aha_reference} 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class TemperatureDefaults 18 | with _$TemperatureDefaults 19 | implements IXmlSerializable { 20 | /// @nodoc 21 | @internal 22 | static const invalid = TemperatureDefaults(); 23 | 24 | /// @nodoc 25 | @internal 26 | @xml.XmlSerializable(createMixin: true) 27 | @With.fromString(r'_$_$_TemperatureDefaultsXmlSerializableMixin') 28 | const factory TemperatureDefaults({ 29 | @xml.XmlElement(name: 'temp', isSelfClosing: true) 30 | @Default([]) 31 | List temperatures, 32 | }) = _TemperatureDefaults; 33 | 34 | /// @nodoc 35 | @internal 36 | factory TemperatureDefaults.fromXmlElement(XmlElement element) => 37 | _$_$_TemperatureDefaultsFromXmlElement(element); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/level_control.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'level.dart'; 7 | import 'percentage.dart'; 8 | 9 | part 'level_control.freezed.dart'; 10 | part 'level_control.g.dart'; 11 | 12 | /// Status information about a level controllable device. 13 | /// 14 | /// {@macro aha_reference} 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class LevelControl with _$LevelControl implements IXmlSerializable { 18 | /// @nodoc 19 | @internal 20 | static const invalid = LevelControl(); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_LevelControlXmlSerializableMixin') 26 | const factory LevelControl({ 27 | @xml.XmlElement() @Default(Level.invalid) Level level, 28 | @xml.XmlElement(name: 'levelpercentage') 29 | @Default(Percentage.invalid) 30 | Percentage levelPercentage, 31 | }) = _LevelControl; 32 | 33 | /// @nodoc 34 | @internal 35 | factory LevelControl.fromXmlElement(XmlElement element) => 36 | _$_$_LevelControlFromXmlElement(element); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/api/login/login_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'models/session_info.dart'; 5 | import 'models/sid.dart'; 6 | 7 | part 'login_service.chopper.dart'; 8 | 9 | /// @nodoc 10 | @internal 11 | @ChopperApi(baseUrl: '/login_sid.lua?version=2') 12 | abstract class LoginService extends ChopperService { 13 | /// @nodoc 14 | static LoginService create([ChopperClient? client]) => _$LoginService(client); 15 | 16 | /// @nodoc 17 | @Get() 18 | Future> getLoginStatus(); 19 | 20 | /// @nodoc 21 | @Post() 22 | @FactoryConverter( 23 | request: FormUrlEncodedConverter.requestFactory, 24 | ) 25 | Future> login({ 26 | @Field() required String username, 27 | @Field() required String response, 28 | }); 29 | 30 | /// @nodoc 31 | @Post() 32 | @FactoryConverter( 33 | request: FormUrlEncodedConverter.requestFactory, 34 | ) 35 | Future> checkSessionValid(@Field() Sid sid); 36 | 37 | /// @nodoc 38 | @Post() 39 | @FactoryConverter( 40 | request: FormUrlEncodedConverter.requestFactory, 41 | ) 42 | Future> logout( 43 | @Field() Sid sid, { 44 | @Field() bool logout = true, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/next_change.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'hkr_temperature.dart'; 7 | import 'timestamp.dart'; 8 | 9 | part 'next_change.freezed.dart'; 10 | part 'next_change.g.dart'; 11 | 12 | /// A scheduled change of temperature of a thermostat. 13 | /// 14 | /// {@macro aha_reference} 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class NextChange with _$NextChange implements IXmlSerializable { 18 | /// @nodoc 19 | @internal 20 | static const invalid = NextChange(); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_NextChangeXmlSerializableMixin') 26 | const factory NextChange({ 27 | @xml.XmlElement(name: 'endperiod') 28 | @Default(Timestamp.deactivated) 29 | Timestamp endPeriod, 30 | @xml.XmlElement(name: 'tchange') 31 | @Default(HkrTemperature.invalid) 32 | HkrTemperature tChange, 33 | }) = _NextChange; 34 | 35 | /// @nodoc 36 | @internal 37 | factory NextChange.fromXmlElement(XmlElement element) => 38 | _$_$_NextChangeFromXmlElement(element); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/power_meter.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'energy.dart'; 7 | import 'power.dart'; 8 | import 'voltage.dart'; 9 | 10 | part 'power_meter.freezed.dart'; 11 | part 'power_meter.g.dart'; 12 | 13 | /// The current measurements of a power meter device. 14 | /// 15 | /// {@macro aha_reference} 16 | @Freezed(makeCollectionsUnmodifiable: false) 17 | @xml.XmlSerializable() 18 | abstract class PowerMeter with _$PowerMeter implements IXmlSerializable { 19 | /// @nodoc 20 | @internal 21 | static const invalid = PowerMeter(); 22 | 23 | /// @nodoc 24 | @internal 25 | @xml.XmlSerializable(createMixin: true) 26 | @With.fromString(r'_$_$_PowerMeterXmlSerializableMixin') 27 | const factory PowerMeter({ 28 | @xml.XmlElement(name: 'power') @Default(Power.invalid) Power power, 29 | @xml.XmlElement(name: 'energy') @Default(Energy.invalid) Energy energy, 30 | @xml.XmlElement(name: 'voltage') @Default(Voltage.invalid) Voltage voltage, 31 | }) = _PowerMeter; 32 | 33 | /// @nodoc 34 | @internal 35 | factory PowerMeter.fromXmlElement(XmlElement element) => 36 | _$_$_PowerMeterFromXmlElement(element); 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/group_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'group_info.freezed.dart'; 8 | part 'group_info.g.dart'; 9 | 10 | /// Information about the devices in a device group. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class GroupInfo with _$GroupInfo implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = GroupInfo(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_GroupInfoXmlSerializableMixin') 24 | const factory GroupInfo({ 25 | @xml.XmlElement(name: 'masterdeviceid') @Default(0) int masterDeviceId, 26 | 27 | /// @nodoc 28 | @xml.XmlElement(name: 'members') 29 | @visibleForOverriding 30 | @Default('') 31 | String rawMembers, 32 | }) = _GroupInfo; 33 | 34 | /// @nodoc 35 | @internal 36 | factory GroupInfo.fromXmlElement(XmlElement element) => 37 | _$_$_GroupInfoFromXmlElement(element); 38 | 39 | const GroupInfo._(); 40 | 41 | // ignore: public_member_api_docs 42 | List get members => rawMembers.split(',').map(int.parse).toList(); 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/stats_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'stats.dart'; 7 | 8 | part 'stats_group.freezed.dart'; 9 | part 'stats_group.g.dart'; 10 | 11 | /// A group of statistics for a measurement. 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class StatsGroup 17 | with _$StatsGroup 18 | implements IXmlSerializable { 19 | /// @nodoc 20 | @internal 21 | static const invalid = StatsGroup(); 22 | 23 | /// @nodoc 24 | @internal 25 | @xml.XmlSerializable(createMixin: true) 26 | @With.fromString(r'_$_$_StatsGroupXmlSerializableMixin') 27 | const factory StatsGroup({ 28 | @xml.XmlElement() List>? stats, 29 | }) = _StatsGroup; 30 | 31 | /// @nodoc 32 | @internal 33 | factory StatsGroup.fromXmlElement(XmlElement element) => 34 | _$_$_StatsGroupFromXmlElement(element).cast(); 35 | 36 | const StatsGroup._(); 37 | 38 | /// @nodoc 39 | @internal 40 | StatsGroup cast() => 41 | StatsGroup( 42 | stats: stats?.map((s) => s.cast()).toList(), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/power.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'power.freezed.dart'; 8 | part 'power.g.dart'; 9 | 10 | /// An electric power value. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Power with _$Power implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Power(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_PowerXmlSerializableMixin') 24 | const factory Power({ 25 | @xml.XmlText() @visibleForOverriding @Default(0) int rawValue, 26 | }) = _Power; 27 | 28 | /// @nodoc 29 | @internal 30 | factory Power.fromXmlElement(XmlElement element) => 31 | _$_$_PowerFromXmlElement(element); 32 | 33 | /// @nodoc 34 | @internal 35 | factory Power.fromString(String rawValue) => 36 | Power(rawValue: int.parse(rawValue)); 37 | 38 | const Power._(); 39 | 40 | /// Returns the energy in milli Watts 41 | int get milliWatts => rawValue; 42 | 43 | /// Returns the energy in Watts 44 | double get watts => rawValue / 1000; 45 | 46 | @override 47 | String toString() => '$rawValue mW'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/energy.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'energy.freezed.dart'; 8 | part 'energy.g.dart'; 9 | 10 | /// An electric energy value. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Energy with _$Energy implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Energy(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_EnergyXmlSerializableMixin') 24 | const factory Energy({ 25 | @xml.XmlText() @visibleForOverriding @Default(0) int rawValue, 26 | }) = _Energy; 27 | 28 | /// @nodoc 29 | @internal 30 | factory Energy.fromXmlElement(XmlElement element) => 31 | _$_$_EnergyFromXmlElement(element); 32 | 33 | /// @nodoc 34 | @internal 35 | factory Energy.fromString(String rawValue) => 36 | Energy(rawValue: int.parse(rawValue)); 37 | 38 | const Energy._(); 39 | 40 | /// Returns the energy in Watt hours 41 | int get wattHours => rawValue; 42 | 43 | /// Returns energy in Joule 44 | int get joule => rawValue * 3600; 45 | 46 | @override 47 | String toString() => '$rawValue Wh'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/voltage.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'voltage.freezed.dart'; 8 | part 'voltage.g.dart'; 9 | 10 | /// An electric voltage value. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Voltage with _$Voltage implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Voltage(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_VoltageXmlSerializableMixin') 24 | const factory Voltage({ 25 | @xml.XmlText() @visibleForOverriding @Default(0) int rawValue, 26 | }) = _Voltage; 27 | 28 | /// @nodoc 29 | @internal 30 | factory Voltage.fromXmlElement(XmlElement element) => 31 | _$_$_VoltageFromXmlElement(element); 32 | 33 | /// @nodoc 34 | @internal 35 | factory Voltage.fromString(String rawValue) => 36 | Voltage(rawValue: int.parse(rawValue)); 37 | 38 | const Voltage._(); 39 | 40 | /// Returns the energy in milli Volts 41 | int get milliVolts => rawValue; 42 | 43 | /// Returns the energy in Volts 44 | double get volts => rawValue / 1000; 45 | 46 | @override 47 | String toString() => '$rawValue mV'; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/switch.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'switch_state.dart'; 7 | 8 | part 'switch.freezed.dart'; 9 | part 'switch.g.dart'; 10 | 11 | /// The mode of the switch. 12 | @xml.XmlEnum() 13 | enum SwitchMode { 14 | // ignore: public_member_api_docs 15 | manuell, 16 | // ignore: public_member_api_docs 17 | auto, 18 | } 19 | 20 | /// The configuration of a switch device. 21 | /// 22 | /// {@macro aha_reference} 23 | @Freezed(makeCollectionsUnmodifiable: false) 24 | @xml.XmlSerializable() 25 | abstract class Switch with _$Switch implements IXmlSerializable { 26 | /// @nodoc 27 | @internal 28 | static const invalid = Switch(); 29 | 30 | /// @nodoc 31 | @internal 32 | @xml.XmlSerializable(createMixin: true) 33 | @With.fromString(r'_$_$_SwitchXmlSerializableMixin') 34 | const factory Switch({ 35 | @xml.XmlElement() @Default(SwitchState.invalid) SwitchState state, 36 | @xml.XmlElement() @Default(SwitchMode.manuell) SwitchMode mode, 37 | @xml.XmlElement() @Default(SwitchState.invalid) SwitchState lock, 38 | @xml.XmlElement(name: 'devicelock') 39 | @Default(SwitchState.invalid) 40 | SwitchState deviceLock, 41 | }) = _Switch; 42 | 43 | /// @nodoc 44 | @internal 45 | factory Switch.fromXmlElement(XmlElement element) => 46 | _$_$_SwitchFromXmlElement(element); 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/color_defaults.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | import 'hs_defaults.dart'; 8 | import 'temperature_defaults.dart'; 9 | 10 | part 'color_defaults.freezed.dart'; 11 | part 'color_defaults.g.dart'; 12 | 13 | /// A collection of default HSV colors to choose from for color control devices. 14 | /// 15 | /// {@macro aha_reference} 16 | @Freezed(makeCollectionsUnmodifiable: false) 17 | @xml.XmlSerializable() 18 | abstract class ColorDefaults with _$ColorDefaults implements IXmlConvertible { 19 | /// @nodoc 20 | @internal 21 | static const elementName = 'colordefaults'; 22 | 23 | /// @nodoc 24 | @internal 25 | static const invalid = ColorDefaults(); 26 | 27 | /// @nodoc 28 | @internal 29 | @xml.XmlSerializable(createMixin: true) 30 | @xml.XmlRootElement(name: ColorDefaults.elementName) 31 | @With.fromString(r'_$_$_ColorDefaultsXmlSerializableMixin') 32 | const factory ColorDefaults({ 33 | @xml.XmlElement(name: 'hsdefaults') 34 | @Default(HsDefaults.invalid) 35 | HsDefaults hsDefaults, 36 | @xml.XmlElement(name: 'temperaturedefaults') 37 | @Default(TemperatureDefaults.invalid) 38 | TemperatureDefaults temperatureDefaults, 39 | }) = _ColorDefaults; 40 | 41 | /// @nodoc 42 | @internal 43 | factory ColorDefaults.fromXmlElement(XmlElement element) => 44 | _$_$_ColorDefaultsFromXmlElement(element); 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/device_stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | import 'stats.dart'; 8 | import 'stats_group.dart'; 9 | 10 | part 'device_stats.freezed.dart'; 11 | part 'device_stats.g.dart'; 12 | 13 | /// A collection of measurement statistics for a given device. 14 | /// 15 | /// {@macro aha_reference} 16 | @Freezed(makeCollectionsUnmodifiable: false) 17 | @xml.XmlSerializable() 18 | abstract class DeviceStats with _$DeviceStats implements IXmlConvertible { 19 | /// @nodoc 20 | @internal 21 | static const elementName = 'devicestats'; 22 | 23 | /// @nodoc 24 | @internal 25 | static const invalid = DeviceStats(); 26 | 27 | /// @nodoc 28 | @internal 29 | @xml.XmlSerializable(createMixin: true) 30 | @xml.XmlRootElement(name: DeviceStats.elementName) 31 | @With.fromString(r'_$_$_DeviceStatsXmlSerializableMixin') 32 | const factory DeviceStats({ 33 | @xml.XmlElement() StatsGroup? temperature, 34 | @xml.XmlElement() StatsGroup? humidity, 35 | @xml.XmlElement() StatsGroup? voltage, 36 | @xml.XmlElement() StatsGroup? power, 37 | @xml.XmlElement() StatsGroup? energy, 38 | }) = _DeviceStats; 39 | 40 | /// @nodoc 41 | @internal 42 | factory DeviceStats.fromXmlElement(XmlElement element) => 43 | _$_$_DeviceStatsFromXmlElement(element); 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Felix Barz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/subscription_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'subscription_state.freezed.dart'; 8 | part 'subscription_state.g.dart'; 9 | 10 | /// The state of the device registration. 11 | @xml.XmlEnum() 12 | enum SubscriptionState { 13 | @xml.XmlValue('0') 14 | // ignore: public_member_api_docs 15 | registrationNotRunning, 16 | @xml.XmlValue('1') 17 | // ignore: public_member_api_docs 18 | registrationRunning, 19 | @xml.XmlValue('2') 20 | // ignore: public_member_api_docs 21 | timeout, 22 | @xml.XmlValue('3') 23 | // ignore: public_member_api_docs 24 | error, 25 | } 26 | 27 | /// The state of the BLE registration. 28 | /// 29 | /// {@macro aha_reference} 30 | @Freezed(makeCollectionsUnmodifiable: false) 31 | @xml.XmlSerializable() 32 | abstract class State with _$State implements IXmlConvertible { 33 | /// @nodoc 34 | @internal 35 | static const elementName = 'state'; 36 | 37 | /// @nodoc 38 | @internal 39 | static const invalid = State(); 40 | 41 | /// @nodoc 42 | @internal 43 | @xml.XmlSerializable(createMixin: true) 44 | @xml.XmlRootElement(name: State.elementName) 45 | @With.fromString(r'_$_$_StateXmlSerializableMixin') 46 | const factory State({ 47 | @xml.XmlAttribute() 48 | @Default(SubscriptionState.registrationNotRunning) 49 | SubscriptionState code, 50 | @xml.XmlElement(name: 'latestain') String? latestAin, 51 | }) = _State; 52 | 53 | /// @nodoc 54 | @internal 55 | factory State.fromXmlElement(XmlElement element) => 56 | _$_$_StateFromXmlElement(element); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:color/color.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:xml/xml.dart'; 4 | import 'package:xml_annotation/xml_annotation.dart' as xml; 5 | 6 | import '../../util/xml_serializable.dart'; 7 | 8 | part 'color.freezed.dart'; 9 | part 'color.g.dart'; 10 | 11 | /// A HSV color representation. 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class Color with _$Color implements IXmlSerializable { 17 | /// @nodoc 18 | @internal 19 | static const invalid = Color(); 20 | 21 | /// @nodoc 22 | @internal 23 | @xml.XmlSerializable(createMixin: true) 24 | @With.fromString(r'_$_$_ColorXmlSerializableMixin') 25 | @Assert('hue >= 0 && hue <= 359', 'hue must be in range [0, 359]') 26 | @Assert('sat >= 0 && sat <= 255', 'sat must be in range [0, 255]') 27 | @Assert('val >= 0 && val <= 255', 'val must be in range [0, 255]') 28 | const factory Color({ 29 | @xml.XmlAttribute(name: 'sat_index') @Default(0) int satIndex, 30 | 31 | /// @nodoc 32 | @xml.XmlAttribute() @visibleForOverriding @Default(0) int hue, 33 | 34 | /// @nodoc 35 | @xml.XmlAttribute() @visibleForOverriding @Default(0) int sat, 36 | 37 | /// @nodoc 38 | @xml.XmlAttribute() @visibleForOverriding @Default(0) int val, 39 | }) = _Color; 40 | 41 | /// @nodoc 42 | @internal 43 | factory Color.fromXmlElement(XmlElement element) => 44 | _$_$_ColorFromXmlElement(element); 45 | 46 | const Color._(); 47 | 48 | // ignore: public_member_api_docs 49 | HsvColor get color => HsvColor( 50 | hue, 51 | sat / 255 * 100, 52 | val / 255 * 100, 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/temperature.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'temperature.freezed.dart'; 8 | part 'temperature.g.dart'; 9 | 10 | /// The temperature of a temperature measurement device. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Temperature with _$Temperature implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = Temperature(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_TemperatureXmlSerializableMixin') 24 | const factory Temperature({ 25 | @xml.XmlElement(name: 'celsius') @visibleForOverriding int? rawCelsius, 26 | @xml.XmlElement(name: 'offset') @visibleForOverriding int? rawOffset, 27 | }) = _Temperature; 28 | 29 | /// @nodoc 30 | @internal 31 | factory Temperature.fromXmlElement(XmlElement element) => 32 | _$_$_TemperatureFromXmlElement(element); 33 | 34 | /// @nodoc 35 | @internal 36 | factory Temperature.fromString(String rawCelsius) => 37 | Temperature(rawCelsius: int.parse(rawCelsius)); 38 | 39 | const Temperature._(); 40 | 41 | // ignore: public_member_api_docs 42 | double get temperature => (rawCelsius ?? 0) / 10; 43 | 44 | // ignore: public_member_api_docs 45 | double get offset => (rawOffset ?? 0) / 10; 46 | 47 | @override 48 | String toString({bool pretty = false}) => pretty 49 | ? (rawOffset != null 50 | ? '$temperature°C (Offset: $offset°C)' 51 | : '$temperature°C') 52 | : rawCelsius.toString(); 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/device_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'device.dart'; 7 | 8 | part 'device_list.freezed.dart'; 9 | part 'device_list.g.dart'; 10 | 11 | /// A list of devices and device groups connected with the fritz.box. 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class DeviceList with _$DeviceList implements IXmlConvertible { 17 | /// @nodoc 18 | @internal 19 | static const elementName = 'devicelist'; 20 | 21 | /// @nodoc 22 | @internal 23 | static const invalid = DeviceList(); 24 | 25 | /// @nodoc 26 | @internal 27 | @xml.XmlSerializable(createMixin: true) 28 | @xml.XmlRootElement(name: DeviceList.elementName) 29 | @With.fromString(r'_$_$_DeviceListXmlSerializableMixin') 30 | @Assert( 31 | 'version == 1', 32 | 'Only the $elementName version 1 format is supported', 33 | ) 34 | const factory DeviceList({ 35 | @xml.XmlAttribute() @Default(1) int version, 36 | @xml.XmlAttribute(name: 'fwversion') @Default('') String fwVersion, 37 | @xml.XmlElement(name: Device.deviceElementName) List? devices, 38 | 39 | /// @nodoc 40 | @xml.XmlElement(name: Device.groupElementName) 41 | @visibleForOverriding 42 | List? rawGroups, 43 | }) = _DeviceList; 44 | 45 | /// @nodoc 46 | @internal 47 | factory DeviceList.fromXmlElement(XmlElement element) => 48 | _$_$_DeviceListFromXmlElement(element); 49 | 50 | const DeviceList._(); 51 | 52 | // ignore: public_member_api_docs 53 | List? get groups => rawGroups?.cast().toList(); 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/api/login/models/session_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'right.dart'; 7 | import 'sid.dart'; 8 | import 'user.dart'; 9 | 10 | part 'session_info.freezed.dart'; 11 | part 'session_info.g.dart'; 12 | 13 | /// @nodoc 14 | @internal 15 | @Freezed(makeCollectionsUnmodifiable: false) 16 | @xml.XmlSerializable() 17 | abstract class SessionInfo with _$SessionInfo implements IXmlConvertible { 18 | /// @nodoc 19 | @internal 20 | static const elementName = 'SessionInfo'; 21 | 22 | /// @nodoc 23 | @internal 24 | static const invalid = SessionInfo(); 25 | 26 | /// @nodoc 27 | @internal 28 | @xml.XmlSerializable(createMixin: true) 29 | @xml.XmlRootElement(name: SessionInfo.elementName) 30 | @With.fromString(r'_$_$_SessionInfoXmlSerializableMixin') 31 | const factory SessionInfo({ 32 | @xml.XmlElement(name: 'SID') @Default(Sid.invalid) Sid sid, 33 | @xml.XmlElement(name: 'Challenge') @Default('') String challenge, 34 | 35 | /// @nodoc 36 | @xml.XmlElement(name: 'BlockTime') 37 | @visibleForOverriding 38 | @Default(0) 39 | int blockTimeRaw, 40 | @xml.XmlElement(name: 'Users') @Default(Users.empty) Users users, 41 | @xml.XmlElement(name: 'Rights') 42 | @Default(AccessRights.empty) 43 | AccessRights accessRights, 44 | }) = _SessionInfo; 45 | 46 | /// @nodoc 47 | @internal 48 | factory SessionInfo.fromXmlElement(XmlElement element) => 49 | _$_$_SessionInfoFromXmlElement(element); 50 | 51 | const SessionInfo._(); 52 | 53 | /// @nodoc 54 | bool get isValid => sid.isValid; 55 | 56 | /// @nodoc 57 | Duration get blockTime => Duration(seconds: blockTimeRaw); 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/api/login/authentication_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'authentication_exception.freezed.dart'; 4 | 5 | /// An error that can be thrown by an AHA API request if there is a problem 6 | /// with the authentication. 7 | /// 8 | /// {@macro aha_reference} 9 | @freezed 10 | class AuthenticationException 11 | with _$AuthenticationException 12 | implements Exception { 13 | /// Indicates that a login request failed with an unexpected [statusCode]. 14 | /// 15 | /// The full error response is found in [error]. 16 | factory AuthenticationException.invalidStatus( 17 | /// The HTTP status code of the failed request 18 | int statusCode, [ 19 | /// The error response body, if one was given. 20 | Object? error, 21 | ]) = _Status; 22 | 23 | /// Indicates that the library is unable to handle the provided authentication 24 | /// challenge. 25 | /// 26 | /// If you get this error, your Fritz-OS might be to old and thus not 27 | /// supported by this library. 28 | /// 29 | /// {@macro aha_reference} 30 | factory AuthenticationException.invalidChallengeFormat() = 31 | _InvalidChallengeFormat; 32 | 33 | /// Indicates that the provided credentials are invalid. 34 | factory AuthenticationException.invalidCredentials() = _InvalidCredentials; 35 | 36 | /// The authentication was canceled by the user. 37 | factory AuthenticationException.loginCanceled() = _LoginCanceled; 38 | 39 | /// The remote rejected the logout and your session is still active. 40 | /// 41 | /// Honestly, this should never happen, but theoretically it can, so here is 42 | /// this error. If you ever get this, please open an issue, because it most 43 | /// likely means something else is broken. 44 | factory AuthenticationException.logoutFailed() = _LogoutFailed; 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/level.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'percentage.dart'; 7 | 8 | part 'level.freezed.dart'; 9 | part 'level.g.dart'; 10 | 11 | /// The level of a level controllable device in discrete values. 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class Level with _$Level implements IXmlSerializable { 17 | /// @nodoc 18 | @internal 19 | static const invalid = Level(); 20 | 21 | /// @nodoc 22 | @internal 23 | @xml.XmlSerializable(createMixin: true) 24 | @With.fromString(r'_$_$_LevelXmlSerializableMixin') 25 | @Assert( 26 | 'level >= 0 && level <= 255', 27 | 'level must be in range [0, 255]', 28 | ) 29 | const factory Level({ 30 | @xml.XmlText() @Default(0) int level, 31 | }) = _Level; 32 | 33 | /// @nodoc 34 | @internal 35 | factory Level.fromXmlElement(XmlElement element) => 36 | _$_$_LevelFromXmlElement(element); 37 | 38 | /// @nodoc 39 | @internal 40 | factory Level.fromString(String level) => Level(level: int.parse(level)); 41 | 42 | /// Create a new level instance from the given [level]. 43 | /// 44 | /// [level] must be an integer value in the range `[0, 255]`. 45 | factory Level.create(int level) { 46 | RangeError.checkValueInInterval(level, 0, 255, 'level'); 47 | return Level(level: level); 48 | } 49 | 50 | const Level._(); 51 | 52 | /// Convert this level into a [Percentage] object. 53 | /// 54 | /// A level of 0 means 0%, a level of 255 means 100%. 55 | // ignore: invalid_use_of_visible_for_overriding_member 56 | Percentage toPercentage() => Percentage(rawValue: level * 100 ~/ 255); 57 | 58 | @override 59 | String toString() => level.toString(); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/api/login/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'user.freezed.dart'; 8 | part 'user.g.dart'; 9 | 10 | /// A user that can login to the fritz.box 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class User with _$User implements IXmlSerializable { 16 | /// @nodoc 17 | @internal 18 | static const invalid = User(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_UserXmlSerializableMixin') 24 | const factory User({ 25 | @xml.XmlText() @Default('') String user, 26 | 27 | /// @nodoc 28 | @visibleForOverriding 29 | @xml.XmlAttribute(name: 'last') 30 | @Default(false) 31 | bool? lastRaw, 32 | }) = _User; 33 | 34 | /// @nodoc 35 | @internal 36 | factory User.fromXmlElement(XmlElement element) => 37 | _$_$_UserFromXmlElement(element); 38 | 39 | const User._(); 40 | 41 | // ignore: public_member_api_docs 42 | bool get last => lastRaw ?? false; 43 | } 44 | 45 | /// A list of users that can login on the fritz.box 46 | /// 47 | /// {@macro aha_reference} 48 | @Freezed(makeCollectionsUnmodifiable: false) 49 | @xml.XmlSerializable() 50 | abstract class Users with _$Users implements IXmlSerializable { 51 | /// @nodoc 52 | @internal 53 | static const empty = Users(); 54 | 55 | /// @nodoc 56 | @internal 57 | @xml.XmlSerializable(createMixin: true) 58 | @With.fromString(r'_$_$_UsersXmlSerializableMixin') 59 | const factory Users({ 60 | @xml.XmlElement() @Default([]) List? users, 61 | }) = _Users; 62 | 63 | /// @nodoc 64 | @internal 65 | factory Users.fromXmlElement(XmlElement element) => 66 | _$_$_UsersFromXmlElement(element); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/api/login/login_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | import 'authentication_exception.dart'; 4 | import 'login_manager.dart'; 5 | import 'models/user.dart'; 6 | 7 | part 'login_info.freezed.dart'; 8 | 9 | /// The reason why a login was requested. 10 | enum LoginReason { 11 | /// An explicit login was triggered. 12 | /// 13 | /// This happens if you call [LoginManager.login]. 14 | manual, 15 | 16 | /// An implicit login was triggered. 17 | /// 18 | /// This happens if you call any of the AHA-Apis of the client without 19 | /// previously authenticating. Rejecting such an automatic login will cause 20 | /// the [AuthenticationException.loginCanceled] exception to be thrown by the 21 | /// original request. 22 | auto, 23 | 24 | /// A previously authentication session needs to be refreshed. 25 | /// 26 | /// This typically happens if your session is idle for to long and times out. 27 | refresh, 28 | } 29 | 30 | /// An information object about a login requested via 31 | /// [LoginManager.obtainCredentials]. 32 | @freezed 33 | class LoginInfo with _$LoginInfo { 34 | /// Default constructor. 35 | const factory LoginInfo({ 36 | /// A list of known users, if one is provided by the remote. 37 | /// 38 | /// Most of the time, this list is either missing or incomplete. You can use 39 | /// it to provide the user with a list of known accounts, but you should 40 | /// always allow them to enter their own name. 41 | required List knownUsers, 42 | 43 | /// Indicates that the login is blocked for the given duration. 44 | /// 45 | /// This can happen if a previous login failed. You should display this to 46 | /// the user and wait with submission of new credentials until this time 47 | /// has expired, otherwise a login will automatically fail. 48 | required Duration? blockTime, 49 | 50 | /// The reason why a login is requested. 51 | required LoginReason reason, 52 | }) = _LoginInfo; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/api/login/models/sid.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import '../login_manager.dart'; 7 | 8 | part 'sid.freezed.dart'; 9 | part 'sid.g.dart'; 10 | 11 | /// The sessions ID of the currently active session 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class Sid with _$Sid implements IXmlSerializable { 17 | static const _invalidValue = '0000000000000000'; 18 | static final _sidRegexp = RegExp(r'^[0-9a-z]{16}$', caseSensitive: false); 19 | 20 | /// An invalid session ID 21 | static const invalid = Sid(); 22 | 23 | /// @nodoc 24 | @internal 25 | @xml.XmlSerializable(createMixin: true) 26 | @With.fromString(r'_$_$_SidXmlSerializableMixin') 27 | @Assert('sid.length == 16', 'must be a 64 bit hex encoded integer') 28 | const factory Sid({ 29 | /// The string value of this SID 30 | @xml.XmlText() @Default(Sid._invalidValue) String sid, 31 | }) = _Sid; 32 | 33 | /// @nodoc 34 | @internal 35 | factory Sid.fromXmlElement(XmlElement element) => 36 | _$_$_SidFromXmlElement(element); 37 | 38 | /// Creates a new SID from the given [sid] string 39 | factory Sid.fromString(String sid) { 40 | if (!_sidRegexp.hasMatch(sid)) { 41 | throw ArgumentError.value( 42 | sid, 43 | 'sid', 44 | 'must be a 64 bit hex encoded integer', 45 | ); 46 | } 47 | 48 | return Sid(sid: sid); 49 | } 50 | 51 | const Sid._(); 52 | 53 | /// Returns true if this ID is valid or false if it is not. 54 | /// 55 | /// A valid session ID is only returned from a successful login. This check 56 | /// does not handle session expiry. To check if a session is still active, 57 | /// use [LoginManager.checkSessionValid]. 58 | bool get isValid => sid != _invalidValue; 59 | 60 | @override 61 | String toString() => sid; 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/alert.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'timestamp.dart'; 7 | 8 | part 'alert.freezed.dart'; 9 | part 'alert.g.dart'; 10 | 11 | /// The state of an alert device. 12 | /// 13 | /// {@macro aha_reference} 14 | @xml.XmlEnum() 15 | enum AlertState { 16 | @xml.XmlValue('0') 17 | // ignore: public_member_api_docs 18 | ok(false), 19 | 20 | @xml.XmlValue('1') 21 | // ignore: public_member_api_docs 22 | alertOrBlocked(true), 23 | 24 | @xml.XmlValue('2') 25 | // ignore: public_member_api_docs 26 | overheated(true), 27 | 28 | @xml.XmlValue('3') 29 | // ignore: public_member_api_docs 30 | blockedAndOverheated(true), 31 | 32 | @xml.XmlValue('') 33 | // ignore: public_member_api_docs 34 | unknown(null); 35 | 36 | /// Checks if the current value is an alert or not. 37 | /// 38 | /// Can be null if the alert state is unknown. 39 | final bool? hasAlert; 40 | 41 | /// @nodoc 42 | @internal 43 | // ignore: avoid_positional_boolean_parameters 44 | const AlertState(this.hasAlert); 45 | } 46 | 47 | /// Status information about an alert device 48 | /// 49 | /// {@macro aha_reference} 50 | @Freezed(makeCollectionsUnmodifiable: false) 51 | @xml.XmlSerializable() 52 | abstract class Alert with _$Alert implements IXmlSerializable { 53 | /// @nodoc 54 | @internal 55 | static const invalid = Alert(); 56 | 57 | /// @nodoc 58 | @internal 59 | @xml.XmlSerializable(createMixin: true) 60 | @With.fromString(r'_$_$_AlertXmlSerializableMixin') 61 | const factory Alert({ 62 | @xml.XmlElement() @Default(AlertState.unknown) AlertState state, 63 | @xml.XmlElement(name: 'lastalertchgtimestamp') 64 | @Default(Timestamp.deactivated) 65 | Timestamp lastAlertChgTimestamp, 66 | }) = _Alert; 67 | 68 | /// @nodoc 69 | @internal 70 | factory Alert.fromXmlElement(XmlElement element) => 71 | _$_$_AlertFromXmlElement(element); 72 | } 73 | -------------------------------------------------------------------------------- /example/aha_client_example.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'dart:async'; 4 | import 'dart:io'; 5 | 6 | import 'package:aha_client/aha_client.dart'; 7 | 8 | class _AhaClientHttpOverrides extends HttpOverrides { 9 | final String _host; 10 | final int _port; 11 | 12 | _AhaClientHttpOverrides(this._host, this._port); 13 | 14 | @override 15 | HttpClient createHttpClient(SecurityContext? context) => 16 | super.createHttpClient(context) 17 | ..badCertificateCallback = 18 | (cert, host, port) => host == _host && port == _port; 19 | } 20 | 21 | // ignore: must_be_immutable 22 | class ExampleLoginManager extends LoginManager { 23 | @override 24 | FutureOr obtainCredentials(LoginInfo loginInfo) async { 25 | if (loginInfo.blockTime != null) { 26 | stdout.writeln( 27 | 'Login blocked until: ${DateTime.now().add(loginInfo.blockTime!)}', 28 | ); 29 | } 30 | stdout 31 | ..writeln('Known usernames: ${loginInfo.knownUsers}') 32 | ..write('Username: '); 33 | await stdout.flush(); 34 | final username = stdin.readLineSync(); 35 | stdout.write('Password: '); 36 | await stdout.flush(); 37 | 38 | final echoMode = stdin.echoMode; 39 | String? password; 40 | try { 41 | stdin.echoMode = false; 42 | password = stdin.readLineSync(); 43 | stdout.writeln(); 44 | } finally { 45 | stdin.echoMode = echoMode; 46 | } 47 | 48 | if (username == null || password == null) { 49 | return null; 50 | } 51 | 52 | return UserCredentials(username: username, password: password); 53 | } 54 | } 55 | 56 | Future main() async { 57 | HttpOverrides.global = 58 | _AhaClientHttpOverrides(AhaClient.defaultHostName, 443); 59 | 60 | final client = AhaClient(loginManager: ExampleLoginManager()); 61 | 62 | final response = await client.aha.getDeviceListInfos(); 63 | print(response.statusCode); 64 | print(''); 65 | if (response.isSuccessful) { 66 | print(response.body); 67 | } else { 68 | print(response.error); 69 | } 70 | 71 | await client.dispose(); 72 | } 73 | -------------------------------------------------------------------------------- /lib/aha_client.dart: -------------------------------------------------------------------------------- 1 | export 'package:enum_flag/enum_flag.dart'; 2 | 3 | export 'src/api/aha/aha_service.dart'; 4 | export 'src/api/aha/models/alert.dart'; 5 | export 'src/api/aha/models/avm_button.dart'; 6 | export 'src/api/aha/models/blind_state.dart'; 7 | export 'src/api/aha/models/button.dart'; 8 | export 'src/api/aha/models/color.dart'; 9 | export 'src/api/aha/models/color_control.dart'; 10 | export 'src/api/aha/models/color_defaults.dart'; 11 | export 'src/api/aha/models/device.dart'; 12 | export 'src/api/aha/models/device_list.dart'; 13 | export 'src/api/aha/models/device_stats.dart'; 14 | export 'src/api/aha/models/energy.dart'; 15 | export 'src/api/aha/models/function_bit_mask.dart'; 16 | export 'src/api/aha/models/group_info.dart'; 17 | export 'src/api/aha/models/hkr.dart'; 18 | export 'src/api/aha/models/hkr_temperature.dart'; 19 | export 'src/api/aha/models/hs_color.dart'; 20 | export 'src/api/aha/models/hs_defaults.dart'; 21 | export 'src/api/aha/models/level.dart'; 22 | export 'src/api/aha/models/level_control.dart'; 23 | export 'src/api/aha/models/name.dart'; 24 | export 'src/api/aha/models/next_change.dart'; 25 | export 'src/api/aha/models/percentage.dart'; 26 | export 'src/api/aha/models/power.dart'; 27 | export 'src/api/aha/models/power_meter.dart'; 28 | export 'src/api/aha/models/simple_on_off.dart'; 29 | export 'src/api/aha/models/stats.dart' hide StatsUnit; 30 | export 'src/api/aha/models/stats_group.dart'; 31 | export 'src/api/aha/models/subscription_state.dart'; 32 | export 'src/api/aha/models/switch.dart'; 33 | export 'src/api/aha/models/switch_action.dart'; 34 | export 'src/api/aha/models/switch_duration.dart'; 35 | export 'src/api/aha/models/switch_state.dart'; 36 | export 'src/api/aha/models/temp.dart'; 37 | export 'src/api/aha/models/temperature.dart'; 38 | export 'src/api/aha/models/temperature_defaults.dart'; 39 | export 'src/api/aha/models/timestamp.dart'; 40 | export 'src/api/aha/models/voltage.dart'; 41 | 42 | export 'src/api/aha_client.dart'; 43 | 44 | export 'src/api/login/authentication_exception.dart'; 45 | export 'src/api/login/login_info.dart'; 46 | export 'src/api/login/login_manager.dart'; 47 | export 'src/api/login/models/right.dart'; 48 | export 'src/api/login/models/sid.dart'; 49 | export 'src/api/login/models/user.dart'; 50 | export 'src/api/login/user_credentials.dart'; 51 | 52 | export 'src/api/util/text_converter.dart' show UnexpectedNullableResponse; 53 | export 'src/api/util/xml_converter.dart' show InvalidRootElement; 54 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/percentage.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'level.dart'; 7 | 8 | part 'percentage.freezed.dart'; 9 | part 'percentage.g.dart'; 10 | 11 | /// The percentage of a percentage controllable device in discrete values. 12 | /// 13 | /// {@macro aha_reference} 14 | @Freezed(makeCollectionsUnmodifiable: false) 15 | @xml.XmlSerializable() 16 | abstract class Percentage with _$Percentage implements IXmlSerializable { 17 | static const _invalidPercentageValue = -9999; 18 | 19 | /// A value indicating a missing/invalid percentage value. 20 | static const invalid = Percentage(); 21 | 22 | /// @nodoc 23 | @internal 24 | @xml.XmlSerializable(createMixin: true) 25 | @With.fromString(r'_$_$_PercentageXmlSerializableMixin') 26 | @Assert( 27 | '(rawValue >= 0 && rawValue <= 100) ' 28 | '|| rawValue == Percentage._invalidPercentageValue', 29 | 'rawValue must be in range [0, 100] or $_invalidPercentageValue', 30 | ) 31 | const factory Percentage({ 32 | @xml.XmlText() 33 | @Default(Percentage._invalidPercentageValue) 34 | @visibleForOverriding 35 | int rawValue, 36 | }) = _Percentage; 37 | 38 | /// @nodoc 39 | @internal 40 | factory Percentage.fromXmlElement(XmlElement element) => 41 | _$_$_PercentageFromXmlElement(element); 42 | 43 | /// Create a new percentage instance from the given [value]. 44 | /// 45 | /// [value] must be an double value in the range `[0, 1]`. 46 | factory Percentage.create(double value) { 47 | if (value < 0 || value > 1) { 48 | throw RangeError.range(value, 0, 1, 'value'); 49 | } 50 | return Percentage(rawValue: (value * 100).toInt()); 51 | } 52 | 53 | const Percentage._(); 54 | 55 | /// Checks if the percentage holds a valid value. 56 | bool isValid() => rawValue != _invalidPercentageValue; 57 | 58 | /// The double percentage value (0.0 to 1.0) 59 | double get value => rawValue != _invalidPercentageValue ? rawValue / 100 : 0; 60 | 61 | /// Convert this level into a [Level] object. 62 | /// 63 | /// A level of 0 means 0%, a level of 255 means 100%. 64 | Level toLevel() => Level(level: rawValue * 255 ~/ 100); 65 | 66 | @override 67 | String toString({bool pretty = false}) => pretty 68 | ? (rawValue != _invalidPercentageValue ? '$rawValue%' : 'invalid') 69 | : rawValue.toString(); 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/color_control.dart: -------------------------------------------------------------------------------- 1 | import 'package:color/color.dart'; 2 | import 'package:enum_flag/enum_flag.dart'; 3 | import 'package:freezed_annotation/freezed_annotation.dart'; 4 | import 'package:xml/xml.dart'; 5 | import 'package:xml_annotation/xml_annotation.dart' as xml; 6 | 7 | import '../../util/xml_serializable.dart'; 8 | import 'level_control.dart'; 9 | 10 | part 'color_control.freezed.dart'; 11 | part 'color_control.g.dart'; 12 | 13 | /// The different color control modes a color control can have 14 | @xml.XmlEnum() 15 | // ignore: prefer_mixin 16 | enum ColorControlMode with EnumFlag { 17 | @xml.XmlValue('1') 18 | // ignore: public_member_api_docs 19 | hueSaturationMode, 20 | // ignore: unused_field 21 | @xml.XmlValue('2') 22 | _reserved1, 23 | @xml.XmlValue('4') 24 | // ignore: public_member_api_docs 25 | colorTemperatureMode, 26 | @xml.XmlValue('') 27 | // ignore: public_member_api_docs 28 | unknown, 29 | } 30 | 31 | /// Status information about a color control device, typically a light bulb. 32 | /// 33 | /// {@macro aha_reference} 34 | @Freezed(makeCollectionsUnmodifiable: false) 35 | @xml.XmlSerializable() 36 | abstract class ColorControl with _$ColorControl implements IXmlSerializable { 37 | /// @nodoc 38 | @internal 39 | static const invalid = ColorControl(); 40 | 41 | /// @nodoc 42 | @internal 43 | @xml.XmlSerializable(createMixin: true) 44 | @With.fromString(r'_$_$_ColorControlXmlSerializableMixin') 45 | const factory ColorControl({ 46 | @xml.XmlAttribute(name: 'supported_modes') @Default(0) int supportedModes, 47 | @xml.XmlAttribute(name: 'current_mode') 48 | @Default(ColorControlMode.unknown) 49 | ColorControlMode currentMode, 50 | 51 | /// @nodoc 52 | @xml.XmlElement() @visibleForOverriding String? hue, 53 | 54 | /// @nodoc 55 | @xml.XmlElement() @visibleForOverriding String? saturation, 56 | @xml.XmlElement(name: 'temperature') int? temperatureKelvin, 57 | }) = _ColorControl; 58 | 59 | /// @nodoc 60 | @internal 61 | factory ColorControl.fromXmlElement(XmlElement element) => 62 | _$_$_ColorControlFromXmlElement(element); 63 | 64 | const ColorControl._(); 65 | 66 | /// Gets the current color in HSV representation. 67 | /// 68 | /// If [levelControl] is given, that level is used for the value of the HSV 69 | /// color. Otherwise, it is set to 100. 70 | HsvColor? getColor([LevelControl? levelControl]) { 71 | if (hue == null || saturation == null) { 72 | return null; 73 | } 74 | 75 | return HsvColor( 76 | int.parse(hue!), 77 | int.parse(saturation!) / 255 * 100, 78 | (levelControl?.levelPercentage.value ?? 1.0) * 100, 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/api/util/combined_converter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | /// @nodoc 7 | @internal 8 | class ConversionNotSupported extends UnsupportedError { 9 | /// @nodoc 10 | final Type type; 11 | 12 | /// @nodoc 13 | ConversionNotSupported(this.type, String mode) 14 | : super('Cannot convert $mode of type $type: No converter registered!'); 15 | } 16 | 17 | /// @nodoc 18 | @internal 19 | abstract class CombinableConverter implements Converter { 20 | /// @nodoc 21 | const CombinableConverter(); 22 | 23 | /// @nodoc 24 | List get supportedContentTypes; 25 | 26 | /// @nodoc 27 | FutureOr maybeConvertRequest(Request request); 28 | 29 | @override 30 | @nonVirtual 31 | FutureOr convertRequest(Request request) async { 32 | final convertedRequest = await maybeConvertRequest(request); 33 | if (convertedRequest != null) { 34 | return convertedRequest; 35 | } else { 36 | // ignore: avoid_dynamic_calls 37 | throw ConversionNotSupported(request.body.runtimeType, 'response'); 38 | } 39 | } 40 | } 41 | 42 | /// @nodoc 43 | @internal 44 | class CombinedConverter implements Converter { 45 | final List _subConverters; 46 | 47 | /// @nodoc 48 | CombinedConverter([List? converters]) 49 | : _subConverters = converters ?? []; 50 | 51 | /// @nodoc 52 | void addConverter(CombinableConverter converter) { 53 | _subConverters.add(converter); 54 | } 55 | 56 | @override 57 | FutureOr convertRequest(Request request) async { 58 | for (final converter in _subConverters) { 59 | final convertedRequest = await converter.maybeConvertRequest(request); 60 | if (convertedRequest != null) { 61 | return convertedRequest; 62 | } 63 | } 64 | 65 | throw ConversionNotSupported(request.runtimeType, 'request'); 66 | } 67 | 68 | @override 69 | FutureOr> convertResponse( 70 | Response response, 71 | ) { 72 | if (BodyType == _getType()) { 73 | return response as Response; 74 | } 75 | 76 | final responseContentType = 77 | response.headers[contentTypeKey]?.split(';').first; 78 | 79 | for (final converter in _subConverters) { 80 | final allowedContentTypes = converter.supportedContentTypes; 81 | if (!allowedContentTypes.contains(responseContentType)) { 82 | continue; 83 | } 84 | 85 | return converter.convertResponse(response); 86 | } 87 | 88 | throw ConversionNotSupported(BodyType, 'response'); 89 | } 90 | } 91 | 92 | Type _getType() => T; 93 | -------------------------------------------------------------------------------- /lib/src/api/login/models/right.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'right.freezed.dart'; 8 | part 'right.g.dart'; 9 | 10 | /// The different access types a user can have 11 | /// 12 | /// {@macro aha_reference} 13 | @xml.XmlEnum() 14 | enum AccessType { 15 | /// Can access the NAS 16 | @xml.XmlValue('NAS') 17 | nas, 18 | 19 | /// Can access the Apps 20 | @xml.XmlValue('App') 21 | app, 22 | 23 | /// Can access and configure the AHA 24 | @xml.XmlValue('HomeAuto') 25 | homeAuto, 26 | 27 | /// Is administrator 28 | @xml.XmlValue('BoxAdmin') 29 | boxAdmin, 30 | 31 | /// Can use the SIP APIs 32 | @xml.XmlValue('Phone') 33 | phone; 34 | } 35 | 36 | /// The different access levels a user can have for a [AccessType] 37 | /// 38 | /// {@macro aha_reference} 39 | @xml.XmlEnum() 40 | enum AccessLevel { 41 | /// No access 42 | @xml.XmlValue('0') 43 | none, 44 | 45 | /// Read-only access 46 | @xml.XmlValue('1') 47 | read, 48 | 49 | /// Read-Write access 50 | @xml.XmlValue('2') 51 | write, 52 | } 53 | 54 | /// The different access types and levels the current user has access to. 55 | /// 56 | /// {@macro aha_reference} 57 | @Freezed(makeCollectionsUnmodifiable: false) 58 | @xml.XmlSerializable() 59 | abstract class AccessRights with _$AccessRights implements IXmlSerializable { 60 | /// @nodoc 61 | @internal 62 | static const empty = AccessRights(); 63 | 64 | /// @nodoc 65 | @internal 66 | @xml.XmlSerializable(createMixin: true) 67 | @With.fromString(r'_$_$_AccessRightsXmlSerializableMixin') 68 | const factory AccessRights({ 69 | /// @nodoc 70 | @visibleForOverriding @xml.XmlElement(name: 'Name') List? names, 71 | 72 | /// @nodoc 73 | @visibleForOverriding 74 | @xml.XmlElement(name: 'Access') 75 | List? accesses, 76 | }) = _AccessRights; 77 | 78 | /// @nodoc 79 | @internal 80 | factory AccessRights.fromXmlElement(XmlElement element) => 81 | _$_$_AccessRightsFromXmlElement(element); 82 | 83 | const AccessRights._(); 84 | 85 | /// Get the [AccessLevel] for the given [accessType]. 86 | /// 87 | /// If the type is not know, [AccessLevel.none] is returned. 88 | AccessLevel getAccessLevelFor(AccessType accessType) { 89 | assert( 90 | names?.length == accesses?.length, 91 | 'names and accesses must have equal lengths', 92 | ); 93 | final nameIndex = names?.indexOf(accessType) ?? -1; 94 | if (nameIndex < 0) { 95 | return AccessLevel.none; 96 | } else { 97 | return accesses?[nameIndex] ?? AccessLevel.none; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /.vscode/dart.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Create XML serializable with root element": { 3 | "scope": "dart", 4 | "prefix": [ 5 | "xml-root", 6 | "xml-serializable-root" 7 | ], 8 | "body": [ 9 | "import 'package:freezed_annotation/freezed_annotation.dart';", 10 | "import 'package:xml/xml.dart';", 11 | "import 'package:xml_annotation/xml_annotation.dart' as xml;", 12 | "", 13 | "import 'package:aha_client/src/api/util/xml_serializable.dart';", 14 | "", 15 | "part '$TM_FILENAME_BASE.freezed.dart';", 16 | "part '$TM_FILENAME_BASE.g.dart';", 17 | "", 18 | "@Freezed(makeCollectionsUnmodifiable: false)", 19 | "@xml.XmlSerializable()", 20 | "abstract class ${1/(.)(.*)/${1:/upcase}$2/} with _$${1/(.)(.*)/${1:/upcase}$2/} implements IXmlConvertible {", 21 | " static const elementName = '${1/(.)(.*)/${1:/upcase}$2/}';", 22 | "", 23 | " static const invalid = ${1/(.)(.*)/${1:/upcase}$2/}();", 24 | "", 25 | " @xml.XmlSerializable(createMixin: true)", 26 | " @xml.XmlRootElement(name: ${1/(.)(.*)/${1:/upcase}$2/}.elementName)", 27 | " @With.fromString(r'_$$_$$_${1/(.)(.*)/${1:/upcase}$2/}XmlSerializableMixin')", 28 | " const factory ${1/(.)(.*)/${1:/upcase}$2/}({", 29 | " $0", 30 | " }) = _${1/(.)(.*)/${1:/upcase}$2/};", 31 | "", 32 | " factory ${1/(.)(.*)/${1:/upcase}$2/}.fromXmlElement(XmlElement element) =>", 33 | " _$$_$$_${1/(.)(.*)/${1:/upcase}$2/}FromXmlElement(element);", 34 | "}" 35 | ], 36 | "description": "Creates a XML serializable and convertible class", 37 | }, 38 | "Create XML serializable": { 39 | "scope": "dart", 40 | "prefix": [ 41 | "xml", 42 | "xml-serializable" 43 | ], 44 | "body": [ 45 | "import 'package:freezed_annotation/freezed_annotation.dart';", 46 | "import 'package:xml/xml.dart';", 47 | "import 'package:xml_annotation/xml_annotation.dart' as xml;", 48 | "", 49 | "import 'package:aha_client/src/api/util/xml_serializable.dart';", 50 | "", 51 | "part '$TM_FILENAME_BASE.freezed.dart';", 52 | "part '$TM_FILENAME_BASE.g.dart';", 53 | "", 54 | "@Freezed(makeCollectionsUnmodifiable: false)", 55 | "@xml.XmlSerializable()", 56 | "abstract class ${1/(.)(.*)/${1:/upcase}$2/} with _$${1/(.)(.*)/${1:/upcase}$2/} implements IXmlSerializable {", 57 | " static const invalid = ${1/(.)(.*)/${1:/upcase}$2/}();", 58 | "", 59 | " @xml.XmlSerializable(createMixin: true)", 60 | " @With.fromString(r'_$$_$$_${1/(.)(.*)/${1:/upcase}$2/}XmlSerializableMixin')", 61 | " const factory ${1/(.)(.*)/${1:/upcase}$2/}({", 62 | " $0", 63 | " }) = _${1/(.)(.*)/${1:/upcase}$2/};", 64 | "", 65 | " factory ${1/(.)(.*)/${1:/upcase}$2/}.fromXmlElement(XmlElement element) =>", 66 | " _$$_$$_${1/(.)(.*)/${1:/upcase}$2/}FromXmlElement(element);", 67 | "}" 68 | ], 69 | "description": "Creates a XML serializable and equatable class", 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/hkr_temperature.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'hkr_temperature.freezed.dart'; 8 | part 'hkr_temperature.g.dart'; 9 | 10 | /// The state of a thermostat. 11 | @freezed 12 | class HkrState with _$HkrState { 13 | /// Device is turned on for the given temperature. 14 | const factory HkrState( 15 | /// The configured temperature in celsius. 16 | double celsiusValue, 17 | ) = _Value; 18 | 19 | /// Device is fully turned on. 20 | const factory HkrState.on() = _On; 21 | 22 | /// Device is turned off. 23 | const factory HkrState.off() = _Off; 24 | 25 | /// Device state is not known. 26 | const factory HkrState.invalid() = _Invalid; 27 | } 28 | 29 | /// The temperature configuration state of a thermostat. 30 | /// 31 | /// {@macro aha_reference} 32 | @Freezed(makeCollectionsUnmodifiable: false) 33 | @xml.XmlSerializable() 34 | abstract class HkrTemperature 35 | with _$HkrTemperature 36 | implements IXmlSerializable { 37 | static const _offValue = 253; 38 | static const _onValue = 254; 39 | static const _invalidValue = 255; 40 | 41 | /// @nodoc 42 | @internal 43 | static const invalid = HkrTemperature(); 44 | 45 | /// @nodoc 46 | @internal 47 | @xml.XmlSerializable(createMixin: true) 48 | @With.fromString(r'_$_$_HkrTemperatureXmlSerializableMixin') 49 | const factory HkrTemperature({ 50 | @xml.XmlText() 51 | @visibleForOverriding 52 | @Default(HkrTemperature._invalidValue) 53 | int rawValue, 54 | }) = _HkrTemperature; 55 | 56 | /// @nodoc 57 | @internal 58 | factory HkrTemperature.fromXmlElement(XmlElement element) => 59 | _$_$_HkrTemperatureFromXmlElement(element); 60 | 61 | /// @nodoc 62 | @internal 63 | factory HkrTemperature.fromString(String rawValue) => 64 | HkrTemperature(rawValue: int.parse(rawValue)); 65 | 66 | /// Create a new HkrTemperature object from the given [value]. 67 | factory HkrTemperature.create(HkrState value) => value.when( 68 | (celsiusValue) => HkrTemperature(rawValue: (celsiusValue * 2).toInt()), 69 | on: () => const HkrTemperature(rawValue: _onValue), 70 | off: () => const HkrTemperature(rawValue: _offValue), 71 | invalid: () => HkrTemperature.invalid, 72 | ); 73 | 74 | const HkrTemperature._(); 75 | 76 | /// The current state of the thermostat 77 | HkrState get state { 78 | if (rawValue == _onValue) { 79 | return const HkrState.on(); 80 | } else if (rawValue == _offValue) { 81 | return const HkrState.off(); 82 | } else if (rawValue == _invalidValue) { 83 | return const HkrState.invalid(); 84 | } else { 85 | return HkrState(rawValue / 2); 86 | } 87 | } 88 | 89 | @override 90 | String toString({bool pretty = false}) => 91 | pretty ? state.toString() : rawValue.toString(); 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/api/util/text_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import 'combined_converter.dart'; 5 | 6 | /// An error that indicated that the response contains an `inval` value where 7 | /// no such value is allowed. 8 | class UnexpectedNullableResponse implements Exception { 9 | /// The type that the response should have been converted to. 10 | final Type type; 11 | 12 | /// Default constructor. 13 | UnexpectedNullableResponse(this.type); 14 | 15 | @override 16 | String toString() => 17 | 'Response has "inval" value which indicates a null response, ' 18 | 'but response type $type does not allow invalid/null values!'; 19 | } 20 | 21 | /// @nodoc 22 | @internal 23 | typedef FromTextFactory = T Function(String); 24 | 25 | /// @nodoc 26 | @internal 27 | class TextConverter extends CombinableConverter { 28 | static const _invalidValue = 'inval'; 29 | 30 | static const _textContentType = 'text/plain'; 31 | 32 | final _fromTextFactories = >{ 33 | String: _identity, 34 | int: int.parse, 35 | double: double.parse, 36 | bool: _boolFromString, 37 | }; 38 | 39 | /// @nodoc 40 | void registerResponseConverter( 41 | FromTextFactory converter, 42 | ) => 43 | _fromTextFactories[T] = converter; 44 | 45 | @override 46 | List get supportedContentTypes => const [_textContentType]; 47 | 48 | @override 49 | Request? maybeConvertRequest(Request request) => request.copyWith( 50 | body: _encode(request.body), 51 | ); 52 | 53 | @override 54 | Response convertResponse( 55 | Response response, 56 | ) => 57 | response.copyWith( 58 | body: _decode(response.bodyString), 59 | ); 60 | 61 | String _encode(dynamic data) { 62 | if (data is Iterable) { 63 | return data.map(_encode).join(','); 64 | } else if (data == null) { 65 | return _invalidValue; 66 | } else { 67 | return data.toString(); 68 | } 69 | } 70 | 71 | TBody _decode(String data) { 72 | if (data == _invalidValue) { 73 | // ignore: prefer_void_to_null 74 | if (isTypeOf()) { 75 | return null as TBody; 76 | } else { 77 | throw UnexpectedNullableResponse(TBody); 78 | } 79 | } else if (isTypeOf>()) { 80 | return data.split(',').map(_decode).toList() as TBody; 81 | } else { 82 | return _getFactory()(data); 83 | } 84 | } 85 | 86 | FromTextFactory _getFactory() { 87 | final factory = _fromTextFactories[T]; 88 | if (factory == null) { 89 | throw ConversionNotSupported(T, 'response'); 90 | } 91 | 92 | return factory as FromTextFactory; 93 | } 94 | } 95 | 96 | T _identity(T t) => t; 97 | 98 | bool _boolFromString(String s) { 99 | switch (s.toLowerCase()) { 100 | case '0': 101 | case 'no': 102 | case 'false': 103 | return false; 104 | case '1': 105 | case 'yes': 106 | case 'true': 107 | return true; 108 | default: 109 | throw ArgumentError.value(s); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/hkr.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'hkr_temperature.dart'; 7 | import 'next_change.dart'; 8 | import 'percentage.dart'; 9 | import 'switch_state.dart'; 10 | import 'timestamp.dart'; 11 | 12 | part 'hkr.freezed.dart'; 13 | part 'hkr.g.dart'; 14 | 15 | /// Possible error states of a thermostat 16 | @xml.XmlEnum() 17 | enum HkrError { 18 | @xml.XmlValue('0') 19 | // ignore: public_member_api_docs 20 | noError, 21 | @xml.XmlValue('1') 22 | // ignore: public_member_api_docs 23 | incorrectInstallation, 24 | @xml.XmlValue('2') 25 | // ignore: public_member_api_docs 26 | incompatibleValveOrLowBattery, 27 | @xml.XmlValue('3') 28 | // ignore: public_member_api_docs 29 | valveBlocked, 30 | @xml.XmlValue('4') 31 | // ignore: public_member_api_docs 32 | preparingInstallation, 33 | @xml.XmlValue('5') 34 | // ignore: public_member_api_docs 35 | installationReady, 36 | @xml.XmlValue('6') 37 | // ignore: public_member_api_docs 38 | installationInProgress, 39 | } 40 | 41 | /// The configuration of a thermostat. 42 | /// 43 | /// {@macro aha_reference} 44 | @Freezed(makeCollectionsUnmodifiable: false) 45 | @xml.XmlSerializable() 46 | abstract class Hkr with _$Hkr implements IXmlSerializable { 47 | /// @nodoc 48 | @internal 49 | static const invalid = Hkr(); 50 | 51 | /// @nodoc 52 | @internal 53 | @xml.XmlSerializable(createMixin: true) 54 | @With.fromString(r'_$_$_HkrXmlSerializableMixin') 55 | const factory Hkr({ 56 | @xml.XmlElement(name: 'tist') 57 | @Default(HkrTemperature.invalid) 58 | HkrTemperature tIst, 59 | @xml.XmlElement(name: 'tsoll') 60 | @Default(HkrTemperature.invalid) 61 | HkrTemperature tSoll, 62 | @xml.XmlElement() @Default(HkrTemperature.invalid) HkrTemperature absenk, 63 | @xml.XmlElement() @Default(HkrTemperature.invalid) HkrTemperature komfort, 64 | @xml.XmlElement() @Default(SwitchState.invalid) SwitchState lock, 65 | @xml.XmlElement(name: 'devicelock') 66 | @Default(SwitchState.invalid) 67 | SwitchState deviceLock, 68 | @xml.XmlElement(name: 'errorcode') 69 | @Default(HkrError.noError) 70 | HkrError errorCode, 71 | @xml.XmlElement(name: 'windowopenactiv') 72 | @Default(false) 73 | bool windowOpenActive, 74 | @xml.XmlElement(name: 'windowopenactiveendtime') 75 | @Default(Timestamp.deactivated) 76 | Timestamp windowOpenActiveEndTime, 77 | @xml.XmlElement(name: 'boostactive') @Default(false) bool boostActive, 78 | @xml.XmlElement(name: 'boostactiveendtime') 79 | @Default(Timestamp.deactivated) 80 | Timestamp boostActiveEndTime, 81 | @xml.XmlElement(name: 'batterylow') @Default(false) bool batteryLow, 82 | @xml.XmlElement() @Default(Percentage.invalid) Percentage battery, 83 | @xml.XmlElement(name: 'nextchange') 84 | @Default(NextChange.invalid) 85 | NextChange nextChange, 86 | @xml.XmlElement(name: 'summeractive') @Default(false) bool summerActive, 87 | @xml.XmlElement(name: 'holidayactive') @Default(false) bool holidayActive, 88 | }) = _Hkr; 89 | 90 | /// @nodoc 91 | @internal 92 | factory Hkr.fromXmlElement(XmlElement element) => 93 | _$_$_HkrFromXmlElement(element); 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/timestamp.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'timestamp.freezed.dart'; 8 | part 'timestamp.g.dart'; 9 | 10 | /// A timestamp for an event. 11 | /// 12 | /// {@macro aha_reference} 13 | @Freezed(makeCollectionsUnmodifiable: false) 14 | @xml.XmlSerializable() 15 | abstract class Timestamp with _$Timestamp implements IXmlSerializable { 16 | /// A deactivated timestamp, meaning that no event is planned or to deactivate 17 | /// a planned event. 18 | static const deactivated = Timestamp(); 19 | 20 | /// @nodoc 21 | @internal 22 | @xml.XmlSerializable(createMixin: true) 23 | @With.fromString(r'_$_$_TimestampXmlSerializableMixin') 24 | const factory Timestamp({ 25 | @xml.XmlText() @Default('0') @visibleForOverriding String rawValue, 26 | }) = _Timestamp; 27 | 28 | /// @nodoc 29 | @internal 30 | factory Timestamp.fromXmlElement(XmlElement element) => 31 | _$_$_TimestampFromXmlElement(element); 32 | 33 | /// @nodoc 34 | @internal 35 | factory Timestamp.fromString(String rawValue) => 36 | Timestamp(rawValue: rawValue); 37 | 38 | /// Creates a new timestamp from a [dateTime] value. 39 | /// 40 | /// The [dateTime] must not be in the past and not further in the future then 41 | /// 24 hours. 42 | /// 43 | /// By default, [DateTime.now] is used to calculate if the [dateTime] is 44 | /// valid. For testing purposes, that can be overwritten with [now]. 45 | factory Timestamp.create(DateTime dateTime, {DateTime? now}) { 46 | final actualNow = now ?? DateTime.now(); 47 | if (dateTime.isBefore(actualNow) || 48 | dateTime.isAfter(actualNow.add(const Duration(days: 1)))) { 49 | throw ArgumentError.value( 50 | dateTime, 51 | 'dateTime', 52 | 'Must not be in the past and at most 24 hours in the future ' 53 | '(now: $actualNow)', 54 | ); 55 | } 56 | 57 | return Timestamp( 58 | rawValue: Duration(milliseconds: dateTime.millisecondsSinceEpoch) 59 | .inSeconds 60 | .toString(), 61 | ); 62 | } 63 | 64 | const Timestamp._(); 65 | 66 | /// The offset since the epoch start (1970-01-01). 67 | /// 68 | /// Can be null if no timestamp is set. 69 | Duration? get epochOffset { 70 | final secondsSinceEpoch = int.tryParse(rawValue, radix: 10) ?? 0; 71 | return secondsSinceEpoch > 0 ? Duration(seconds: secondsSinceEpoch) : null; 72 | } 73 | 74 | /// The point in time of this timestamp as local time. 75 | DateTime? get localTime { 76 | final offset = epochOffset; 77 | return offset != null 78 | ? DateTime.fromMillisecondsSinceEpoch(offset.inMilliseconds) 79 | : null; 80 | } 81 | 82 | /// The point in time of this timestamp as UTC time. 83 | DateTime? get utc { 84 | final offset = epochOffset; 85 | return offset != null 86 | ? DateTime.fromMillisecondsSinceEpoch( 87 | offset.inMilliseconds, 88 | isUtc: true, 89 | ) 90 | : null; 91 | } 92 | 93 | /// Checks if this timestamp is active or [deactivated]. 94 | bool get isActive => epochOffset != null; 95 | 96 | @override 97 | String toString({bool pretty = false}) => 98 | pretty ? localTime.toString() : rawValue; 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/stats.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | 7 | part 'stats.freezed.dart'; 8 | part 'stats.g.dart'; 9 | 10 | /// @nodoc 11 | @internal 12 | abstract class StatsUnit {} 13 | 14 | /// Indicates that the measurements are celsius values. 15 | abstract class StatsUnitCelsius implements StatsUnit {} 16 | 17 | /// Indicates that the measurements are Volt values. 18 | abstract class StatsUnitVolt implements StatsUnit {} 19 | 20 | /// Indicates that the measurements are Watt values. 21 | abstract class StatsUnitWatt implements StatsUnit {} 22 | 23 | /// Indicates that the measurements are Watt hour values. 24 | abstract class StatsUnitWattHours implements StatsUnit {} 25 | 26 | /// Indicates that the measurements are percentage values. 27 | abstract class StatsUnitPercent implements StatsUnit {} 28 | 29 | /// A statistics for a measurement. 30 | /// 31 | /// {@macro aha_reference} 32 | @Freezed(makeCollectionsUnmodifiable: false) 33 | @xml.XmlSerializable() 34 | abstract class Stats 35 | with _$Stats 36 | implements IXmlSerializable { 37 | static const _separatorValue = ','; 38 | static const _invalidValue = '-'; 39 | 40 | /// @nodoc 41 | @internal 42 | static const invalid = Stats(); 43 | 44 | /// @nodoc 45 | @internal 46 | @xml.XmlSerializable(createMixin: true) 47 | @With.fromString(r'_$_$_StatsXmlSerializableMixin') 48 | const factory Stats({ 49 | @xml.XmlAttribute() @Default(0) int count, 50 | 51 | /// @nodoc 52 | @xml.XmlAttribute(name: 'grid') 53 | @visibleForOverriding 54 | @Default(0) 55 | int rawGrid, 56 | 57 | /// @nodoc 58 | @xml.XmlText() @visibleForOverriding @Default('') String rawValues, 59 | }) = _Stats; 60 | 61 | /// @nodoc 62 | @internal 63 | factory Stats.fromXmlElement(XmlElement element) => 64 | _$_$_StatsFromXmlElement(element).cast(); 65 | 66 | const Stats._(); 67 | 68 | // ignore: public_member_api_docs 69 | Duration get grid => Duration(seconds: rawGrid); 70 | 71 | // ignore: public_member_api_docs 72 | List get values => 73 | rawValues.split(_separatorValue).map(_parseElement).toList(); 74 | 75 | /// Returns the value at the given [index]. 76 | double operator [](int index) => _parseElement( 77 | rawValues.split(_separatorValue).elementAt(index), 78 | ); 79 | 80 | /// @nodoc 81 | @internal 82 | Stats cast() => Stats( 83 | count: count, 84 | rawGrid: rawGrid, 85 | rawValues: rawValues, 86 | ); 87 | 88 | static double _parseElement(String value) { 89 | final intValue = value == _invalidValue ? null : int.parse(value); 90 | if (intValue == null) { 91 | return double.nan; 92 | } 93 | 94 | switch (TUnit) { 95 | case const (StatsUnitCelsius): 96 | return intValue / 10; 97 | case const (StatsUnitVolt): 98 | return intValue / 1000; 99 | case const (StatsUnitWatt): 100 | return intValue / 100; 101 | case const (StatsUnitWattHours): 102 | return intValue.toDouble(); 103 | case const (StatsUnitPercent): 104 | return intValue / 100; 105 | default: 106 | throw StateError('Unknown unit type $TUnit'); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/api/util/xml_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:meta/meta.dart'; 3 | import 'package:xml/xml.dart'; 4 | 5 | import 'combined_converter.dart'; 6 | import 'xml_serializable.dart'; 7 | 8 | /// An error that indicates that the XML document cannot be deserialized because 9 | /// the root element of the document is not as expected. 10 | class InvalidRootElement implements Exception { 11 | /// The type that the XML could not be deserialized to. 12 | final Type type; 13 | 14 | /// A list of elements that are supported for the given [type]. 15 | final List exceptedElements; 16 | 17 | /// The actual XML name of the root element of the document. 18 | final XmlName actualElement; 19 | 20 | /// Default constructor. 21 | InvalidRootElement(this.type, this.exceptedElements, this.actualElement); 22 | 23 | @override 24 | String toString() => 'Invalid root element for type $type: ' 25 | 'Expected ${exceptedElements.map((e) => '<$e>').join(' or ')}, ' 26 | 'but was $actualElement'; 27 | } 28 | 29 | /// @nodoc 30 | @internal 31 | typedef FromXmlFactory = T Function(XmlElement); 32 | 33 | class _XmlConverter { 34 | final String rootElementName; 35 | final FromXmlFactory fromXmlElement; 36 | final List? additionalElementNames; 37 | 38 | const _XmlConverter( 39 | this.rootElementName, 40 | this.fromXmlElement, 41 | this.additionalElementNames, 42 | ); 43 | 44 | List get allowedElementNames => 45 | [rootElementName, ...?additionalElementNames]; 46 | } 47 | 48 | /// @nodoc 49 | @internal 50 | class XmlConverter extends CombinableConverter { 51 | static const _xmlContentType = 'text/xml'; 52 | 53 | final _xmlFactories = >{}; 54 | 55 | /// @nodoc 56 | void registerResponseConverter( 57 | String element, 58 | FromXmlFactory fromXmlElement, [ 59 | List? additionalElementNames, 60 | ]) => 61 | _xmlFactories[T] = _XmlConverter( 62 | element, 63 | fromXmlElement, 64 | additionalElementNames, 65 | ); 66 | 67 | @override 68 | List get supportedContentTypes => 69 | const [_xmlContentType, 'application/xml']; 70 | 71 | @override 72 | Request? maybeConvertRequest(Request request) { 73 | final dynamic body = request.body; 74 | if (body is IXmlConvertible) { 75 | return _xmlRequest(request).copyWith( 76 | body: body.toXmlElement().toXmlString(), 77 | ); 78 | } else if (body is XmlElement) { 79 | return _xmlRequest(request).copyWith(body: body.toXmlString()); 80 | } else if (body is XmlDocument) { 81 | return _xmlRequest(request).copyWith(body: body.toXmlString()); 82 | } 83 | 84 | return null; 85 | } 86 | 87 | @override 88 | Response convertResponse( 89 | Response response, 90 | ) { 91 | final xmlDocument = XmlDocument.parse(response.bodyString); 92 | if (BodyType == XmlDocument) { 93 | return response.copyWith(body: xmlDocument as BodyType); 94 | } 95 | 96 | final rootElement = xmlDocument.rootElement; 97 | if (BodyType == XmlElement) { 98 | return response.copyWith(body: rootElement as BodyType); 99 | } 100 | 101 | final factory = _getFactory(rootElement.name); 102 | return response.copyWith( 103 | body: factory(rootElement), 104 | ); 105 | } 106 | 107 | Request _xmlRequest(Request request) => 108 | applyHeader(request, contentTypeKey, _xmlContentType); 109 | 110 | FromXmlFactory _getFactory(XmlName rootElementName) { 111 | final factory = _xmlFactories[T]; 112 | if (factory == null) { 113 | throw ConversionNotSupported(T, 'response'); 114 | } 115 | 116 | if (!factory.allowedElementNames.contains(rootElementName.local)) { 117 | throw InvalidRootElement(T, factory.allowedElementNames, rootElementName); 118 | } 119 | 120 | return factory.fromXmlElement as FromXmlFactory; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/api/aha_client.dart: -------------------------------------------------------------------------------- 1 | import 'package:chopper/chopper.dart'; 2 | import 'package:http/http.dart'; 3 | 4 | import 'aha/aha_service.dart'; 5 | import 'aha/models/color_defaults.dart'; 6 | import 'aha/models/device.dart'; 7 | import 'aha/models/device_list.dart'; 8 | import 'aha/models/device_stats.dart'; 9 | import 'aha/models/energy.dart'; 10 | import 'aha/models/hkr_temperature.dart'; 11 | import 'aha/models/power.dart'; 12 | import 'aha/models/subscription_state.dart'; 13 | import 'aha/models/temperature.dart'; 14 | import 'aha/models/timestamp.dart'; 15 | import 'login/login_manager.dart'; 16 | import 'login/login_service.dart'; 17 | import 'login/models/session_info.dart'; 18 | import 'util/combined_converter.dart'; 19 | import 'util/text_converter.dart'; 20 | import 'util/xml_converter.dart'; 21 | 22 | /// The AHA Api client. 23 | /// 24 | /// This client handles both authentication and the actual API access. Please 25 | /// refer to the official documentation on how the API works. 26 | /// 27 | /// {@template aha_reference} 28 | /// See https://avm.de/service/schnittstellen/ 29 | /// {@endtemplate} 30 | class AhaClient { 31 | /// The default host name of a fritz.box. 32 | static const defaultHostName = 'fritz.box'; 33 | 34 | /// The underlying chopper client being used for the API requests. 35 | final ChopperClient client; 36 | 37 | /// The login manager that can be used to handle sessions. 38 | /// 39 | /// Provides methods to log in and out of the remote and to check for session 40 | /// validity. 41 | final LoginManager loginManager; 42 | 43 | /// Default constructor. 44 | /// 45 | /// By default, the client connects to http://fritz.box:80. Can be configured 46 | /// via [hostName] and [port]. 47 | /// 48 | /// The [loginManager] parameter is required and should be your extension of 49 | /// the [LoginManager] to handle user authentication. 50 | /// 51 | /// Finally, a custom [httpClient] can be provided to be passed to chopper. If 52 | /// given, you are responsible for disposing that client. 53 | AhaClient({ 54 | String hostName = defaultHostName, 55 | int? port, 56 | required this.loginManager, 57 | Client? httpClient, 58 | }) : client = ChopperClient( 59 | client: httpClient, 60 | baseUrl: Uri( 61 | scheme: 'https', 62 | host: hostName, 63 | port: port, 64 | ), 65 | converter: CombinedConverter([ 66 | XmlConverter() 67 | ..registerResponseConverter( 68 | SessionInfo.elementName, 69 | SessionInfo.fromXmlElement, 70 | ) 71 | ..registerResponseConverter( 72 | DeviceList.elementName, 73 | DeviceList.fromXmlElement, 74 | ) 75 | ..registerResponseConverter( 76 | Device.deviceElementName, 77 | Device.fromXmlElement, 78 | const [Device.groupElementName], 79 | ) 80 | ..registerResponseConverter( 81 | DeviceStats.elementName, 82 | DeviceStats.fromXmlElement, 83 | ) 84 | ..registerResponseConverter( 85 | ColorDefaults.elementName, 86 | ColorDefaults.fromXmlElement, 87 | ) 88 | ..registerResponseConverter( 89 | State.elementName, 90 | State.fromXmlElement, 91 | ), 92 | TextConverter() 93 | ..registerResponseConverter(Power.fromString) 94 | ..registerResponseConverter(Energy.fromString) 95 | ..registerResponseConverter(Temperature.fromString) 96 | ..registerResponseConverter(HkrTemperature.fromString) 97 | ..registerResponseConverter(Timestamp.fromString), 98 | ]), 99 | interceptors: [ 100 | loginManager, 101 | ], 102 | authenticator: loginManager, 103 | services: [ 104 | LoginService.create(), 105 | AhaService.create(), 106 | ], 107 | ) { 108 | loginManager.setup(client.getService()); 109 | } 110 | 111 | /// Dispose of the client. 112 | /// 113 | /// By default, this will automatically logout the user. 114 | Future dispose({bool withLogout = true}) async { 115 | if (withLogout) { 116 | await loginManager.logout(); 117 | } 118 | 119 | client.dispose(); 120 | } 121 | 122 | /// The AHA API client to access the actual API. 123 | late final AhaService aha = client.getService(); 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/api/aha/models/device.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:xml/xml.dart'; 3 | import 'package:xml_annotation/xml_annotation.dart' as xml; 4 | 5 | import '../../util/xml_serializable.dart'; 6 | import 'alert.dart'; 7 | import 'avm_button.dart'; 8 | import 'button.dart'; 9 | import 'color_control.dart'; 10 | import 'group_info.dart'; 11 | import 'hkr.dart'; 12 | import 'level_control.dart'; 13 | import 'percentage.dart'; 14 | import 'power_meter.dart'; 15 | import 'simple_on_off.dart'; 16 | import 'switch.dart'; 17 | import 'temperature.dart'; 18 | 19 | part 'device.freezed.dart'; 20 | part 'device.g.dart'; 21 | 22 | /// A device with all it's data points. 23 | /// 24 | /// {@macro aha_reference} 25 | @Freezed(makeCollectionsUnmodifiable: false) 26 | @xml.XmlSerializable() 27 | abstract class Device with _$Device implements IXmlConvertible { 28 | /// @nodoc 29 | @internal 30 | static const deviceElementName = 'device'; 31 | 32 | /// @nodoc 33 | @internal 34 | static const groupElementName = 'group'; 35 | 36 | /// @nodoc 37 | @internal 38 | static const invalidDevice = Device(); 39 | 40 | /// @nodoc 41 | @internal 42 | static const invalidGroup = DeviceGroup(); 43 | 44 | /// @nodoc 45 | @internal 46 | @xml.XmlSerializable(createMixin: true) 47 | @xml.XmlRootElement(name: Device.deviceElementName) 48 | @With.fromString(r'_$_$_DeviceXmlSerializableMixin') 49 | const factory Device({ 50 | @xml.XmlAttribute() @Default(0) int id, 51 | @xml.XmlAttribute() @Default('') String identifier, 52 | @xml.XmlAttribute(name: 'fwversion') @Default('') String fwVersion, 53 | @xml.XmlAttribute() @Default('') String manufacturer, 54 | @xml.XmlAttribute(name: 'productname') @Default('') String productName, 55 | @xml.XmlAttribute(name: 'functionbitmask') @Default(0) int functionBitMask, 56 | @xml.XmlElement() @Default(false) bool present, 57 | @xml.XmlElement(name: 'txbusy') @Default(false) bool txBusy, 58 | @xml.XmlElement() @Default('') String name, 59 | @xml.XmlElement() Percentage? battery, 60 | @xml.XmlElement(name: 'batterylow') bool? batteryLow, 61 | @xml.XmlElement(name: 'switch') Switch? switch_, 62 | @xml.XmlElement(name: 'powermeter') PowerMeter? powerMeter, 63 | @xml.XmlElement(name: 'temperature') Temperature? temperature, 64 | @xml.XmlElement() Alert? alert, 65 | @xml.XmlElement(name: 'button') List