From 4fa38b691edf11fca6be90254a7d1a0d31cb9c7b Mon Sep 17 00:00:00 2001 From: Fr4nz D13trich Date: Mon, 24 Nov 2025 09:22:21 +0100 Subject: [PATCH] Repo created --- .gitattributes | 2 + .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug-report.md | 31 + .github/ISSUE_TEMPLATE/feature-request.md | 18 + .github/workflows/ci.yml | 72 ++ .gitignore | 89 ++ .idea/AndroidProjectSystem.xml | 6 + .idea/codeStyles/Project.xml | 123 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/compiler.xml | 6 + .idea/deploymentTargetDropDown.xml | 10 + .idea/deploymentTargetSelector.xml | 10 + .idea/encodings.xml | 4 + .idea/inspectionProfiles/Strict.xml | 1006 +++++++++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/kotlinc.xml | 6 + .idea/migrations.xml | 10 + .idea/misc.xml | 58 + .idea/runConfigurations.xml | 17 + LICENSE | 674 +++++++++++ README.md | 57 +- app/.gitignore | 1 + app/build.gradle.kts | 103 ++ app/detekt-config.yml | 784 +++++++++++++ app/proguard-rules.pro | 27 + app/src/main/AndroidManifest.xml | 157 +++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 48568 bytes .../java/com/rine/upnpdiscovery/UPnPDevice.kt | 110 ++ .../com/rine/upnpdiscovery/UPnPDiscovery.kt | 178 +++ .../github/domi04151309/home/Application.kt | 10 + .../home/activities/AboutActivity.kt | 112 ++ .../home/activities/BaseActivity.kt | 22 + .../home/activities/ControlInfoActivity.kt | 70 ++ .../activities/ControlInfoActivityHueRoom.kt | 97 ++ .../home/activities/DeviceInfoActivity.kt | 238 ++++ .../home/activities/DevicesActivity.kt | 152 +++ .../home/activities/EditDeviceActivity.kt | 387 +++++++ .../home/activities/HueConnectActivity.kt | 76 ++ .../home/activities/HueLampActivity.kt | 266 +++++ .../home/activities/HueSceneActivity.kt | 362 ++++++ .../home/activities/LibraryActivity.kt | 45 + .../home/activities/MainActivity.kt | 616 ++++++++++ .../home/activities/SearchDevicesActivity.kt | 118 ++ .../home/activities/SettingsActivity.kt | 82 ++ .../home/activities/ShortcutDeviceActivity.kt | 82 ++ .../activities/ShortcutHueRoomActivity.kt | 122 ++ .../ShortcutHueSceneActionActivity.kt | 26 + .../activities/ShortcutHueSceneActivity.kt | 181 +++ .../ShortcutTasmotaActionActivity.kt | 45 + .../activities/ShortcutTasmotaActivity.kt | 125 ++ .../home/activities/WebActivity.kt | 154 +++ .../activities/WebActivityWebViewClient.kt | 191 ++++ .../adapters/DeviceDiscoveryListAdapter.kt | 76 ++ .../home/adapters/DeviceListAdapter.kt | 60 + .../home/adapters/HueDetailsTabAdapter.kt | 24 + .../home/adapters/HueLampListAdapter.kt | 110 ++ .../home/adapters/HueSceneGridAdapter.kt | 79 ++ .../home/adapters/HueSceneLampListAdapter.kt | 115 ++ .../home/adapters/IconSpinnerAdapter.kt | 62 + .../home/adapters/MainListAdapter.kt | 209 ++++ .../home/adapters/SimpleListAdapter.kt | 48 + .../domi04151309/home/api/EspEasyAPI.kt | 86 ++ .../domi04151309/home/api/EspEasyAPIParser.kt | 118 ++ .../io/github/domi04151309/home/api/HueAPI.kt | 275 +++++ .../domi04151309/home/api/HueAPIParser.kt | 174 +++ .../github/domi04151309/home/api/ShellyAPI.kt | 244 ++++ .../domi04151309/home/api/ShellyAPIParser.kt | 233 ++++ .../domi04151309/home/api/SimpleHomeAPI.kt | 137 +++ .../home/api/SimpleHomeAPIParser.kt | 34 + .../github/domi04151309/home/api/Tasmota.kt | 97 ++ .../domi04151309/home/api/UnifiedAPI.kt | 89 ++ .../home/custom/CustomJsonArrayRequest.kt | 33 + .../home/custom/JsonObjectRequestAuth.kt | 26 + .../domi04151309/home/custom/TextWatcher.kt | 28 + .../domi04151309/home/data/DeviceItem.kt | 31 + .../domi04151309/home/data/LightStates.kt | 92 ++ .../domi04151309/home/data/ListViewItem.kt | 19 + .../domi04151309/home/data/SceneGridItem.kt | 7 + .../domi04151309/home/data/SceneListItem.kt | 9 + .../domi04151309/home/data/SimpleListItem.kt | 8 + .../home/data/UnifiedRequestCallback.kt | 7 + .../NetworkServiceDiscoveryListener.kt | 57 + .../NetworkServiceResolveListener.kt | 84 ++ .../home/discovery/UPnPListener.kt | 69 ++ .../home/fragments/ControlInfoFragment.kt | 33 + .../home/fragments/HueColorFragment.kt | 238 ++++ .../home/fragments/HueColorSheet.kt | 225 ++++ .../home/fragments/HueLampsFragment.kt | 171 +++ .../home/fragments/HueScenesFragment.kt | 261 +++++ .../domi04151309/home/helpers/ColorUtils.kt | 62 + .../home/helpers/DeviceSecrets.kt | 54 + .../domi04151309/home/helpers/Devices.kt | 146 +++ .../domi04151309/home/helpers/Global.kt | 157 +++ .../home/helpers/HueLightListener.kt | 20 + .../domi04151309/home/helpers/HueUtils.kt | 66 ++ .../io/github/domi04151309/home/helpers/P.kt | 10 + .../domi04151309/home/helpers/SliderUtils.kt | 68 ++ .../home/helpers/TasmotaHelper.kt | 149 +++ .../home/helpers/UpdateHandler.kt | 32 + .../HomeRecyclerViewHelperInterface.kt | 17 + .../interfaces/HueAdvancedLampInterface.kt | 12 + .../home/interfaces/HueLampInterface.kt | 12 + .../home/interfaces/HueRoomInterface.kt | 9 + .../interfaces/RecyclerViewHelperInterface.kt | 10 + .../RecyclerViewHelperInterfaceAdvanced.kt | 7 + .../SceneRecyclerViewHelperInterface.kt | 17 + .../home/services/ControlBuilders.kt | 163 +++ .../home/services/ControlService.kt | 194 ++++ .../main/res/drawable-nodpi/header_bg.webp | Bin 0 -> 101230 bytes .../res/drawable/ic_about_contributor.xml | 9 + app/src/main/res/drawable/ic_about_github.xml | 9 + app/src/main/res/drawable/ic_about_info.xml | 9 + .../main/res/drawable/ic_about_library.xml | 9 + .../main/res/drawable/ic_about_palette.xml | 9 + app/src/main/res/drawable/ic_add.xml | 9 + app/src/main/res/drawable/ic_arrow_back.xml | 9 + app/src/main/res/drawable/ic_buttonpress.xml | 11 + app/src/main/res/drawable/ic_circle.xml | 11 + .../main/res/drawable/ic_color_palette.xml | 9 + app/src/main/res/drawable/ic_delete.xml | 9 + .../drawable/ic_device_christmas_tree.webp | Bin 0 -> 870 bytes app/src/main/res/drawable/ic_device_clock.xml | 27 + .../main/res/drawable/ic_device_display.xml | 12 + .../res/drawable/ic_device_display_alt.xml | 12 + .../main/res/drawable/ic_device_docker.xml | 18 + .../res/drawable/ic_device_electricity.xml | 10 + app/src/main/res/drawable/ic_device_gauge.xml | 20 + .../main/res/drawable/ic_device_grafana.xml | 11 + .../res/drawable/ic_device_hygrometer.xml | 12 + app/src/main/res/drawable/ic_device_lamp.xml | 6 + .../res/drawable/ic_device_raspberry_pi.xml | 29 + .../drawable/ic_device_raspberry_pi_alt.xml | 11 + .../main/res/drawable/ic_device_router.xml | 13 + .../res/drawable/ic_device_schwibbogen.xml | 12 + .../main/res/drawable/ic_device_socket.xml | 6 + .../main/res/drawable/ic_device_speaker.xml | 14 + app/src/main/res/drawable/ic_device_stack.xml | 12 + .../res/drawable/ic_device_thermometer.xml | 11 + .../main/res/drawable/ic_device_webcam.xml | 8 + app/src/main/res/drawable/ic_do.xml | 15 + app/src/main/res/drawable/ic_done.xml | 9 + app/src/main/res/drawable/ic_drag_handle.xml | 9 + app/src/main/res/drawable/ic_edit.xml | 9 + app/src/main/res/drawable/ic_home_accent.xml | 9 + app/src/main/res/drawable/ic_home_white.xml | 9 + .../main/res/drawable/ic_hue_lamp_base.xml | 5 + .../main/res/drawable/ic_hue_lamp_color.xml | 4 + .../main/res/drawable/ic_hue_scene_add.xml | 51 + .../main/res/drawable/ic_hue_scene_base.xml | 19 + .../main/res/drawable/ic_hue_scene_color.xml | 5 + app/src/main/res/drawable/ic_info.xml | 9 + .../res/drawable/ic_launcher_background.xml | 9 + .../res/drawable/ic_launcher_foreground.xml | 14 + .../main/res/drawable/ic_nav_open_in_new.xml | 9 + app/src/main/res/drawable/ic_room.xml | 8 + app/src/main/res/drawable/ic_scene.xml | 12 + app/src/main/res/drawable/ic_scene_white.xml | 12 + app/src/main/res/drawable/ic_search.xml | 9 + app/src/main/res/drawable/ic_settings.xml | 11 + app/src/main/res/drawable/ic_warning.xml | 12 + app/src/main/res/drawable/ic_zone.xml | 8 + .../res/layout-land/activity_hue_lamp.xml | 68 ++ app/src/main/res/layout/activity_devices.xml | 5 + .../main/res/layout/activity_edit_device.xml | 285 +++++ .../main/res/layout/activity_hue_connect.xml | 41 + app/src/main/res/layout/activity_hue_lamp.xml | 61 + .../main/res/layout/activity_hue_scene.xml | 110 ++ app/src/main/res/layout/activity_main.xml | 86 ++ app/src/main/res/layout/activity_settings.xml | 6 + app/src/main/res/layout/activity_web.xml | 55 + app/src/main/res/layout/dialog_input.xml | 18 + .../main/res/layout/dialog_tasmota_add.xml | 29 + .../layout/dialog_tasmota_execute_once.xml | 19 + .../res/layout/dialog_web_authentication.xml | 29 + app/src/main/res/layout/dropdown_item.xml | 9 + .../main/res/layout/fragment_control_info.xml | 37 + .../res/layout/fragment_hue_bri_color.xml | 29 + .../main/res/layout/fragment_hue_color.xml | 16 + .../main/res/layout/fragment_hue_lamps.xml | 8 + .../main/res/layout/fragment_hue_scenes.xml | 7 + app/src/main/res/layout/grid_item.xml | 28 + .../main/res/layout/hue_color_controls.xml | 49 + app/src/main/res/layout/hue_controls.xml | 84 ++ .../main/res/layout/icon_dropdown_item.xml | 31 + app/src/main/res/layout/list_item.xml | 60 + .../res/layout/list_item_device_discovery.xml | 60 + app/src/main/res/layout/list_item_devices.xml | 59 + app/src/main/res/layout/list_item_simple.xml | 50 + .../res/menu/activity_hue_lamp_actions.xml | 7 + .../res/menu/activity_hue_lamp_context.xml | 6 + .../menu/activity_main_tasmota_context.xml | 6 + .../main/res/menu/activity_web_actions.xml | 10 + app/src/main/res/menu/top_app_bar.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 3510 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 2374 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 4882 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 7532 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 10576 bytes app/src/main/res/values-de/strings.xml | 182 +++ app/src/main/res/values-es/strings.xml | 80 ++ app/src/main/res/values-nl/strings.xml | 80 ++ app/src/main/res/values/dimens.xml | 12 + app/src/main/res/values/strings.xml | 251 ++++ app/src/main/res/values/styles.xml | 7 + app/src/main/res/xml/backup_descriptor.xml | 4 + .../main/res/xml/data_extraction_rules.xml | 9 + app/src/main/res/xml/pref_about.xml | 43 + app/src/main/res/xml/pref_about_list.xml | 5 + app/src/main/res/xml/pref_general.xml | 76 ++ .../domi04151309/home/EspEasyAPIParserTest.kt | 111 ++ .../io/github/domi04151309/home/Helpers.kt | 8 + .../domi04151309/home/HueAPIParserTest.kt | 96 ++ .../domi04151309/home/ShellyAPIParserTest.kt | 189 ++++ .../home/SimpleHomeAPIParserTest.kt | 83 ++ app/src/test/resources/espeasy/espeasy-1.json | 196 ++++ .../espeasy/espeasy-disabledtasks.json | 161 +++ .../test/resources/espeasy/espeasy-nan.json | 105 ++ .../resources/espeasy/espeasy-pressure.json | 110 ++ app/src/test/resources/hue/docs-groups.json | 47 + app/src/test/resources/hue/home-groups.json | 272 +++++ .../shelly-MiniPMG3-Shelly.GetConfig.json | 109 ++ .../shelly-MiniPMG3-Shelly.GetStatus.json | 69 ++ .../shelly-plus-1-Shelly.GetConfig.json | 108 ++ .../shelly-plus-1-Shelly.GetStatus.json | 41 + .../resources/shelly/shelly1-settings.json | 164 +++ .../test/resources/shelly/shelly1-shelly.json | 8 + .../test/resources/shelly/shelly1-status.json | 74 ++ .../shelly/shellyplug1-icons-settings.json | 176 +++ .../shelly/shellyplug1-icons-status.json | 100 ++ .../shelly/shellyplug1-settings.json | 116 ++ .../resources/shelly/shellyplug1-status.json | 67 ++ .../temperature-sensor-commands.json | 16 + .../simplehome/test-server-commands.json | 29 + build.gradle.kts | 6 + .../android/en-US/changelogs/1100.txt | 1 + .../android/en-US/changelogs/1110.txt | 1 + .../android/en-US/changelogs/1120.txt | 3 + .../metadata/android/en-US/changelogs/161.txt | 3 + .../metadata/android/en-US/changelogs/170.txt | 4 + .../metadata/android/en-US/changelogs/171.txt | 5 + .../metadata/android/en-US/changelogs/172.txt | 1 + .../metadata/android/en-US/changelogs/173.txt | 1 + .../metadata/android/en-US/changelogs/180.txt | 2 + .../metadata/android/en-US/changelogs/190.txt | 1 + .../android/en-US/full_description.txt | 27 + .../android/en-US/images/featureGraphic.jpg | Bin 0 -> 68757 bytes .../metadata/android/en-US/images/icon.png | Bin 0 -> 38121 bytes .../en-US/images/phoneScreenshots/1.jpg | Bin 0 -> 62695 bytes .../en-US/images/phoneScreenshots/2.jpg | Bin 0 -> 49183 bytes .../en-US/images/phoneScreenshots/3.jpg | Bin 0 -> 72599 bytes .../en-US/images/phoneScreenshots/4.jpg | Bin 0 -> 42269 bytes .../en-US/images/phoneScreenshots/5.jpg | Bin 0 -> 86362 bytes .../en-US/images/phoneScreenshots/6.jpg | Bin 0 -> 55349 bytes .../android/en-US/short_description.txt | 1 + fastlane/metadata/android/en-US/title.txt | 1 + gradle.properties | 23 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 +++ gradlew.bat | 90 ++ settings.gradle.kts | 18 + 262 files changed, 17567 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Strict.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 LICENSE create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/detekt-config.yml create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt create mode 100644 app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/Application.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/HueConnectActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/HueLampActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/HueSceneActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/LibraryActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/MainActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/SearchDevicesActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/SettingsActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutDeviceActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutHueRoomActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutHueSceneActionActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutHueSceneActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutTasmotaActionActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/ShortcutTasmotaActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/WebActivity.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/activities/WebActivityWebViewClient.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/DeviceDiscoveryListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/DeviceListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/HueDetailsTabAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/HueLampListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/HueSceneGridAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/HueSceneLampListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/IconSpinnerAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/MainListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/adapters/SimpleListAdapter.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/EspEasyAPI.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/EspEasyAPIParser.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/HueAPI.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/HueAPIParser.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/ShellyAPI.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/ShellyAPIParser.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/SimpleHomeAPI.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/SimpleHomeAPIParser.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/Tasmota.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/api/UnifiedAPI.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/custom/CustomJsonArrayRequest.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/custom/JsonObjectRequestAuth.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/custom/TextWatcher.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/DeviceItem.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/LightStates.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/ListViewItem.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/SceneGridItem.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/SceneListItem.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/SimpleListItem.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/data/UnifiedRequestCallback.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/discovery/NetworkServiceDiscoveryListener.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/discovery/NetworkServiceResolveListener.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/discovery/UPnPListener.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/fragments/ControlInfoFragment.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/fragments/HueColorFragment.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/fragments/HueColorSheet.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/fragments/HueLampsFragment.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/fragments/HueScenesFragment.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/ColorUtils.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/DeviceSecrets.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/Devices.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/Global.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/HueLightListener.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/HueUtils.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/P.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/SliderUtils.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/TasmotaHelper.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/helpers/UpdateHandler.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/HomeRecyclerViewHelperInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/HueAdvancedLampInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/HueLampInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/HueRoomInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/RecyclerViewHelperInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/RecyclerViewHelperInterfaceAdvanced.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/interfaces/SceneRecyclerViewHelperInterface.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/services/ControlBuilders.kt create mode 100644 app/src/main/java/io/github/domi04151309/home/services/ControlService.kt create mode 100644 app/src/main/res/drawable-nodpi/header_bg.webp create mode 100644 app/src/main/res/drawable/ic_about_contributor.xml create mode 100644 app/src/main/res/drawable/ic_about_github.xml create mode 100644 app/src/main/res/drawable/ic_about_info.xml create mode 100644 app/src/main/res/drawable/ic_about_library.xml create mode 100644 app/src/main/res/drawable/ic_about_palette.xml create mode 100644 app/src/main/res/drawable/ic_add.xml create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml create mode 100644 app/src/main/res/drawable/ic_buttonpress.xml create mode 100644 app/src/main/res/drawable/ic_circle.xml create mode 100644 app/src/main/res/drawable/ic_color_palette.xml create mode 100644 app/src/main/res/drawable/ic_delete.xml create mode 100644 app/src/main/res/drawable/ic_device_christmas_tree.webp create mode 100644 app/src/main/res/drawable/ic_device_clock.xml create mode 100644 app/src/main/res/drawable/ic_device_display.xml create mode 100644 app/src/main/res/drawable/ic_device_display_alt.xml create mode 100644 app/src/main/res/drawable/ic_device_docker.xml create mode 100644 app/src/main/res/drawable/ic_device_electricity.xml create mode 100644 app/src/main/res/drawable/ic_device_gauge.xml create mode 100644 app/src/main/res/drawable/ic_device_grafana.xml create mode 100644 app/src/main/res/drawable/ic_device_hygrometer.xml create mode 100644 app/src/main/res/drawable/ic_device_lamp.xml create mode 100644 app/src/main/res/drawable/ic_device_raspberry_pi.xml create mode 100644 app/src/main/res/drawable/ic_device_raspberry_pi_alt.xml create mode 100644 app/src/main/res/drawable/ic_device_router.xml create mode 100644 app/src/main/res/drawable/ic_device_schwibbogen.xml create mode 100644 app/src/main/res/drawable/ic_device_socket.xml create mode 100644 app/src/main/res/drawable/ic_device_speaker.xml create mode 100644 app/src/main/res/drawable/ic_device_stack.xml create mode 100644 app/src/main/res/drawable/ic_device_thermometer.xml create mode 100644 app/src/main/res/drawable/ic_device_webcam.xml create mode 100644 app/src/main/res/drawable/ic_do.xml create mode 100644 app/src/main/res/drawable/ic_done.xml create mode 100644 app/src/main/res/drawable/ic_drag_handle.xml create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/drawable/ic_home_accent.xml create mode 100644 app/src/main/res/drawable/ic_home_white.xml create mode 100644 app/src/main/res/drawable/ic_hue_lamp_base.xml create mode 100644 app/src/main/res/drawable/ic_hue_lamp_color.xml create mode 100644 app/src/main/res/drawable/ic_hue_scene_add.xml create mode 100644 app/src/main/res/drawable/ic_hue_scene_base.xml create mode 100644 app/src/main/res/drawable/ic_hue_scene_color.xml create mode 100644 app/src/main/res/drawable/ic_info.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_nav_open_in_new.xml create mode 100644 app/src/main/res/drawable/ic_room.xml create mode 100644 app/src/main/res/drawable/ic_scene.xml create mode 100644 app/src/main/res/drawable/ic_scene_white.xml create mode 100644 app/src/main/res/drawable/ic_search.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_warning.xml create mode 100644 app/src/main/res/drawable/ic_zone.xml create mode 100644 app/src/main/res/layout-land/activity_hue_lamp.xml create mode 100644 app/src/main/res/layout/activity_devices.xml create mode 100644 app/src/main/res/layout/activity_edit_device.xml create mode 100644 app/src/main/res/layout/activity_hue_connect.xml create mode 100644 app/src/main/res/layout/activity_hue_lamp.xml create mode 100644 app/src/main/res/layout/activity_hue_scene.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/activity_web.xml create mode 100644 app/src/main/res/layout/dialog_input.xml create mode 100644 app/src/main/res/layout/dialog_tasmota_add.xml create mode 100644 app/src/main/res/layout/dialog_tasmota_execute_once.xml create mode 100644 app/src/main/res/layout/dialog_web_authentication.xml create mode 100644 app/src/main/res/layout/dropdown_item.xml create mode 100644 app/src/main/res/layout/fragment_control_info.xml create mode 100644 app/src/main/res/layout/fragment_hue_bri_color.xml create mode 100644 app/src/main/res/layout/fragment_hue_color.xml create mode 100644 app/src/main/res/layout/fragment_hue_lamps.xml create mode 100644 app/src/main/res/layout/fragment_hue_scenes.xml create mode 100644 app/src/main/res/layout/grid_item.xml create mode 100644 app/src/main/res/layout/hue_color_controls.xml create mode 100644 app/src/main/res/layout/hue_controls.xml create mode 100644 app/src/main/res/layout/icon_dropdown_item.xml create mode 100644 app/src/main/res/layout/list_item.xml create mode 100644 app/src/main/res/layout/list_item_device_discovery.xml create mode 100644 app/src/main/res/layout/list_item_devices.xml create mode 100644 app/src/main/res/layout/list_item_simple.xml create mode 100644 app/src/main/res/menu/activity_hue_lamp_actions.xml create mode 100644 app/src/main/res/menu/activity_hue_lamp_context.xml create mode 100644 app/src/main/res/menu/activity_main_tasmota_context.xml create mode 100644 app/src/main/res/menu/activity_web_actions.xml create mode 100644 app/src/main/res/menu/top_app_bar.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/backup_descriptor.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/pref_about.xml create mode 100644 app/src/main/res/xml/pref_about_list.xml create mode 100644 app/src/main/res/xml/pref_general.xml create mode 100644 app/src/test/java/io/github/domi04151309/home/EspEasyAPIParserTest.kt create mode 100644 app/src/test/java/io/github/domi04151309/home/Helpers.kt create mode 100644 app/src/test/java/io/github/domi04151309/home/HueAPIParserTest.kt create mode 100644 app/src/test/java/io/github/domi04151309/home/ShellyAPIParserTest.kt create mode 100644 app/src/test/java/io/github/domi04151309/home/SimpleHomeAPIParserTest.kt create mode 100644 app/src/test/resources/espeasy/espeasy-1.json create mode 100644 app/src/test/resources/espeasy/espeasy-disabledtasks.json create mode 100644 app/src/test/resources/espeasy/espeasy-nan.json create mode 100644 app/src/test/resources/espeasy/espeasy-pressure.json create mode 100644 app/src/test/resources/hue/docs-groups.json create mode 100644 app/src/test/resources/hue/home-groups.json create mode 100644 app/src/test/resources/shelly/shelly-MiniPMG3-Shelly.GetConfig.json create mode 100644 app/src/test/resources/shelly/shelly-MiniPMG3-Shelly.GetStatus.json create mode 100644 app/src/test/resources/shelly/shelly-plus-1-Shelly.GetConfig.json create mode 100644 app/src/test/resources/shelly/shelly-plus-1-Shelly.GetStatus.json create mode 100644 app/src/test/resources/shelly/shelly1-settings.json create mode 100644 app/src/test/resources/shelly/shelly1-shelly.json create mode 100644 app/src/test/resources/shelly/shelly1-status.json create mode 100644 app/src/test/resources/shelly/shellyplug1-icons-settings.json create mode 100644 app/src/test/resources/shelly/shellyplug1-icons-status.json create mode 100644 app/src/test/resources/shelly/shellyplug1-settings.json create mode 100644 app/src/test/resources/shelly/shellyplug1-status.json create mode 100644 app/src/test/resources/simplehome/temperature-sensor-commands.json create mode 100644 app/src/test/resources/simplehome/test-server-commands.json create mode 100644 build.gradle.kts create mode 100644 fastlane/metadata/android/en-US/changelogs/1100.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/1110.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/1120.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/161.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/170.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/171.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/172.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/173.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/180.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/190.txt create mode 100644 fastlane/metadata/android/en-US/full_description.txt create mode 100644 fastlane/metadata/android/en-US/images/featureGraphic.jpg create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/4.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/5.jpg create mode 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg create mode 100644 fastlane/metadata/android/en-US/short_description.txt create mode 100644 fastlane/metadata/android/en-US/title.txt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..6f48231 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: Domi04151309 +custom: ['https://www.paypal.com/donate/?hosted_button_id=487FTCX52P9WA'] diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..5e21670 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,31 @@ +--- +name: Bug Report +about: Create a report to help improving the app +title: "[Bug Report] Your Title" +labels: bug +assignees: '' + +--- + +**Description** + +_A clear and concise description of what the bug is. +Please add steps to reproduce the behavior._ + +
+ Screenshots + + _Add screenshots here to describe the problem._ +
+ +
+ Logs + + _Add a detailed stack trace / crash log here if applicable_ +
+ +**Additional information** + + - Android version: _System Settings > About > Android version (varies per device)_ + - Home App version: _Settings > About > Version_ + - Installation source: _Google Play / F-Drod / Own Build_ diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..26704ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[Feature Request] Your Title" +labels: enhancement +assignees: '' + +--- + +**Describe the solution you'd like** + +_A clear and concise description of what you want to happen._ + +
+ Additional context + + _Add any other context or screenshots about the feature request here._ +
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bca3767 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: Continuous Integration + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + ktlint: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Install + run: | + curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.0.1/ktlint + chmod a+x ktlint + sudo mv ktlint /usr/local/bin/ + - name: Run + run: ktlint --reporter sarif -l none > ktlint.sarif + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: ktlint.sarif + detekt: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Run + run: | + chmod +x gradlew + ./gradlew detekt + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: app/build/reports/detekt/detekt.sarif + - name: Job Summary + if: success() || failure() + run: cat ./app/build/reports/detekt/detekt.md >> $GITHUB_STEP_SUMMARY + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Run + run: | + chmod +x gradlew + ./gradlew build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d18272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/jarRepositories.xml +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..15a15b2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Strict.xml b/.idea/inspectionProfiles/Strict.xml new file mode 100644 index 0000000..7532a01 --- /dev/null +++ b/.idea/inspectionProfiles/Strict.xml @@ -0,0 +1,1006 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..ce11e3f --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c22b6fa --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5cc7043 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61d1860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md index 9010d67..758fe4b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -# home-app +![App Icon](https://raw.githubusercontent.com/Domi04151309/HomeApp/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp) +# Home App for Android™ +HomeApp is a small and easy to use smart home app with a simple framework. The goal of this application is to make remote execution of predefined features as easy and user-friendly as possible to help you get started with smart home technology. -Home-Lab Integration for Android \ No newline at end of file + + Get it on F-Droid + + +## Donate +Support the development by donating. + + + Donate + + +## Supported devices +Home App natively supports the following devices: + +- [Philips Hue Bridge](https://github.com/Domi04151309/HomeApp/wiki/Hue-API-%28v1%29) +- [Shelly](https://github.com/Domi04151309/HomeApp/wiki/Shelly) Gen 1 devices +- [Shelly](https://github.com/Domi04151309/HomeApp/wiki/Shelly) Gen 2 devices +- Devices using [ESP Easy](https://github.com/Domi04151309/HomeApp/wiki/ESP-Easy) +- Devices using [Tasmota](https://github.com/Domi04151309/HomeApp/wiki/Tasmota) +- Devices using the [Node-RED dashboard](https://github.com/Domi04151309/HomeApp/wiki/Node-RED-Dashboard) +- Devices using the [SimpleHome API](https://github.com/Domi04151309/HomeApp/wiki/SimpleHome-API) +- Devices with a [web interface](https://github.com/Domi04151309/HomeApp/wiki/Websites) + +## How it works +Communication between the devices uses HTTP requests and JSON strings. After the commanding device has send a HTTP request to the smart home device, the smart home device sends back a JSON string containing the information the app needs. + +This app is especially useful if you are using microcontrollers or other small devices such as the Raspberry Pi for smart home automation. + +## Previews + + +Android, Google Play and the Google Play logo are trademarks of Google LLC. + +## Legal Notice +Copyright (C) 2020 Domi04151309 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Impressum diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e1db910 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,103 @@ +import com.android.build.gradle.internal.tasks.factory.dependsOn + +private val readAndUnderstoodLicense = false + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("io.gitlab.arturbosch.detekt") +} + +android { + namespace = "io.github.domi04151309.home" + compileSdk = 34 + + defaultConfig { + applicationId = "io.github.domi04151309.home" + minSdk = 23 + //noinspection EditedTargetSdkVersion + targetSdk = 34 + versionCode = 1120 + versionName = "1.12.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + testOptions { + unitTests.isIncludeAndroidResources = true + } + buildFeatures { + buildConfig = true + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + detekt { + config.setFrom(file("detekt-config.yml")) + buildUponDefaultConfig = true + basePath = rootProject.projectDir.absolutePath + } + lint { + disable += "MissingTranslation" + } + project.tasks.preBuild.dependsOn("license") +} + +tasks.register("license") { + doFirst { + val data = + file("./src/main/res/xml/pref_about.xml") + .readText() + .contains("app:key=\"license\"") + if (!data) { + throw Exception( + "Please note that removing the license from the about page is not allowed if you " + + "plan to publish your modified version of this app. " + + "Please read the project's LICENSE.", + ) + } + if (!( + android.defaultConfig.applicationId?.contains("domi04151309") == true || + readAndUnderstoodLicense + ) + ) { + throw Exception( + "Please make sure you have read and understood the LICENSE!", + ) + } + } +} + +dependencies { + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("com.google.android.material:material:1.12.0") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.annotation:annotation:1.9.1") + implementation("com.android.volley:volley:1.2.1") + implementation("androidx.security:security-crypto-ktx:1.1.0-beta01") + implementation("com.github.skydoves:colorpickerview:2.3.0") + + testImplementation("junit:junit:4.13.2") + testImplementation("org.robolectric:robolectric:4.14.1") +} diff --git a/app/detekt-config.yml b/app/detekt-config.yml new file mode 100644 index 0000000..af903e4 --- /dev/null +++ b/app/detekt-config.yml @@ -0,0 +1,784 @@ +build: + maxIssues: 0 + excludeCorrectable: true + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: true + checkExhaustiveness: true + # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' + excludes: '' + +processors: + active: true + exclude: + - 'DetektProgressListener' + # - 'KtFileCountProcessor' + # - 'PackageCountProcessor' + # - 'ClassCountProcessor' + # - 'FunctionCountProcessor' + # - 'PropertyCountProcessor' + # - 'ProjectComplexityProcessor' + # - 'ProjectCognitiveComplexityProcessor' + # - 'ProjectLLOCProcessor' + # - 'ProjectCLOCProcessor' + # - 'ProjectLOCProcessor' + # - 'ProjectSLOCProcessor' + # - 'LicenseHeaderLoaderExtension' + +console-reports: + active: true + exclude: + - 'ProjectStatisticsReport' + - 'ComplexityReport' + - 'NotificationReport' + - 'FindingsReport' + - 'FileBasedFindingsReport' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: true + CommentOverPrivateFunction: + active: true + CommentOverPrivateProperty: + active: true + DeprecatedBlockTag: + active: true + EndOfSentenceFormat: + active: true + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: true + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: true + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: true + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: true + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: true + +complexity: + active: true + CognitiveComplexMethod: + active: true + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: true + threshold: 10 + includeStaticDeclarations: true + includePrivateDeclarations: true + ignoreOverloaded: true + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: true + ignoreSimpleWhenEntries: true + ignoreNestingFunctions: true + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + LabeledExpression: + active: true + ignoredLabels: [] + LargeClass: + active: true + threshold: 600 + LongMethod: + active: true + threshold: 60 + LongParameterList: + active: true + functionThreshold: 6 + constructorThreshold: 7 + ignoreDefaultParameters: true + ignoreDataClasses: true + ignoreAnnotatedParameter: [] + MethodOverloading: + active: true + threshold: 6 + NamedArguments: + active: true + threshold: 3 + ignoreArgumentsMatchingNames: true + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: true + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: true + StringLiteralDuplication: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: true + ignorePrivate: true + ignoreOverridden: true + +coroutines: + active: true + GlobalCoroutineUsage: + active: true + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: true + SuspendFunWithCoroutineScopeReceiver: + active: true + SuspendFunWithFlowReturnType: + active: true + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + allowedExceptionNameRegex: '_|(ignore|expected).*' + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + ignoreOverridden: true + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyTryBlock: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: true + ObjectExtendsThrowable: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: true + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: true + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: true + forbiddenName: [] + FunctionMaxLength: + active: true + maximumFunctionNameLength: 30 + FunctionMinLength: + active: true + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: true + ObjectPropertyNaming: + active: true + constantPattern: '[A-Za-z][_A-Za-z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' + PackageNaming: + active: true + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + constantPattern: '[A-Z][_A-Z0-9]*' + propertyPattern: '[A-Za-z][_A-Za-z0-9]*' + privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' + VariableMaxLength: + active: true + maximumVariableNameLength: 64 + VariableMinLength: + active: true + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: true + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: true + CastToNullableType: + active: true + Deprecation: + active: true + DontDowncastCollectionTypes: + active: true + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: true + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: true + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: true + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: true + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: true + NullableToStringCall: + active: true + PropertyUsedBeforeDeclaration: + active: true + UnconditionalJumpStatementInLoop: + active: true + UnnecessaryNotNullCheck: + active: true + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: true + BracesOnIfStatements: + active: true + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: true + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: true + CascadingCallWrapping: + active: true + includeElvis: true + ClassOrdering: + active: true + CollapsibleIfStatements: + active: true + DataClassContainsFunctions: + active: true + conversionFunctionPrefix: + - 'to' + allowOperators: true + DataClassShouldBeImmutable: + active: true + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: true + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitCollectionElementAccessMethod: + active: true + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: true + includeLineWrapping: true + ForbiddenAnnotation: + active: true + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: true + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: true + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: true + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: true + ignoreUsageInGenerics: true + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + ignoreNumbers: + - '-1' + - '0' + - '1' + - '2' + ignoreHashCodeFunction: true + ignorePropertyDeclaration: true + ignoreLocalVariableDeclaration: true + ignoreConstantDeclaration: true + ignoreCompanionObjectPropertyDeclaration: true + ignoreAnnotation: true + ignoreNamedArgument: true + ignoreEnums: true + ignoreRanges: true + ignoreExtensionFunctions: true + MandatoryBracesLoops: + active: true + MaxChainedCallsOnSameLine: + active: true + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: true + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: true + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + NullableBooleanCheck: + active: true + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: true + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: true + ReturnCount: + active: true + max: 2 + excludedFunctions: + - 'equals' + excludeLabeled: true + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + StringShouldBeRawString: + active: true + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: true + TrailingWhitespace: + active: true + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: true + acceptableLength: 4 + allowNonStandardGrouping: true + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: true + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: true + UnnecessaryBracesAroundTrailingLambda: + active: true + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + allowForUnclearPrecedence: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: true + allowVars: true + UseEmptyCounterpart: + active: true + UseIfEmptyOrIfBlank: + active: true + UseIfInsteadOfWhen: + active: true + ignoreWhenContainingVariableDeclaration: true + UseIsNullOrEmpty: + active: true + UseLet: + active: true + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: true + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..2f45576 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,27 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable +-dontobfuscate + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# This is generated automatically by the Android Gradle plugin. +-dontwarn com.google.errorprone.annotations.Immutable +-dontwarn javax.annotation.concurrent.GuardedBy +-dontwarn javax.annotation.Nullable \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..230adaa --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..183b8ed2f2b28e806b1e4e47c220a8047ef1bfb1 GIT binary patch literal 48568 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 ZMVd!eL~btJzh-^;*@90~K3Tc#{{eF_BWVBt literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt b/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt new file mode 100644 index 0000000..2ce6696 --- /dev/null +++ b/app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt @@ -0,0 +1,110 @@ +package com.rine.upnpdiscovery + +import android.util.Log +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import org.xmlpull.v1.XmlPullParserFactory +import java.io.ByteArrayInputStream + +class UPnPDevice internal constructor(val hostAddress: String, header: String) { + internal val location: String + val server: String + + // XML content + private var descriptionXML: String = "" + + // From description XML + var friendlyName: String = "" + private var deviceType: String = "" + private var presentationURL: String = "" + private var serialNumber: String = "" + private var modelName: String = "" + private var modelNumber: String = "" + private var modelURL: String = "" + private var manufacturer: String = "" + private var manufacturerURL: String = "" + private var udn: String = "" + private var urlBase: String = "" + + init { + location = parseHeader(header, "LOCATION: ") + server = parseHeader(header, "SERVER: ") + } + + internal fun update(xml: String) { + descriptionXML = xml + try { + xmlParse(xml) + } catch (e: XmlPullParserException) { + Log.w(UPnPDiscovery.TAG, e.toString()) + } + } + + override fun toString(): String = + "FriendlyName: " + friendlyName + LINE_END + + "ModelName: " + modelName + LINE_END + + "HostAddress: " + hostAddress + LINE_END + + "Location: " + location + LINE_END + + "DeviceType: " + deviceType + LINE_END + + "PresentationURL: " + presentationURL + LINE_END + + "SerialNumber: " + serialNumber + LINE_END + + "ModelURL: " + modelURL + LINE_END + + "ModelNumber: " + modelNumber + LINE_END + + "Manufacturer: " + manufacturer + LINE_END + + "ManufacturerURL: " + manufacturerURL + LINE_END + + "UDN: " + udn + LINE_END + + "URLBase: " + urlBase + + private fun parseHeader( + mSearchAnswer: String, + whatSearch: String, + ): String { + var result = "" + var searchLinePos = mSearchAnswer.indexOf(whatSearch) + if (searchLinePos != -1) { + searchLinePos += whatSearch.length + val locColon = mSearchAnswer.indexOf(LINE_END, searchLinePos) + result = mSearchAnswer.substring(searchLinePos, locColon) + } + return result + } + + private fun readText(parser: XmlPullParser): String { + var result = "" + if (parser.next() == XmlPullParser.TEXT) { + result = parser.text + parser.nextTag() + } + return result + } + + private fun xmlParse(xml: String) { + val xmlFactoryObject = XmlPullParserFactory.newInstance() + val parser = xmlFactoryObject.newPullParser() + parser.setInput(ByteArrayInputStream(xml.toByteArray(Charsets.UTF_8)), null) + var event = parser.eventType + while (event != XmlPullParser.END_DOCUMENT) { + val name = parser.name + if (event == XmlPullParser.START_TAG) { + when (name) { + "friendlyName" -> friendlyName = readText(parser) + "deviceType" -> deviceType = readText(parser) + "presentationURL" -> presentationURL = readText(parser) + "serialNumber" -> serialNumber = readText(parser) + "modelName" -> modelName = readText(parser) + "modelNumber" -> modelNumber = readText(parser) + "modelURL" -> modelURL = readText(parser) + "manufacturer" -> manufacturer = readText(parser) + "manufacturerURL" -> manufacturerURL = readText(parser) + "UDN" -> udn = readText(parser) + "URLBase" -> urlBase = readText(parser) + } + } + event = parser.next() + } + } + + companion object { + private const val LINE_END = "\r\n" + } +} diff --git a/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt b/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt new file mode 100644 index 0000000..deef452 --- /dev/null +++ b/app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt @@ -0,0 +1,178 @@ +package com.rine.upnpdiscovery + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.net.wifi.WifiManager +import android.os.AsyncTask +import android.util.Log +import com.android.volley.Request +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import java.io.IOException +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.net.InetSocketAddress + +@Suppress("MagicNumber") +class UPnPDiscovery : AsyncTask { + private val devices = HashSet() + + @SuppressLint("StaticFieldLeak") + private val mContext: Context + private var mThreadsCount: Int = 0 + private val mCustomQuery: String + private val mInternetAddress: String + private val mPort: Int + + private val mListener: OnDiscoveryListener + + interface OnDiscoveryListener { + fun onStart() + + fun onFoundNewDevice(device: UPnPDevice) + + fun onFinish(devices: HashSet) + + fun onError(e: Exception) + } + + private constructor(activity: Activity, listener: OnDiscoveryListener) { + mContext = activity.applicationContext + mListener = listener + mThreadsCount = 0 + mCustomQuery = DEFAULT_QUERY + mInternetAddress = DEFAULT_ADDRESS + mPort = 1900 + } + + private constructor( + activity: Activity, + listener: OnDiscoveryListener, + customQuery: String, + address: String, + port: Int, + ) { + mContext = activity.applicationContext + mListener = listener + mThreadsCount = 0 + mCustomQuery = customQuery + mInternetAddress = address + mPort = port + } + + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg p0: Activity?): Void? { + mListener.onStart() + val wifi = mContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + val lock = wifi.createMulticastLock("The Lock") + lock.acquire() + var socket: DatagramSocket? = null + try { + val group = InetAddress.getByName(mInternetAddress) + val port = mPort + val query = mCustomQuery + socket = DatagramSocket(null) + socket.reuseAddress = true + socket.broadcast = true + socket.bind(InetSocketAddress(port)) + + val datagramPacketRequest = DatagramPacket(query.toByteArray(), query.length, group, port) + socket.send(datagramPacketRequest) + + val time = System.currentTimeMillis() + var curTime = System.currentTimeMillis() + + while (curTime - time < 1000) { + val datagramPacket = DatagramPacket(ByteArray(1024), 1024) + socket.receive(datagramPacket) + val response = String(datagramPacket.data, 0, datagramPacket.length) + if (response.substring(0, 12).uppercase() == "HTTP/1.1 200") { + val device = UPnPDevice(datagramPacket.address.hostAddress ?: continue, response) + mThreadsCount++ + getData(device.location, device) + } + curTime = System.currentTimeMillis() + } + } catch (e: IOException) { + mListener.onError(e) + } finally { + socket?.close() + } + lock.release() + return null + } + + private fun getData( + url: String, + device: UPnPDevice, + ) { + val stringRequest = + StringRequest( + Request.Method.GET, + url, + { response -> + device.update(response) + mListener.onFoundNewDevice(device) + devices.add(device) + mThreadsCount-- + if (mThreadsCount == 0) { + mListener.onFinish(devices) + } + }, + { + mThreadsCount-- + Log.e(TAG, "URL: $url get content error!") + }, + ) + stringRequest.tag = TAG + "SSDP description request" + Volley.newRequestQueue(mContext).add(stringRequest) + } + + companion object { + internal val TAG: String = UPnPDiscovery::class.java.simpleName + + private const val DISCOVER_TIMEOUT = 1500 + private const val LINE_END = "\r\n" + private const val DEFAULT_QUERY = + "M-SEARCH * HTTP/1.1" + LINE_END + + "HOST: 239.255.255.250:1900" + LINE_END + + "MAN: \"ssdp:discover\"" + LINE_END + + "MX: 1" + LINE_END + + "ST: ssdp:all" + LINE_END + + LINE_END + private const val DEFAULT_ADDRESS = "239.255.255.250" + + fun discoveryDevices( + activity: Activity, + listener: OnDiscoveryListener, + ): Boolean { + val discover = UPnPDiscovery(activity, listener) + discover.execute() + return try { + Thread.sleep(DISCOVER_TIMEOUT.toLong()) + true + } catch (e: InterruptedException) { + false + } + } + + fun discoveryDevices( + activity: Activity, + listener: OnDiscoveryListener, + customQuery: String, + address: String, + port: Int, + ): Boolean { + val discover = UPnPDiscovery(activity, listener, customQuery, address, port) + discover.execute() + return try { + Thread.sleep(DISCOVER_TIMEOUT.toLong()) + true + } catch (e: InterruptedException) { + false + } + } + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/Application.kt b/app/src/main/java/io/github/domi04151309/home/Application.kt new file mode 100644 index 0000000..a57db64 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/Application.kt @@ -0,0 +1,10 @@ +package io.github.domi04151309.home + +import com.google.android.material.color.DynamicColors + +class Application : android.app.Application() { + override fun onCreate() { + super.onCreate() + DynamicColors.applyToActivitiesIfAvailable(this) + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt new file mode 100644 index 0000000..4d91266 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/AboutActivity.kt @@ -0,0 +1,112 @@ +package io.github.domi04151309.home.activities + +import android.content.Intent +import android.os.Bundle +import androidx.core.net.toUri +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.github.domi04151309.home.BuildConfig +import io.github.domi04151309.home.R + +class AboutActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, GeneralPreferenceFragment()) + .commit() + } + + class GeneralPreferenceFragment : PreferenceFragmentCompat() { + @Suppress("SameReturnValue") + private fun onIconsClicked(): Boolean { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.about_icons) + .setItems(resources.getStringArray(R.array.about_icons_array)) { _, which -> + startActivity( + Intent( + Intent.ACTION_VIEW, + when (which) { + 0 -> "https://icons8.com/" + 1 -> "https://fonts.google.com/icons?selected=Material+Icons" + else -> "about:blank" + }.toUri(), + ), + ) + } + .show() + return true + } + + @Suppress("SameReturnValue") + private fun onExternalClicked(link: String): Boolean { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.about_privacy) + .setMessage(R.string.about_privacy_desc) + .setPositiveButton(android.R.string.ok) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + link.toUri(), + ), + ) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setNeutralButton(R.string.about_privacy_policy) { _, _ -> + startActivity( + Intent( + Intent.ACTION_VIEW, + "https://docs.github.com/en/github/site-policy/github-privacy-statement".toUri(), + ), + ) + } + .show() + return true + } + + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + addPreferencesFromResource(R.xml.pref_about) + findPreference("app_version")?.apply { + summary = + requireContext().getString( + R.string.about_app_version_desc, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE, + ) + setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/releases") + } + } + findPreference("github")?.apply { + summary = REPOSITORY_URL + setOnPreferenceClickListener { + onExternalClicked(REPOSITORY_URL) + } + } + findPreference("license")?.setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/blob/$BRANCH/LICENSE") + } + findPreference("icons")?.setOnPreferenceClickListener { + onIconsClicked() + } + findPreference("contributors")?.setOnPreferenceClickListener { + onExternalClicked("$REPOSITORY_URL/graphs/contributors") + } + findPreference("libraries")?.setOnPreferenceClickListener { + startActivity(Intent(requireContext(), LibraryActivity::class.java)) + true + } + } + } + + companion object { + private const val REPOSITORY: String = "Domi04151309/HomeApp" + private const val BRANCH: String = "main" + private const val REPOSITORY_URL: String = "https://github.com/$REPOSITORY" + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt new file mode 100644 index 0000000..f059af5 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/BaseActivity.kt @@ -0,0 +1,22 @@ +package io.github.domi04151309.home.activities + +import android.content.res.Configuration +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.elevation.SurfaceColors +import io.github.domi04151309.home.R + +abstract class BaseActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (resources.configuration.uiMode.and( + Configuration.UI_MODE_NIGHT_MASK, + ) != Configuration.UI_MODE_NIGHT_YES + ) { + setTheme(R.style.LightStatusBarOverlay) + } + val color = SurfaceColors.SURFACE_2.getColor(this) + window.statusBarColor = color + window.navigationBarColor = color + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt new file mode 100644 index 0000000..b1b390c --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivity.kt @@ -0,0 +1,70 @@ +package io.github.domi04151309.home.activities + +import android.os.Bundle +import com.google.android.material.elevation.SurfaceColors +import io.github.domi04151309.home.R +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.fragments.ControlInfoFragment +import io.github.domi04151309.home.fragments.HueColorFragment +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global +import io.github.domi04151309.home.interfaces.HueRoomInterface + +class ControlInfoActivity : BaseActivity() { + private var hueRoom: ControlInfoActivityHueRoom? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + + window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this) + + val id = intent.getStringExtra(EXTRA_ID) + if (id === null) { + return + } + + val device = Devices(this).getDeviceById(id.substring(0, id.indexOf('@'))) + + if (device.mode == Global.HUE_API) { + showHueFragment(id, device) + return + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, ControlInfoFragment(device, intent.getStringExtra(EXTRA_TITLE) ?: "")) + .commit() + } + + override fun onStart() { + super.onStart() + hueRoom?.onStart() + } + + override fun onStop() { + super.onStop() + hueRoom?.onStop() + } + + override fun onDestroy() { + super.onDestroy() + hueRoom?.onDestroy() + } + + private fun showHueFragment( + id: String, + device: DeviceItem, + ) { + hueRoom = ControlInfoActivityHueRoom(this, device, id.substring(id.indexOf('@') + 1)) + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, HueColorFragment(hueRoom as HueRoomInterface)) + .commit() + } + + companion object { + const val EXTRA_ID: String = "EXTRA_ID" + const val EXTRA_TITLE: String = "EXTRA_TITLE" + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt new file mode 100644 index 0000000..1a5ba4c --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/ControlInfoActivityHueRoom.kt @@ -0,0 +1,97 @@ +package io.github.domi04151309.home.activities + +import android.content.Context +import com.android.volley.Request +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import io.github.domi04151309.home.api.HueAPI +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.LightStates +import io.github.domi04151309.home.helpers.HueLightListener +import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE +import io.github.domi04151309.home.helpers.UpdateHandler +import io.github.domi04151309.home.interfaces.HueRoomInterface +import org.json.JSONArray + +class ControlInfoActivityHueRoom : HueRoomInterface { + override var lights: JSONArray? + override var lampData: HueLightListener + override var id: String + override var device: DeviceItem + override var addressPrefix: String + override var canReceiveRequest: Boolean + + private var updateDataRequest: JsonObjectRequest? = null + private var updateHandler: UpdateHandler = UpdateHandler() + + constructor(context: Context, device: DeviceItem, id: String) { + val hueApi = HueAPI(context, device.id) + val queue = Volley.newRequestQueue(context) + + this.lights = null + this.lampData = HueLightListener() + this.id = id + this.device = device + this.addressPrefix = device.address + "api/" + hueApi.getUsername() + this.canReceiveRequest = false + + updateDataRequest = getUpdateRequest() + updateHandler.setUpdateFunction { + if (canReceiveRequest && hueApi.readyForRequest) { + queue.add(updateDataRequest) + } + } + + onStart() + } + + fun onStart() { + canReceiveRequest = true + } + + fun onStop() { + canReceiveRequest = false + } + + fun onDestroy() { + updateHandler.stop() + } + + override fun onColorChanged(color: Int) { + // Do nothing. + } + + private fun getUpdateRequest() = + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/groups/$id", + null, + { response -> + lights = response.getJSONArray("lights") + val action = response.getJSONObject("action") + val light = LightStates.Light() + + light.ct = + if (action.has("ct")) { + action.getInt("ct") - MIN_COLOR_TEMPERATURE + } else { + -1 + } + + if (action.has("hue") && action.has("sat")) { + light.hue = action.getInt("hue") + light.sat = action.getInt("sat") + } else { + light.hue = -1 + light.sat = -1 + } + + light.on = response.getJSONObject("state").getBoolean("any_on") + + lampData.state = light + }, + { + canReceiveRequest = false + }, + ) +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt new file mode 100644 index 0000000..3b2f045 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/DeviceInfoActivity.kt @@ -0,0 +1,238 @@ +package io.github.domi04151309.home.activities + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.volley.Request +import com.android.volley.RequestQueue +import com.android.volley.toolbox.JsonObjectRequest +import com.android.volley.toolbox.Volley +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.SimpleListAdapter +import io.github.domi04151309.home.api.HueAPI +import io.github.domi04151309.home.api.HueAPIParser +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.SimpleListItem +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global +import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface +import org.json.JSONObject +import java.util.Locale +import java.util.concurrent.TimeUnit + +@Suppress("TooManyFunctions") +class DeviceInfoActivity : BaseActivity(), RecyclerViewHelperInterface { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_devices) + + val devices = Devices(this) + val id = intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: "" + if (!devices.idExists(id)) { + finish() + return + } + + val device = devices.getDeviceById(id) + val queue = Volley.newRequestQueue(this) + val recyclerView = findViewById(R.id.recyclerView) + val items = mutableListOf() + recyclerView.layoutManager = LinearLayoutManager(this) + items.add( + SimpleListItem( + device.name, + device.address, + icon = device.iconId, + ), + ) + + when (device.mode) { + Global.HUE_API -> showHueInfo(device, queue, items, recyclerView) + Global.SHELLY_GEN_2 -> showShelly2Info(device, queue, items, recyclerView) + Global.SHELLY_GEN_3 -> showShelly2Info(device, queue, items, recyclerView) + } + } + + override fun onItemClicked( + view: View, + position: Int, + ) { + // Do nothing. + } + + private fun boolToString(bool: Boolean): String = + resources.getString( + if (bool) R.string.str_on else R.string.str_off, + ) + + @Suppress("MagicNumber") + private fun rssiToPercent(rssi: Int): Int = + if (rssi <= -100) { + 0 + } else if (rssi >= -50) { + 100 + } else { + 2 * (rssi + 100) + } + + private fun formatUptime(uptime: Long) = + String.format( + Locale.getDefault(), + "%02d:%02d:%02d", + TimeUnit.SECONDS.toHours(uptime), + TimeUnit.SECONDS.toMinutes(uptime) - + TimeUnit.HOURS.toMinutes( + TimeUnit.SECONDS.toHours( + uptime, + ), + ), + TimeUnit.SECONDS.toSeconds(uptime) - + TimeUnit.MINUTES.toSeconds( + TimeUnit.SECONDS.toMinutes( + uptime, + ), + ), + ) + + private fun showHueInfo( + device: DeviceItem, + queue: RequestQueue, + items: MutableList, + recyclerView: RecyclerView, + ) { + val hueAPI = HueAPI(this, device.id) + val addressPrefix = device.address + "api/" + hueAPI.getUsername() + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/config", + null, + { response -> + items.addAll(HueAPIParser.parseHueConfig(resources, response)) + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/sensors", + null, + { innerResponse -> + items.addAll(HueAPIParser.parseHueSensors(resources, innerResponse)) + + queue.add( + JsonObjectRequest( + Request.Method.GET, + "$addressPrefix/lights", + null, + { innerInnerResponse -> + items.addAll(HueAPIParser.parseHueLights(resources, innerInnerResponse)) + recyclerView.adapter = SimpleListAdapter(items, this) + }, + { }, + ), + ) + }, + { }, + ), + ) + }, + { }, + ), + ) + } + + @Suppress("LongMethod") + private fun parseShelly2Info(response: JSONObject) = + listOf( + SimpleListItem(summary = resources.getString(R.string.device_config_info_status)), + SimpleListItem( + (response.optJSONObject("wifi") ?: JSONObject()).run { + optString("ssid") + " (" + rssiToPercent(optInt("rssi")) + " %)" + }, + resources.getString(R.string.shelly_wifi), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + boolToString( + ( + response.optJSONObject("mqtt") + ?: JSONObject() + ).optBoolean("connected"), + ), + resources.getString(R.string.shelly_mqtt), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + boolToString( + ( + response.optJSONObject("cloud") + ?: JSONObject() + ).optBoolean("connected"), + ), + resources.getString(R.string.shelly_cloud), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + formatUptime((response.optJSONObject("sys") ?: JSONObject()).optLong("uptime")), + resources.getString(R.string.shelly_uptime), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + (response.optJSONObject("sys") ?: JSONObject()).run { + "${(optInt("fs_free") / optInt("fs_size").toFloat() * TO_PERCENT).toInt()} %" + }, + resources.getString(R.string.shelly_storage), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + (response.optJSONObject("sys") ?: JSONObject()).run { + "${(optInt("ram_free") / optInt("ram_size").toFloat() * TO_PERCENT).toInt()} %" + }, + resources.getString(R.string.shelly_ram), + icon = R.drawable.ic_about_info, + ), + SimpleListItem( + resources.getString( + if (( + ( + response.optJSONObject("sys") + ?: JSONObject() + ).optJSONObject("available_updates") + ?: JSONObject() + ).has("stable") + ) { + R.string.str_yes + } else { + R.string.str_no + }, + ), + resources.getString(R.string.shelly_update), + icon = R.drawable.ic_about_info, + ), + ) + + private fun showShelly2Info( + device: DeviceItem, + queue: RequestQueue, + items: MutableList, + recyclerView: RecyclerView, + ) { + queue.add( + JsonObjectRequest( + Request.Method.GET, + device.address + "rpc/Shelly.GetStatus", + null, + { response -> + items.addAll(parseShelly2Info(response)) + recyclerView.adapter = SimpleListAdapter(items, this) + }, + { }, + ), + ) + } + + companion object { + private const val TO_PERCENT = 100 + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt new file mode 100644 index 0000000..52b3f05 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/DevicesActivity.kt @@ -0,0 +1,152 @@ +package io.github.domi04151309.home.activities + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.DeviceListAdapter +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.data.SimpleListItem +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterfaceAdvanced + +class DevicesActivity : BaseActivity(), RecyclerViewHelperInterfaceAdvanced { + private var reset = true + private lateinit var devices: Devices + private lateinit var recyclerView: RecyclerView + private lateinit var itemTouchHelper: ItemTouchHelper + + private val itemTouchHelperCallback = + object : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ): Int = + if ( + viewHolder.adapterPosition == (recyclerView.adapter?.itemCount ?: -1) - 1 + ) { + makeMovementFlags(0, 0) + } else { + makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + val adapter = recyclerView.adapter ?: return false + return if (target.adapterPosition == adapter.itemCount - 1) { + false + } else { + recyclerView.adapter?.notifyItemMoved( + viewHolder.adapterPosition, + target.adapterPosition, + ) + devices.moveDevice(viewHolder.adapterPosition, target.adapterPosition) + true + } + } + + override fun isLongPressDragEnabled(): Boolean = true + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int, + ) { + // Do nothing. + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + super.clearView(recyclerView, viewHolder) + devices.saveChanges() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_devices) + + devices = Devices(this) + recyclerView = findViewById(R.id.recyclerView) + itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) + + itemTouchHelper.attachToRecyclerView(recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + } + + private fun loadDevices() { + val listItems: ArrayList = ArrayList(devices.length) + var currentDevice: DeviceItem + for (i in 0 until devices.length) { + currentDevice = devices.getDeviceByIndex(i) + listItems += + SimpleListItem( + title = currentDevice.name, + summary = + if (currentDevice.hide) { + resources.getString(R.string.device_config_hidden) + " · " + currentDevice.address + } else { + currentDevice.address + }, + hidden = "edit#${currentDevice.id}", + icon = currentDevice.iconId, + ) + } + listItems += + SimpleListItem( + title = resources.getString(R.string.pref_add), + summary = resources.getString(R.string.pref_add_summary), + hidden = "add", + icon = R.drawable.ic_add, + ) + + recyclerView.adapter = DeviceListAdapter(listItems, this) + } + + override fun onItemClicked( + view: View, + position: Int, + ) { + val action = view.findViewById(R.id.hidden).text + if (action.contains("edit")) { + reset = true + startActivity( + Intent(this, EditDeviceActivity::class.java) + .putExtra("deviceId", action.substring(action.indexOf('#') + 1)), + ) + } else if (action == "add") { + reset = true + MaterialAlertDialogBuilder(this) + .setTitle(R.string.pref_add_method) + .setItems(resources.getStringArray(R.array.pref_add_method_array)) { _, which -> + if (which == 0) { + startActivity(Intent(this, EditDeviceActivity::class.java)) + } else if (which == 1) { + startActivity(Intent(this, SearchDevicesActivity::class.java)) + } + } + .show() + } + } + + override fun onItemHandleTouched(viewHolder: RecyclerView.ViewHolder) { + itemTouchHelper.startDrag(viewHolder) + } + + override fun onStart() { + super.onStart() + if (reset) { + reset = false + loadDevices() + } + } +} diff --git a/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt b/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt new file mode 100644 index 0000000..9abb9d2 --- /dev/null +++ b/app/src/main/java/io/github/domi04151309/home/activities/EditDeviceActivity.kt @@ -0,0 +1,387 @@ +package io.github.domi04151309.home.activities + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import android.widget.Button +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast +import androidx.core.net.toUri +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.elevation.SurfaceColors +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.textfield.TextInputLayout +import io.github.domi04151309.home.R +import io.github.domi04151309.home.adapters.IconSpinnerAdapter +import io.github.domi04151309.home.custom.TextWatcher +import io.github.domi04151309.home.data.DeviceItem +import io.github.domi04151309.home.helpers.DeviceSecrets +import io.github.domi04151309.home.helpers.Devices +import io.github.domi04151309.home.helpers.Global + +class EditDeviceActivity : BaseActivity() { + private lateinit var devices: Devices + private lateinit var deviceId: String + private lateinit var deviceSecrets: DeviceSecrets + private lateinit var deviceIcon: ImageView + private lateinit var nameText: TextView + private lateinit var nameBox: TextInputLayout + private lateinit var addressBox: TextInputLayout + private lateinit var iconSpinner: AutoCompleteTextView + private lateinit var modeSpinner: AutoCompleteTextView + private lateinit var specialDivider: View + private lateinit var specialSection: LinearLayout + private lateinit var usernameBox: TextInputLayout + private lateinit var passwordBox: TextInputLayout + private lateinit var configHide: CheckBox + private lateinit var configDirectView: CheckBox + private lateinit var configButton: Button + private lateinit var infoButton: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_edit_device) + + window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this) + + devices = Devices(this) + var deviceId = intent.getStringExtra("deviceId") + val editing = + if (deviceId == null) { + deviceId = devices.generateNewId() + false + } else { + true + } + this.deviceId = deviceId + + deviceSecrets = DeviceSecrets(this, deviceId) + + deviceIcon = findViewById(R.id.deviceIcn) + nameText = findViewById(R.id.nameTxt) + nameBox = findViewById(R.id.nameBox) + addressBox = findViewById(R.id.addressBox) + iconSpinner = findViewById(R.id.iconSpinner).editText as AutoCompleteTextView + modeSpinner = findViewById(R.id.modeSpinner).editText as AutoCompleteTextView + specialDivider = findViewById(R.id.specialDivider) + specialSection = findViewById(R.id.specialSection) + usernameBox = findViewById(R.id.usernameBox) + passwordBox = findViewById(R.id.passwordBox) + configHide = findViewById(R.id.configHide) + configDirectView = findViewById(R.id.configDirectView) + configButton = findViewById(R.id.configBtn) + infoButton = findViewById(R.id.infoBtn) + + findViewById(R.id.idTxt).text = resources.getString(R.string.pref_add_id, deviceId) + + iconSpinner.addTextChangedListener(getIconTextWatcher()) + modeSpinner.addTextChangedListener(getModeTextWatcher(editing)) + nameBox.editText?.addTextChangedListener(getNameTextWatcher()) + + if (editing) { + onEditDevice() + } else { + onCreateDevice() + } + + iconSpinner.setAdapter(IconSpinnerAdapter(resources.getStringArray(R.array.pref_icons))) + modeSpinner.setAdapter( + ArrayAdapter( + this, + R.layout.dropdown_item, + resources.getStringArray(R.array.pref_add_mode_array), + ), + ) + + findViewById(R.id.fab).setOnClickListener { + onFloatingActionButtonClicked() + } + + findViewById(R.id.toolbar).apply { + setNavigationIcon(R.drawable.ic_arrow_back) + setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + } + } + + private fun getIconTextWatcher() = + TextWatcher { + deviceIcon.setImageResource(Global.getIcon(it)) + } + + private fun showExternalInfoBasedOnMode(mode: String) { + configButton.visibility = + if (HAS_CONFIG.contains(mode)) { + View.VISIBLE + } else { + View.GONE + } + infoButton.visibility = + if (HAS_INFO.contains(mode)) { + View.VISIBLE + } else { + View.GONE + } + } + + @Suppress("ComplexCondition") + private fun getModeTextWatcher(editing: Boolean) = + TextWatcher { + val specialVisibility = + if ( + it == Global.FRITZ_AUTO_LOGIN || + it == Global.GRAFANA_AUTO_LOGIN || + it == Global.PI_HOLE_AUTO_LOGIN || + it == Global.SHELLY_GEN_1 + ) { + View.VISIBLE + } else { + View.GONE + } + val usernameVisibility = + if ( + it == Global.GRAFANA_AUTO_LOGIN || + it == Global.SHELLY_GEN_1 + ) { + View.VISIBLE + } else { + View.GONE + } + specialDivider.visibility = specialVisibility + specialSection.visibility = specialVisibility + usernameBox.visibility = usernameVisibility + + if (SUPPORTS_DIRECT_VIEW.contains(it)) { + configDirectView.isEnabled = true + } else { + configDirectView.isEnabled = false + configDirectView.isChecked = false + } + + if (editing) { + showExternalInfoBasedOnMode(it) + } + } + + private fun getNameTextWatcher() = + TextWatcher { + if (it == "") { + nameText.text = resources.getString(R.string.pref_add_name_empty) + } else { + nameText.text = it + } + } + + private fun onEditDevice() { + val device = devices.getDeviceById(deviceId) + nameBox.editText?.setText(device.name) + addressBox.editText?.setText(device.address) + iconSpinner.setText(device.iconName) + modeSpinner.setText(device.mode) + usernameBox.editText?.setText(deviceSecrets.username) + passwordBox.editText?.setText(deviceSecrets.password) + configHide.isChecked = device.hide + configDirectView.isChecked = device.directView + + configButton.setOnClickListener { + onConfigButtonClicked() + } + + infoButton.setOnClickListener { + startActivity(Intent(this, DeviceInfoActivity::class.java).putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId)) + } + + findViewById