From 1804564295ae4a112f0556311f72989eba0fad03 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 1 Dec 2019 15:19:44 +0100 Subject: [PATCH 01/26] Build and Test using Github Actions - job build_and_test runs on push to any feature branches, develop or master - job coverity scan runs on pull requests against develop --- .github/workflows/build_and_test.yml | 71 +++++++++++++++++++++++++ .github/workflows/coverity.yml | 77 ++++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build_and_test.yml create mode 100644 .github/workflows/coverity.yml diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml new file mode 100644 index 0000000..98149a5 --- /dev/null +++ b/.github/workflows/build_and_test.yml @@ -0,0 +1,71 @@ +name: Build & Test +on: + push: + branches: + - develop + - master + - 'features/**' + branches_ignore: + - 'test*' + pull_request: + branches: + - develop +jobs: + build_and_test: + runs-on: ubuntu-latest + env: + BUILD_DIR_HOST: /tmp/build + BUILD_DIR: /build + SRC_DIR: /src + steps: + - name: Clone Repository + uses: actions/checkout@v1 + - name: Check Environment + run: | + docker --version + echo "USER: $USER ($UID:$GID)" + echo "github workspace: $GITHUB_WORKSPACE" + echo "host dir: $BUILD_DIR_HOST" + echo "container build dir: $BUILD_DIR" + echo "container src_dir: $SRC_DIR" + - name: Create build dir + run: | + mkdir -p $BUILD_DIR_HOST + chmod o+w $BUILD_DIR_HOST + touch $BUILD_DIR_HOST/created + ls -la $BUILD_DIR_HOST + - name: Pull docker container + run: docker pull ruschi/devlinuxqtquick2:latest + timeout-minutes: 5 + - name: Start Docker + run: > + docker run -itd -u $UID:$GID --privileged --name build_container + -v$GITHUB_WORKSPACE:$SRC_DIR -v$BUILD_DIR_HOST:$BUILD_DIR + ruschi/devlinuxqtquick2 + - name: Configure + run: > + docker exec build_container cmake + -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=On + -H$SRC_DIR -B$BUILD_DIR + -DBUILD_TESTS=On -DTEST_COVERAGE=On -DBUILD_GTEST_FROM_SRC=On + - name: Build + run: docker exec build_container cmake --build $BUILD_DIR --parallel + - name: Run tests + run: docker exec -w $BUILD_DIR build_container bin/DigitalRooster_gtest + - name: Collect coverage + run: > + docker exec -w $BUILD_DIR build_container + lcov --directory . + --capture --output-file $BUILD_DIR/coverage.info + - name: Prune 3rd party code from coverage info + run: > + docker exec -w $BUILD_DIR build_container + lcov --remove $BUILD_DIR/coverage.info + --output-file $BUILD_DIR/coverage.info + "/usr/*" "*/GTestExternal/*" "*/__/*" + - name: Upload coverage to codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: /tmp/build/coverage.info diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml new file mode 100644 index 0000000..ba744d7 --- /dev/null +++ b/.github/workflows/coverity.yml @@ -0,0 +1,77 @@ +name: Coverity +on: + push: + branches: + - develop + - master + pull_request: + branches: + - develop +jobs: + run_coverity: + runs-on: ubuntu-latest + env: + BUILD_DIR_HOST: /tmp/build + BUILD_DIR: /build + SRC_DIR: /src + COVERITY_INSTALL_DIR: /tmp/coverity + COVERITY_RESULT_DIR: cov-int + COVERITY_TARBALL: digitalrooster_coverity.tar.bz2 + steps: + - name: Clone Repository + uses: actions/checkout@v1 + - name: Check Environment + run: | + docker --version + echo "USER: $USER $UID:$GID" + echo "github workspace: $GITHUB_WORKSPACE" + echo "host dir: $BUILD_DIR_HOST" + echo "container build dir: $BUILD_DIR" + echo "container src_dir: $SRC_DIR" + echo "Coverity tarball:" $COVERITY_TARBALL + echo "Coverity result:" $COVERITY_RESULT_DIR + - name: Create build dir + run: | + mkdir -p $BUILD_DIR_HOST + mkdir -p $COVERITY_INSTALL_DIR + - name: Install Coverity + run: | + wget -q https://scan.coverity.com/download/cxx/linux64 \ + --post-data "token=$TOKEN&project=$GITHUB_REPOSITORY" \ + -O cov-analysis-linux64.tar.gz + tar xzf cov-analysis-linux64.tar.gz --strip 1 -C $COVERITY_INSTALL_DIR + env: + TOKEN: ${{ secrets.COVERITY_TOKEN }} + - name: Pull docker container + run: docker pull ruschi/devlinuxqtquick2:latest + timeout-minutes: 5 + - name: Start Docker + run: > + docker run -itd -u $UID:$GID --privileged --name build_container + -v$GITHUB_WORKSPACE:$SRC_DIR + -v$BUILD_DIR_HOST:$BUILD_DIR + -v$COVERITY_INSTALL_DIR:/coverity + ruschi/devlinuxqtquick2 + - name: Configure (Release, No Tests) + run: > + docker exec build_container cmake + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=On + -H$SRC_DIR -B$BUILD_DIR + -DBUILD_TESTS=Off -DTEST_COVERAGE=Off -DBUILD_GTEST_FROM_SRC=Off + - name: Gather Coverity build info + run: > + docker exec -w $BUILD_DIR build_container + /coverity/bin/cov-build --dir $COVERITY_RESULT_DIR make -j 3 + - name: Package Coverity output + run: tar cjvf $COVERITY_TARBALL -C $BUILD_DIR_HOST $COVERITY_RESULT_DIR + - name: Upload Coverity Info + run: > + curl --form token=$TOKEN + --form email=thomas@ruschival.de + --form file=@$COVERITY_TARBALL + --form version=$GITHUB_SHA + --form description="Auto scan on $GITHUB_REF" + https://scan.coverity.com/builds?project=$GITHUB_REPOSITORY + env: + TOKEN: ${{ secrets.COVERITY_TOKEN }} diff --git a/README.md b/README.md index 64bb0a0..ca72ad4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Build Status](https://travis-ci.com/truschival/DigitalRoosterGui.svg?branch=develop)](https://travis-ci.com/truschival/DigitalRoosterGui) +[![Build Status](https://github.com/truschival/DigitalRoosterGui/workflows/Build%20%26%20Test/badge.svg "Build Develop/Master")](#) [![codecov](https://codecov.io/gh/truschival/DigitalRoosterGui/branch/develop/graph/badge.svg)](https://codecov.io/gh/truschival/DigitalRoosterGui) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a95a270a2f8548f59a26811e7f2de20b)](https://www.codacy.com/app/truschival/DigitalRoosterGui) [![Coverity](https://scan.coverity.com/projects/18711/badge.svg)](https://scan.coverity.com/projects/truschival-digitalroostergui) From ca973bf54eeac9e0311c8ec1d13c8787b8ba4b94 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 1 Dec 2019 15:20:56 +0100 Subject: [PATCH 02/26] Fix uncaught exception --- libsrc/configuration_manager.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index 9f0a2df..a493d28 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -397,8 +397,14 @@ void ConfigurationManager::store_current_config() { for (const auto& alarm : alarms) { QJsonObject alarmcfg; alarmcfg[KEY_ID] = alarm->get_id().toString(); - alarmcfg[KEY_ALARM_PERIOD] = - alarm_period_to_json_string(alarm->get_period()); + try { + alarmcfg[KEY_ALARM_PERIOD] = + alarm_period_to_json_string(alarm->get_period()); + } catch (std::invalid_argument& exc) { + qCCritical(CLASS_LC) << " invalid period " << alarm->get_period() + << " using default " << KEY_ALARM_ONCE; + alarmcfg[KEY_ALARM_PERIOD] = KEY_ALARM_ONCE; + } alarmcfg[KEY_TIME] = alarm->get_time().toString("hh:mm"); alarmcfg[KEY_VOLUME] = alarm->get_volume(); alarmcfg[KEY_URI] = alarm->get_media()->get_url().toString(); From c37d2dbfbd7f42d9d004d47c585e5c78a6b562da Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 1 Dec 2019 20:33:44 +0100 Subject: [PATCH 03/26] Add contributing guidelines --- CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dcc5328 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing + +There are many ways to contribute to DigitalRoosterGui for everyone and all +skills and skill levels. If you don’t like programming, file a bug or feature +request, write documentation or just tell your friends about the project. + +Here is a pretty good write-up on +[contributing to open source.](https://opensource.guide/how-to-contribute/) + +It is a good idea to open an issue in the respective project beforehand +to share the thought and reach consensus before doing the programming. + +## Social rules + +*TL;DR:* This project welcomes everybody who acts decently and professionally. +Communicate with the other as if you are sitting together at your grandparents +dining table. If your grandma would frown upon your statement it is not +appropriate for this project either. + +## Some technical rules + +- This project uses + [git flow](https://nvie.com/posts/a-successful-git-branching-model/) + as development workflow. All features are developed in feature branches. + Only trivial corrections are fixed on the branch 'develop' directly. + +- Create a pull request against the 'develop' branch. + +- Write meaningful git + [commit messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) + +- You should sign-off your commits (``git commit -s``) using a PGP key. This + `Signed-Off-by: your full name ` means that you publish your + changes to the project under the projects license (GPLv3) and you are + allowed to do this. + +- **Code Style** - easy just automate it use the .clang-format file on + your code ``clang-format style=file`` + I prefer ``snake_case`` for variables and methods but use ``PascalCase`` + for classes - but this is no hard rule that prevents a merge. + +- **Naming** - give the variables, classes, file names etc. meaningful names. + *Again, name it professionally without slur (remember you want to show it + to your grandma)* + +- **Comments** Check your comments, when you are done! The comment should + match the code, be concise and contain relevant information + +- **Write Tests** - code without unit test cases will most likely not get + merged + +Note: Not all changes get immediately (or eventually) merged, even if all rules +are met. From 028ac5305120146d16599c1ecab54dc3322953c2 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Fri, 6 Dec 2019 10:41:08 +0100 Subject: [PATCH 04/26] Cleanup CMakeList.txt refactor common install commands and move to includable CmakeModules file --- CMakeLists.txt | 8 +- CMakeModules/InstallLibraryComponents.cmake | 48 ++++++++++++ libhwif/CMakeLists.txt | 83 +++++--------------- libsrc/CMakeLists.txt | 84 ++++----------------- qtgui/CMakeLists.txt | 33 ++++---- test/CMakeLists.txt | 38 +++++----- wpa_ctrl/CMakeLists.txt | 75 ++++-------------- 7 files changed, 127 insertions(+), 242 deletions(-) create mode 100644 CMakeModules/InstallLibraryComponents.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f8d37f..3723e41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,7 +137,7 @@ include_directories(${PROJECT_INCLUDE_DIR} ${GENERATED_DIR}) # Output configuration #------------------------------------------------------------------------------- #Since the "standard" cmake template works with component name set it -SET(COMPONENT_NAME ${PROJECT_NAME}) +#SET(COMPONENT_NAME ${PROJECT_NAME}) # Make debug libs visible per default -> SET_TARGET_PROPERTIES SET(CMAKE_DEBUG_POSTFIX "d") @@ -155,12 +155,9 @@ SET(INSTALL_INCLUDE_DIR include CACHE PATH "Installation directory for header files") SET(INSTALL_CMAKE_DIR ${INSTALL_LIB_DIR}/cmake/ CACHE PATH "Installation directory for CMake files") - SET(INSTALL_DOC_DIR "doc/${PROJECT_NAME}" CACHE PATH +SET(INSTALL_DOC_DIR "doc/${PROJECT_NAME}" CACHE PATH "Installation directory for doxygen docs") -SET(version_config "${GENERATED_DIR}/${COMPONENT_NAME}ConfigVersion.cmake") -SET(component_config "${GENERATED_DIR}/${COMPONENT_NAME}Config.cmake") -SET(targets_export_name "${COMPONENT_NAME}Targets") export(PACKAGE ${PROJECT_NAME}) #------------------------------------------------------------------------------- @@ -279,7 +276,6 @@ SET(CPACK_COMPONENT_DEVELOP_DISPLAY_NAME "Libraries + Headers") SET(CPACK_COMPONENT_APIDOC_DISPLAY_NAME "API Documentation") SET(CPACK_COMPONENT_DEVELOP_DESCRIPTION "Components needed for development") SET(CPACK_COMPONENT_APIDOC_DESCRIPTION "API Documentation") - SET(CPACK_NSIS_CONTACT "thomas@ruschival.de") SET(CPACK_NSIS_MODIFY_PATH ON) diff --git a/CMakeModules/InstallLibraryComponents.cmake b/CMakeModules/InstallLibraryComponents.cmake new file mode 100644 index 0000000..a981e8b --- /dev/null +++ b/CMakeModules/InstallLibraryComponents.cmake @@ -0,0 +1,48 @@ +MESSAGE(STATUS + "** Generating Package Configurations for ${COMPONENT_NAME} **") + +SET(version_config "${GENERATED_DIR}/${COMPONENT_NAME}ConfigVersion.cmake") +SET(component_config "${GENERATED_DIR}/${COMPONENT_NAME}Config.cmake") +SET(targets_export_name "${COMPONENT_NAME}Targets") + +include(CMakePackageConfigHelpers) +WRITE_BASIC_PACKAGE_VERSION_FILE( + ${version_config} + VERSION ${COMPONENT_VERSION} + COMPATIBILITY SameMajorVersion +) + +# Configure 'Config.cmake' +# Note: variable 'targets_export_name' used +CONFIGURE_FILE("${CMAKE_SOURCE_DIR}/cmake/Config.cmake.in" + "${component_config}" @ONLY) + +INSTALL(TARGETS ${COMPONENT_NAME} + EXPORT ${targets_export_name} + COMPONENT DEVELOP + ARCHIVE DESTINATION ${INSTALL_LIB_DIR} + LIBRARY DESTINATION ${INSTALL_LIB_DIR} + RUNTIME DESTINATION ${INSTALL_BIN_DIR} + # this will add -Iinclude/component to client compile flags + #INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR}/${COMPONENT_PATH} + INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR} + ) + +INSTALL(DIRECTORY + ${PROJECT_INCLUDE_DIR}/${COMPONENT_PATH} + COMPONENT DEVELOP + DESTINATION ${INSTALL_INCLUDE_DIR} +) + +INSTALL( + EXPORT ${targets_export_name} + COMPONENT DEVELOP + NAMESPACE "${PROJECT_NAME}::" + DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" + ) + +INSTALL( + FILES "${component_config}" "${version_config}" + COMPONENT DEVELOP + DESTINATION "${INSTALL_CMAKE_DIR}/${PROJECT_NAME}/${COMPONENT_NAME}" + ) \ No newline at end of file diff --git a/libhwif/CMakeLists.txt b/libhwif/CMakeLists.txt index b0ec9f0..2efddb8 100644 --- a/libhwif/CMakeLists.txt +++ b/libhwif/CMakeLists.txt @@ -2,20 +2,13 @@ MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") # name of library (without lib- prefix) SET(LIBRARY_NAME hwif) - -#Since the "standard" cmake template works with component name set it -set(COMPONENT_NAME "HWIF") - +# Component name (what to build and install) used in add_executable or add_library +set(COMPONENT_NAME ${LIBRARY_NAME}) +# Interface/binary version +SET(COMPONENT_VERSION ${PROJECT_VERSION}) #includes etc. in folder SET(COMPONENT_PATH ${LIBRARY_NAME}) -set(version_config "${GENERATED_DIR}/${COMPONENT_NAME}ConfigVersion.cmake") -set(component_config "${GENERATED_DIR}/${COMPONENT_NAME}Config.cmake") -set(targets_export_name "${COMPONENT_NAME}Targets") - -# Interface/binary version -SET(LIBRARY_VERSION ${PROJECT_VERSION}) - # QT5 Components used in library find_package(Qt5 COMPONENTS Core) @@ -55,43 +48,43 @@ SET(MOC_SRCS #------------------------------ # Output a library #------------------------------ -ADD_LIBRARY(${LIBRARY_NAME} STATIC +ADD_LIBRARY(${COMPONENT_NAME} STATIC ${SRCS} ${MOC_SRCS} ) -SET_TARGET_PROPERTIES( - ${LIBRARY_NAME} PROPERTIES - VERSION ${LIBRARY_VERSION} - SOVERSION ${LIBRARY_VERSION} +SET_TARGET_PROPERTIES(${COMPONENT_NAME} + PROPERTIES + VERSION ${COMPONENT_VERSION} + SOVERSION ${COMPONENT_VERSION} ) -TARGET_INCLUDE_DIRECTORIES( - ${LIBRARY_NAME} +TARGET_INCLUDE_DIRECTORIES(${COMPONENT_NAME} PRIVATE - $/hwif + $/${COMPONENT_PATH} PUBLIC $ ) -TARGET_COMPILE_DEFINITIONS(${LIBRARY_NAME} +TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} PUBLIC ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${LIBRARY_NAME} PRIVATE +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} + PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}> ) target_link_libraries( - ${LIBRARY_NAME} + ${COMPONENT_NAME} PRIVATE Qt5::Core ) if(NOT ${SYSTEM_TARGET_NAME} STREQUAL "Host" ) target_link_libraries( - ${LIBRARY_NAME} + ${COMPONENT_NAME} PUBLIC ${WIRING_LIB} ) @@ -100,47 +93,5 @@ endif() #----- # Install #----- -MESSAGE(STATUS "** Generating Package Configurations **") - -include(CMakePackageConfigHelpers) -WRITE_BASIC_PACKAGE_VERSION_FILE( - ${version_config} - VERSION ${LIBRARY_VERSION} - COMPATIBILITY SameMajorVersion -) - -# Configure 'Config.cmake' -# Note: variable 'targets_export_name' used -CONFIGURE_FILE("${CMAKE_SOURCE_DIR}/cmake/Config.cmake.in" - "${component_config}" @ONLY) - -INSTALL(TARGETS ${LIBRARY_NAME} - EXPORT ${targets_export_name} - COMPONENT DEVELOP - ARCHIVE DESTINATION ${INSTALL_LIB_DIR} - LIBRARY DESTINATION ${INSTALL_LIB_DIR} - RUNTIME DESTINATION ${INSTALL_BIN_DIR} - # this will add -Iinclude/transmog to client compile flags - #INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR}/${COMPONENT_PATH} - INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR} - ) - -INSTALL(DIRECTORY - ${PROJECT_INCLUDE_DIR}/${COMPONENT_PATH} - COMPONENT DEVELOP - DESTINATION ${INSTALL_INCLUDE_DIR} -) - -INSTALL( - EXPORT ${targets_export_name} - COMPONENT DEVELOP - NAMESPACE "${COMPONENT_NAME}::" - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) - -INSTALL( - FILES "${component_config}" "${version_config}" - COMPONENT DEVELOP - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) +include(InstallLibraryComponents) diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt index 4b7d9cb..ce4d1a6 100644 --- a/libsrc/CMakeLists.txt +++ b/libsrc/CMakeLists.txt @@ -2,22 +2,13 @@ MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") # name of library (without lib- prefix) string(TOLOWER ${PROJECT_NAME} LIBRARY_NAME) - -#Since the "standard" cmake template works with component name set it -set(COMPONENT_NAME ${PROJECT_NAME}) - -#includes etc. in folder -SET(COMPONENT_PATH ${LIBRARY_NAME}) - -SET(version_config "${GENERATED_DIR}/${COMPONENT_NAME}ConfigVersion.cmake") -SET(component_config "${GENERATED_DIR}/${COMPONENT_NAME}Config.cmake") -SET(targets_export_name "${COMPONENT_NAME}Targets") - +# Component name (what to build and install) used in add_executable or add_library +set(COMPONENT_NAME ${LIBRARY_NAME}) # Interface/binary version -SET(LIBRARY_VERSION ${PROJECT_VERSION}) +SET(COMPONENT_VERSION ${PROJECT_VERSION}) +#includes etc. in folder (no subfolder here) +SET(COMPONENT_PATH "") -# add . to the includes during the build -#SET(CMAKE_INCLUDE_CURRENT_DIR ON) SET(CMAKE_POSITION_INDEPENDENT_CODE ON) # QT5 Components used in library @@ -103,38 +94,36 @@ SET(MOC_SRC #------------------------------ # Output a library #------------------------------ -ADD_LIBRARY(${LIBRARY_NAME} STATIC +ADD_LIBRARY(${COMPONENT_NAME} STATIC ${SRCS} ${HW_DEP_SRCS} ${MOC_SRC} ) -SET_TARGET_PROPERTIES( - ${LIBRARY_NAME} PROPERTIES - VERSION ${LIBRARY_VERSION} - SOVERSION ${LIBRARY_VERSION} +SET_TARGET_PROPERTIES(${COMPONENT_NAME} + PROPERTIES + VERSION ${COMPONENT_VERSION} + SOVERSION ${COMPONENT_VERSION} ) -TARGET_INCLUDE_DIRECTORIES( - ${LIBRARY_NAME} +TARGET_INCLUDE_DIRECTORIES(${COMPONENT_NAME} PRIVATE $ PUBLIC $ ) -TARGET_COMPILE_DEFINITIONS(${LIBRARY_NAME} +TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} PUBLIC ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${LIBRARY_NAME} +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}> ) -target_link_libraries( - ${LIBRARY_NAME} +target_link_libraries(${COMPONENT_NAME} PUBLIC Qt5::Core Qt5::Multimedia @@ -145,50 +134,7 @@ target_link_libraries( Qt5::Network ) - #------------------------------ # Install #------------------------------ -MESSAGE(STATUS "** Generating Package Configurations **") - -include(CMakePackageConfigHelpers) -WRITE_BASIC_PACKAGE_VERSION_FILE( - ${version_config} - VERSION ${LIBRARY_VERSION} - COMPATIBILITY SameMajorVersion -) - -# Configure 'Config.cmake' -# Note: variable 'targets_export_name' used -CONFIGURE_FILE("${CMAKE_SOURCE_DIR}/cmake/Config.cmake.in" - "${component_config}" @ONLY) - -INSTALL(TARGETS ${LIBRARY_NAME} - EXPORT ${targets_export_name} - COMPONENT DEVELOP - ARCHIVE DESTINATION ${INSTALL_LIB_DIR} - LIBRARY DESTINATION ${INSTALL_LIB_DIR} - RUNTIME DESTINATION ${INSTALL_BIN_DIR} - # this will add -Iinclude/digitalrooster to client compile flags - #INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR}/${COMPONENT_PATH} - INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR} - ) - -INSTALL(DIRECTORY - ${PROJECT_INCLUDE_DIR}/ - COMPONENT DEVELOP - DESTINATION ${INSTALL_INCLUDE_DIR}/${COMPONENT_PATH} - ) - -INSTALL( - EXPORT ${targets_export_name} - COMPONENT DEVELOP - NAMESPACE "${COMPONENT_NAME}::" - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) - -INSTALL( - FILES "${component_config}" "${version_config}" - COMPONENT DEVELOP - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) +include(InstallLibraryComponents) diff --git a/qtgui/CMakeLists.txt b/qtgui/CMakeLists.txt index 634c3cf..6a09ce6 100644 --- a/qtgui/CMakeLists.txt +++ b/qtgui/CMakeLists.txt @@ -1,9 +1,13 @@ MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") + SET(BINARY_NAME "DigitalRoosterGui") +# Component name (what to build and install) used in add_executable or add_library +set(COMPONENT_NAME ${BINARY_NAME}) +# Interface/binary version +SET(COMPONENT_VERSION ${PROJECT_VERSION}) # Gui has extra QT dependencies: QML and QTquick find_package(Qt5 COMPONENTS Qml Quick REQUIRED) - #Find includes in corresponding build directories set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -61,34 +65,32 @@ LIST(APPEND CPP_DEFS "" ) # Binary #------------------------------ # Executable (for target) -ADD_EXECUTABLE(${BINARY_NAME} +ADD_EXECUTABLE(${COMPONENT_NAME} ${SRCS} ${UI_HDRS} ${MOC_SRC} ${QRC_SRC} ) -SET_TARGET_PROPERTIES( - ${BINARY_NAME} +SET_TARGET_PROPERTIES(${COMPONENT_NAME} PROPERTIES - VERSION ${PROJECT_VERSION} - DEBUG_POSTFIX "_dbg" + VERSION ${COMPONENT_VERSION} ) -TARGET_COMPILE_DEFINITIONS(${BINARY_NAME} +TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} # no public flags, nobody should use the main app as dependency PRIVATE ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${BINARY_NAME} PRIVATE +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} + PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}> ) # Linkage with gtest_main coverage etc. -TARGET_LINK_LIBRARIES( - ${BINARY_NAME} +TARGET_LINK_LIBRARIES(${COMPONENT_NAME} PRIVATE ${PROJECT_CORE_LIB} Qt5::Quick @@ -99,13 +101,4 @@ TARGET_LINK_LIBRARIES( #----- # Install #----- -MESSAGE(STATUS "** Generating Package Configurations **") - -INSTALL(TARGETS ${BINARY_NAME} - EXPORT ${targets_export_name} - COMPONENT APPLICATION - ARCHIVE DESTINATION ${INSTALL_LIB_DIR} - LIBRARY DESTINATION ${INSTALL_LIB_DIR} - RUNTIME DESTINATION ${INSTALL_BIN_DIR} - INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR} - ) +include(InstallLibraryComponents) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 882c0f5..cc3ed51 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,8 +1,9 @@ - MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") +MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") SET(BINARY_NAME "${PROJECT_NAME}_gtest") - -#set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(COMPONENT_NAME ${BINARY_NAME}) +# Interface/binary version +SET(COMPONENT_VERSION ${PROJECT_VERSION}) # Extra QT5 components for tests find_package(Qt5 COMPONENTS Test REQUIRED) @@ -199,29 +200,28 @@ ENDIF() #------------------------------ # UnitTests Executable (for target) -add_executable( - ${BINARY_NAME} +add_executable(${COMPONENT_NAME} ${TEST_HARNESS_SRCS} ) -SET_TARGET_PROPERTIES( - ${BINARY_NAME} +SET_TARGET_PROPERTIES(${COMPONENT_NAME} PROPERTIES VERSION ${PROJECT_VERSION} VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" ) -TARGET_COMPILE_DEFINITIONS(${BINARY_NAME} +TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} PUBLIC ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${BINARY_NAME} PRIVATE +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} + PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}>) # Linkage with gtest_main coverage etc. TARGET_LINK_LIBRARIES( - ${BINARY_NAME} + ${COMPONENT_NAME} ${DUT_LIBS} # Units under test GMock # main not required, implemented in test.cpp GTest @@ -236,24 +236,24 @@ IF(TEST_COVERAGE) #Remove googletest, autogenerated QT code and headers for testcovera SET(COVERAGE_EXCLUDES '*GTestExternal/*' "*_autogen*" '/usr/*') SETUP_TARGET_FOR_COVERAGE( - NAME ${BINARY_NAME}_coverage # New target name - EXECUTABLE ${BINARY_NAME} -j 4 # Executable in PROJECT_BINARY_DIR - DEPENDENCIES ${BINARY_NAME}) + NAME ${COMPONENT_NAME}_coverage # New target name + EXECUTABLE ${COMPONENT_NAME} -j 4 # Executable in PROJECT_BINARY_DIR + DEPENDENCIES ${COMPONENT_NAME}) ELSE(NOT MSVC) - SET_PROPERTY(TARGET ${BINARY_NAME} APPEND PROPERTY LINK_FLAGS + SET_PROPERTY(TARGET ${COMPONENT_NAME} APPEND PROPERTY LINK_FLAGS /PROFILE) ENDIF(NOT MSVC) ENDIF(TEST_COVERAGE) # Call the testBinary with junit-xml output ADD_TEST(junitout - "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BINARY_NAME}" + "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${COMPONENT_NAME}" --gtest_output=xml:gtestresults.xml WORKING_DIRECTORY ${PROJECT_BINARY_DIR} ) +#----- +# Install (optional) +#----- IF(INSTALL_UNIT_TEST_ON_TARGET) - INSTALL(TARGETS - ${TEST_BINARY_NAME} - RUNTIME DESTINATION ${TARGET_EXE_INSTALL_DIR} - ) + include(IntallLibraryComponents) ENDIF(INSTALL_UNIT_TEST_ON_TARGET) diff --git a/wpa_ctrl/CMakeLists.txt b/wpa_ctrl/CMakeLists.txt index 0c6741c..b0ab867 100644 --- a/wpa_ctrl/CMakeLists.txt +++ b/wpa_ctrl/CMakeLists.txt @@ -2,20 +2,13 @@ MESSAGE(STATUS "Checking ${CMAKE_CURRENT_SOURCE_DIR} ") # name of library (without lib- prefix) SET(LIBRARY_NAME wpa_ctrl) - -#Since the "standard" cmake template works with component name set it -SET(COMPONENT_NAME "wpa_ctrl") - +# Component name (what to build and install) used in add_executable or add_library +set(COMPONENT_NAME ${LIBRARY_NAME}) +# Interface/binary version +SET(COMPONENT_VERSION ${PROJECT_VERSION}) #includes etc. in folder SET(COMPONENT_PATH ${LIBRARY_NAME}) -SET(version_config "${GENERATED_DIR}/${COMPONENT_NAME}ConfigVersion.cmake") -SET(component_config "${GENERATED_DIR}/${COMPONENT_NAME}Config.cmake") -SET(targets_export_name "${COMPONENT_NAME}Targets") - -# Interface/binary version -SET(LIBRARY_VERSION ${PROJECT_VERSION}) - #------------------------------ # add compile definitions #------------------------------ @@ -42,30 +35,30 @@ endif() #------------------------------ # Output a library #------------------------------ -ADD_LIBRARY(${LIBRARY_NAME} +ADD_LIBRARY(${COMPONENT_NAME} STATIC ${SRCS} ) -SET_TARGET_PROPERTIES(${LIBRARY_NAME} +SET_TARGET_PROPERTIES(${COMPONENT_NAME} PROPERTIES - VERSION ${LIBRARY_VERSION} - SOVERSION ${LIBRARY_VERSION} + VERSION ${COMPONENT_VERSION} + SOVERSION ${COMPONENT_VERSION} ) -TARGET_INCLUDE_DIRECTORIES(${LIBRARY_NAME} +TARGET_INCLUDE_DIRECTORIES(${COMPONENT_NAME} PRIVATE - $/wpa_ctrl + $/${COMPONENT_PATH} PUBLIC $ ) -TARGET_COMPILE_DEFINITIONS(${LIBRARY_NAME} +TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} PUBLIC "" PRIVATE ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${LIBRARY_NAME} +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}> @@ -74,46 +67,4 @@ TARGET_COMPILE_OPTIONS(${LIBRARY_NAME} #----- # Install #----- -MESSAGE(STATUS "** Generating Package Configurations **") - -include(CMakePackageConfigHelpers) -WRITE_BASIC_PACKAGE_VERSION_FILE( - ${version_config} - VERSION ${LIBRARY_VERSION} - COMPATIBILITY SameMajorVersion -) - -# Configure 'Config.cmake' -# Note: variable 'targets_export_name' used -CONFIGURE_FILE("${CMAKE_SOURCE_DIR}/cmake/Config.cmake.in" - "${component_config}" @ONLY) - -INSTALL(TARGETS ${LIBRARY_NAME} - EXPORT ${targets_export_name} - COMPONENT DEVELOP - ARCHIVE DESTINATION ${INSTALL_LIB_DIR} - LIBRARY DESTINATION ${INSTALL_LIB_DIR} - RUNTIME DESTINATION ${INSTALL_BIN_DIR} - # this will add -Iinclude/transmog to client compile flags - #INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR}/${COMPONENT_PATH} - INCLUDES DESTINATION ${INSTALL_INCLUDE_DIR} - ) - -INSTALL(DIRECTORY - ${PROJECT_INCLUDE_DIR}/${COMPONENT_PATH} - COMPONENT DEVELOP - DESTINATION ${INSTALL_INCLUDE_DIR} - ) - -INSTALL( - EXPORT ${targets_export_name} - COMPONENT DEVELOP - NAMESPACE "${COMPONENT_NAME}::" - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) - -INSTALL( - FILES "${component_config}" "${version_config}" - COMPONENT DEVELOP - DESTINATION "${INSTALL_CMAKE_DIR}/${COMPONENT_NAME}" - ) +include(InstallLibraryComponents) From c7a3df611cd87c72f7d901251b9b3ba4a711dda1 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 15 Dec 2019 14:52:00 +0100 Subject: [PATCH 05/26] Fix set_positon on remote media When media was not loaded quickly enough set_positon did not only have no effect. but positon of PodcastEpisode was actually reset to 0. --- include/PlayableItem.hpp | 13 ++ libsrc/mediaplayerproxy.cpp | 308 ++++++++++++++++++++------------- test/test_mediaplayerproxy.cpp | 73 ++++---- 3 files changed, 241 insertions(+), 153 deletions(-) diff --git a/include/PlayableItem.hpp b/include/PlayableItem.hpp index a726309..2ecce1e 100644 --- a/include/PlayableItem.hpp +++ b/include/PlayableItem.hpp @@ -86,6 +86,16 @@ class PlayableItem : public QObject { */ virtual void set_position(qint64 newVal); + /** + * Mark as seekable: position can be updated + */ + void set_seekable(bool seek){ + seekable = seek; + } + bool is_seekable() const{ + return seekable; + }; + /** * Title for Playable item * @return title @@ -151,6 +161,9 @@ class PlayableItem : public QObject { /** Current position in stream */ qint64 position = 0; + /** Can ressource positon be set ?*/ + bool seekable{false}; + protected: /** * Display name means differnt things to differnt playable items diff --git a/libsrc/mediaplayerproxy.cpp b/libsrc/mediaplayerproxy.cpp index 87f8aff..95abb32 100644 --- a/libsrc/mediaplayerproxy.cpp +++ b/libsrc/mediaplayerproxy.cpp @@ -20,201 +20,261 @@ #include using namespace DigitalRooster; -static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy"); +static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy") +; /***********************************************************************/ -MediaPlayerProxy::MediaPlayerProxy() - : backend(std::make_unique()) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - QObject::connect(backend.get(), &QMediaPlayer::mediaChanged, - [=](const QMediaContent& media) { emit media_changed(media); }); - - QObject::connect( - backend.get(), &QMediaPlayer::positionChanged, [=](qint64 position) { - emit position_changed(position); - if (current_item.get() != nullptr) { - current_item->set_position(position); - } - }); - - QObject::connect(backend.get(), - static_cast( - &QMediaPlayer::error), - [=](QMediaPlayer::Error err) { - qCWarning(CLASS_LC) << "Player Error" << err; - emit error(err); - }); - - QObject::connect(backend.get(), &QMediaPlayer::mutedChanged, - [=](bool muted) { emit muted_changed(muted); }); - - QObject::connect(backend.get(), &QMediaPlayer::stateChanged, - [=](QMediaPlayer::State state) { emit playback_state_changed(state); }); - - QObject::connect(backend.get(), &QMediaPlayer::durationChanged, - [=](qint64 duration) { emit duration_changed(duration); }); - - QObject::connect(backend.get(), &QMediaPlayer::seekableChanged, - [=](bool seekable) { emit seekable_changed(seekable); }); - - QObject::connect(backend.get(), &QMediaPlayer::mediaStatusChanged, - [=](QMediaPlayer::MediaStatus status) { - emit media_status_changed(status); - }); - - QObject::connect(backend.get(), &QMediaPlayer::metaDataAvailableChanged, - [=](bool available) { - qCDebug(CLASS_LC) << " metaDataAvailableChanged " << available; - if (available) { - QString title = - backend->metaData(QMediaMetaData::Title).toString(); - QString publisher = - backend->metaData(QMediaMetaData::Publisher).toString(); - - qCDebug(CLASS_LC) - << "\n\tTitle:" << title +MediaPlayerProxy::MediaPlayerProxy() : + backend(std::make_unique()) { + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + QObject::connect(backend.get(), &QMediaPlayer::mediaChanged, + [=](const QMediaContent &media) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy media_changed()"; + /* + * We know nothing of new media, assume it is not seekable + * otherwise we might overwrite/lose saved position + */ + current_item->set_seekable(false); + emit media_changed(media); + }); + + QObject::connect(backend.get(), &QMediaPlayer::positionChanged, + [=](qint64 position) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy position_changed()" << position; + emit position_changed(position); + /** Only update position of seekable media */ + if (current_item.get() != nullptr + && current_item->is_seekable()) { + current_item->set_position(position); + } + }); + + QObject::connect(backend.get(), + static_cast(&QMediaPlayer::error), + [=](QMediaPlayer::Error err) { + qCWarning(CLASS_LC) + << "MediaPlayerProxy Error" << err; + emit error(err); + }); + + QObject::connect(backend.get(), &QMediaPlayer::mutedChanged, + [=](bool muted) { + emit muted_changed(muted); + }); + + QObject::connect(backend.get(), &QMediaPlayer::stateChanged, + [=](QMediaPlayer::State state) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy playback_state_changed()" << state; + + emit playback_state_changed(state); + }); + + QObject::connect(backend.get(), &QMediaPlayer::durationChanged, + [=](qint64 duration) { + emit duration_changed(duration); + }); + + QObject::connect(backend.get(), &QMediaPlayer::seekableChanged, + [=](bool seekable) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy seekable_changed()" << seekable; + current_item->set_seekable(seekable); + emit seekable_changed(seekable); + /* jump to previously saved position once we know we can seek */ + if(current_item->is_seekable()){ + /* update backend position */ + set_position(current_item->get_position()); + } + }); + + QObject::connect(backend.get(), &QMediaPlayer::mediaStatusChanged, + [=](QMediaPlayer::MediaStatus status) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy media_status_changed()" << status; + emit media_status_changed(status); + }); + + QObject::connect(backend.get(), &QMediaPlayer::metaDataAvailableChanged, + [=](bool available) { + qCDebug(CLASS_LC) + << " metaDataAvailableChanged " << available; + if (available) { + QString title = + backend->metaData(QMediaMetaData::Title).toString(); + QString publisher = backend->metaData( + QMediaMetaData::Publisher).toString(); + + qCDebug(CLASS_LC) + << "\n\tTitle:" << title << "\n\tPublisher:" << publisher << "\n\tPublisher:" << publisher - << "\n\tAlbumArtist:" - << backend->metaData(QMediaMetaData::AlbumArtist).toString() - << "\n\tAuthor:" - << backend->metaData(QMediaMetaData::Author).toString() - << "\n\tDescription:" - << backend->metaData(QMediaMetaData::Description).toString(); - - if (title != "") { - current_item->set_title(title); - } - if (publisher != "") { - current_item->set_publisher(publisher); - } - } else { - qCDebug(CLASS_LC) << "No metadata."; - } - }); + << "\n\tAlbumArtist:" + << backend->metaData(QMediaMetaData::AlbumArtist).toString() + << "\n\tAuthor:" + << backend->metaData(QMediaMetaData::Author).toString() + << "\n\tDescription:" + << backend->metaData(QMediaMetaData::Description).toString(); + + if (title != "") { + current_item->set_title(title); + } + if (publisher != "") { + current_item->set_publisher(publisher); + } + } else { + qCDebug(CLASS_LC) + << "No metadata."; + } + }); } /*****************************************************************************/ void MediaPlayerProxy::do_seek(qint64 incr) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - if (seekable()) - set_position(get_position() + incr); -}; + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + if (seekable()) + set_position(get_position() + incr); +} +; /*****************************************************************************/ bool MediaPlayerProxy::is_seekable() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->isSeekable(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->isSeekable(); } /*****************************************************************************/ QMediaPlayer::Error MediaPlayerProxy::do_error() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->error(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->error(); } /*****************************************************************************/ qint64 MediaPlayerProxy::do_get_position() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->position(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->position(); } /*****************************************************************************/ QMediaPlayer::MediaStatus MediaPlayerProxy::do_media_status() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->mediaStatus(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->mediaStatus(); } /*****************************************************************************/ qint64 MediaPlayerProxy::do_get_duration() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->duration(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->duration(); } /*****************************************************************************/ void MediaPlayerProxy::do_set_position(qint64 position) { - qCDebug(CLASS_LC) << Q_FUNC_INFO << position; - backend->setPosition(position); + qCDebug(CLASS_LC) + << Q_FUNC_INFO << position; + backend->setPosition(position); } /*****************************************************************************/ bool MediaPlayerProxy::is_muted() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->isMuted(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->isMuted(); } /*****************************************************************************/ int MediaPlayerProxy::do_get_volume() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return linear_volume; + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return linear_volume; } /*****************************************************************************/ void MediaPlayerProxy::do_set_media( - std::shared_ptr media) { - qCDebug(CLASS_LC) << Q_FUNC_INFO << media->get_display_name(); - current_item = media; - auto previous_position = media->get_position(); + std::shared_ptr media) { + qCDebug(CLASS_LC) + << Q_FUNC_INFO << media->get_display_name(); + current_item = media; + auto previous_position = media->get_position(); - backend->setMedia(QMediaContent(media->get_url())); - if (previous_position != 0) { - qDebug() << "restarting from position" << previous_position; - set_position(previous_position); - } + backend->setMedia(QMediaContent(media->get_url())); + if (previous_position != 0) { + qCDebug(CLASS_LC) + << "restarting from position" << previous_position; + set_position(previous_position); + } } /*****************************************************************************/ -void MediaPlayerProxy::do_set_playlist(QMediaPlaylist* playlist) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - backend->setPlaylist(playlist); +void MediaPlayerProxy::do_set_playlist(QMediaPlaylist *playlist) { + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + backend->setPlaylist(playlist); } /*****************************************************************************/ void MediaPlayerProxy::do_set_muted(bool muted) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - backend->setMuted(muted); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + backend->setMuted(muted); } /*****************************************************************************/ QMediaPlayer::State MediaPlayerProxy::do_playback_state() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return backend->state(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + return backend->state(); } /*****************************************************************************/ void MediaPlayerProxy::do_set_volume(int volume) { - qCDebug(CLASS_LC) << Q_FUNC_INFO << volume; - if (volume < 0 || volume > 100) { - qCWarning(CLASS_LC) << "invalid volume (must be 0..100%)"; - return; - } - linear_volume = volume; - emit volume_changed(volume); + qCDebug(CLASS_LC) + << Q_FUNC_INFO << volume; + if (volume < 0 || volume > 100) { + qCWarning(CLASS_LC) + << "invalid volume (must be 0..100%)"; + return; + } + linear_volume = volume; + emit volume_changed(volume); - /* backend works with logarithmic volume */ - auto adjusted_volume = QAudio::convertVolume(volume / qreal(100.0), - QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale); - backend->setVolume(qRound(adjusted_volume * 100.0)); + /* backend works with logarithmic volume */ + auto adjusted_volume = QAudio::convertVolume(volume / qreal(100.0), + QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale); + backend->setVolume(qRound(adjusted_volume * 100.0)); } /*****************************************************************************/ void MediaPlayerProxy::do_increment_volume(int increment) { - qCDebug(CLASS_LC) << Q_FUNC_INFO << increment; - qCDebug(CLASS_LC) << " current volume" << linear_volume; - set_volume(linear_volume + increment); + qCDebug(CLASS_LC) + << Q_FUNC_INFO << increment; + qCDebug(CLASS_LC) + << " current volume" << linear_volume; + set_volume(linear_volume + increment); } /*****************************************************************************/ void MediaPlayerProxy::do_pause() { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - backend->pause(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + backend->pause(); } /*****************************************************************************/ void MediaPlayerProxy::do_play() { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - backend->play(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + backend->play(); } /*****************************************************************************/ void MediaPlayerProxy::do_stop() { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - backend->stop(); + qCDebug(CLASS_LC) + << Q_FUNC_INFO; + backend->stop(); } /*****************************************************************************/ diff --git a/test/test_mediaplayerproxy.cpp b/test/test_mediaplayerproxy.cpp index 9544772..e02c071 100644 --- a/test/test_mediaplayerproxy.cpp +++ b/test/test_mediaplayerproxy.cpp @@ -28,30 +28,35 @@ using namespace DigitalRooster; class PlayerFixture : public virtual ::testing::Test { public: PlayerFixture() - : podcast( - std::make_unique("TestEpisode", - QUrl::fromLocalFile(TEST_FILE_PATH + "/testaudio.mp3"))) { + : local_audio( + std::make_shared("TestEpisode", + QUrl::fromLocalFile(TEST_FILE_PATH + "/testaudio.mp3"))) + , remote_audio( + std::make_shared("Remote", + QUrl("http://www.ruschival.de/wp-content/uploads/2019/12/" + "sample-128k.mp3"))) { } protected: - std::shared_ptr podcast; + std::shared_ptr local_audio; + std::shared_ptr remote_audio; const qint64 desired_pos = 10000; // 10 seconds MediaPlayerProxy dut; }; + /*****************************************************************************/ TEST_F(PlayerFixture, emitMediaChanged) { QSignalSpy spy(&dut, SIGNAL(media_changed(const QMediaContent&))); ASSERT_TRUE(spy.isValid()); - dut.set_media(podcast); + dut.set_media(local_audio); ASSERT_EQ(spy.count(), 1); } - /*****************************************************************************/ TEST_F(PlayerFixture, emitStateChanged) { QSignalSpy spy(&dut, SIGNAL(playback_state_changed(QMediaPlayer::State))); ASSERT_TRUE(spy.isValid()); - dut.set_media(podcast); + dut.set_media(local_audio); dut.play(); spy.wait(500); dut.pause(); @@ -62,7 +67,7 @@ TEST_F(PlayerFixture, emitStateChanged) { TEST_F(PlayerFixture, stop) { QSignalSpy spy(&dut, SIGNAL(playback_state_changed(QMediaPlayer::State))); ASSERT_TRUE(spy.isValid()); - dut.set_media(podcast); + dut.set_media(local_audio); dut.play(); spy.wait(500); ASSERT_EQ(dut.playback_state(), QMediaPlayer::PlayingState); @@ -76,7 +81,7 @@ TEST_F(PlayerFixture, setMuted) { QSignalSpy spy(&dut, SIGNAL(muted_changed(bool))); ASSERT_TRUE(spy.isValid()); - dut.set_media(podcast); + dut.set_media(local_audio); dut.play(); dut.set_muted(true); dut.pause(); @@ -124,7 +129,7 @@ TEST_F(PlayerFixture, incrementVolume) { TEST_F(PlayerFixture, checkSeekable) { QSignalSpy seekspy(&dut, SIGNAL(seekable_changed(bool))); ASSERT_TRUE(seekspy.isValid()); - dut.set_media(podcast); + dut.set_media(local_audio); dut.play(); seekspy.wait(1000); @@ -140,26 +145,11 @@ TEST_F(PlayerFixture, checkSeekable) { ASSERT_GE(dut.get_position(),500); // at least what we seeked } -/*****************************************************************************/ -TEST_F(PlayerFixture, setPositionForward) { - dut.set_media(podcast); - dut.play(); - - QSignalSpy spy(&dut, SIGNAL(position_changed(qint64))); - ASSERT_TRUE(spy.isValid()); - auto pos = dut.get_position(); - dut.set_position(pos+500); - spy.wait(1000); - spy.wait(1000); - ASSERT_GE(spy.count(), 2); // some times position changed was emitted - ASSERT_GE(dut.get_position(),500); // less than 50ms delta -} - /*****************************************************************************/ TEST_F(PlayerFixture, getDuration) { ASSERT_EQ(dut.error(), QMediaPlayer::NoError); ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); - dut.set_media(podcast); + dut.set_media(local_audio); // we have to wait until media is loaded QSignalSpy spy( &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); @@ -183,7 +173,7 @@ TEST_F(PlayerFixture, getDuration) { /*****************************************************************************/ TEST_F(PlayerFixture, getStatus) { ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); - dut.set_media(podcast); + dut.set_media(local_audio); // we have to wait until media is loaded QSignalSpy spy( &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); @@ -226,6 +216,31 @@ TEST_F(PlayerFixture, checkErrorStates) { EXPECT_EQ(dut.error(), QMediaPlayer::NoMedia); } /*****************************************************************************/ +TEST_F(PlayerFixture, setPositionRemote) { + remote_audio->set_position(10000); + QSignalSpy spy( + &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); + QSignalSpy spy_playing( + &dut, SIGNAL(playback_state_changed(QMediaPlayer::State))); + ASSERT_TRUE(spy.isValid()); + ASSERT_TRUE(spy_playing.isValid()); - - + dut.set_media(remote_audio); + ASSERT_FALSE(remote_audio->is_seekable()); // not seekable + spy.wait(500); + ASSERT_FALSE(spy.isEmpty()); + ASSERT_EQ(spy.takeFirst().at(0).toInt(), QMediaPlayer::LoadingMedia); + spy.wait(500); + ASSERT_FALSE(spy.isEmpty()); + ASSERT_EQ(spy.takeFirst().at(0).toInt(), QMediaPlayer::LoadedMedia); + dut.play(); + spy_playing.wait(200); + ASSERT_EQ( + spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::PlayingState); + ASSERT_TRUE(remote_audio->is_seekable()); // playing, should be seekable + dut.stop(); + spy_playing.wait(200); + ASSERT_EQ(spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::StoppedState); + EXPECT_GE(remote_audio->get_position(), 10000); +} +/*****************************************************************************/ From 2f52d9d381ae511bb53e5245b02452351703a75f Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 22 Dec 2019 16:31:43 +0100 Subject: [PATCH 06/26] Use Github as media source other remote media is more likely to stall --- test/test_mediaplayerproxy.cpp | 141 +++++++++++++++++---------------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/test/test_mediaplayerproxy.cpp b/test/test_mediaplayerproxy.cpp index e02c071..eae7585 100644 --- a/test/test_mediaplayerproxy.cpp +++ b/test/test_mediaplayerproxy.cpp @@ -33,8 +33,8 @@ class PlayerFixture : public virtual ::testing::Test { QUrl::fromLocalFile(TEST_FILE_PATH + "/testaudio.mp3"))) , remote_audio( std::make_shared("Remote", - QUrl("http://www.ruschival.de/wp-content/uploads/2019/12/" - "sample-128k.mp3"))) { + QUrl("https://github.com/truschival/DigitalRoosterGui/raw/" + "develop/test/testaudio.mp3"))) { } protected: @@ -105,11 +105,11 @@ TEST_F(PlayerFixture, setVolume) { TEST_F(PlayerFixture, setVolumeInvalid) { QSignalSpy spy(&dut, SIGNAL(volume_changed(int))); ASSERT_TRUE(spy.isValid()); - + dut.set_volume(-1); // invalid dut.set_volume(100); - ASSERT_EQ(spy.count(), 1); // only 1 set calls is valid and should emit + ASSERT_EQ(spy.count(), 1); // only 1 set calls is valid and should emit ASSERT_EQ(dut.get_volume(), 100); } @@ -127,13 +127,13 @@ TEST_F(PlayerFixture, incrementVolume) { /*****************************************************************************/ TEST_F(PlayerFixture, checkSeekable) { - QSignalSpy seekspy(&dut, SIGNAL(seekable_changed(bool))); - ASSERT_TRUE(seekspy.isValid()); + QSignalSpy seekspy(&dut, SIGNAL(seekable_changed(bool))); + ASSERT_TRUE(seekspy.isValid()); dut.set_media(local_audio); dut.play(); seekspy.wait(1000); - ASSERT_EQ(seekspy.count(),1); + ASSERT_EQ(seekspy.count(), 1); ASSERT_TRUE(dut.seekable()); QSignalSpy spy(&dut, SIGNAL(position_changed(qint64))); @@ -142,78 +142,77 @@ TEST_F(PlayerFixture, checkSeekable) { spy.wait(1000); spy.wait(1000); ASSERT_GE(spy.count(), 2); // some times position changed was emitted - ASSERT_GE(dut.get_position(),500); // at least what we seeked + ASSERT_GE(dut.get_position(), 500); // at least what we seeked } /*****************************************************************************/ TEST_F(PlayerFixture, getDuration) { - ASSERT_EQ(dut.error(), QMediaPlayer::NoError); - ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); - dut.set_media(local_audio); - // we have to wait until media is loaded - QSignalSpy spy( - &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); - ASSERT_TRUE(spy.isValid()); - spy.wait(500); - EXPECT_EQ(spy.count(), 1); - auto signal_params = spy.takeFirst(); - EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::LoadedMedia); - - dut.play(); - spy.wait(500); - EXPECT_EQ(spy.count(), 1); - signal_params = spy.takeFirst(); - EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::BufferedMedia); - - auto duration = dut.get_duration(); - // Rounded to seconds - it behaves differently on windows - ASSERT_EQ(duration/1000, 202617/1000); + ASSERT_EQ(dut.error(), QMediaPlayer::NoError); + ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); + dut.set_media(local_audio); + // we have to wait until media is loaded + QSignalSpy spy( + &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); + ASSERT_TRUE(spy.isValid()); + spy.wait(500); + EXPECT_EQ(spy.count(), 1); + auto signal_params = spy.takeFirst(); + EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::LoadedMedia); + + dut.play(); + spy.wait(500); + EXPECT_EQ(spy.count(), 1); + signal_params = spy.takeFirst(); + EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::BufferedMedia); + + auto duration = dut.get_duration(); + // Rounded to seconds - it behaves differently on windows + ASSERT_EQ(duration / 1000, 202617 / 1000); } /*****************************************************************************/ TEST_F(PlayerFixture, getStatus) { - ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); - dut.set_media(local_audio); - // we have to wait until media is loaded - QSignalSpy spy( - &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); - ASSERT_TRUE(spy.isValid()); - spy.wait(500); - EXPECT_EQ(spy.count(), 1); - auto signal_params = spy.takeFirst(); - EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::LoadedMedia); - EXPECT_EQ(dut.media_status(), QMediaPlayer::LoadedMedia); + ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); + dut.set_media(local_audio); + // we have to wait until media is loaded + QSignalSpy spy( + &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); + ASSERT_TRUE(spy.isValid()); + spy.wait(500); + EXPECT_EQ(spy.count(), 1); + auto signal_params = spy.takeFirst(); + EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::LoadedMedia); + EXPECT_EQ(dut.media_status(), QMediaPlayer::LoadedMedia); } /*****************************************************************************/ TEST_F(PlayerFixture, checkErrorStates) { - ASSERT_EQ(dut.error(), QMediaPlayer::NoError); - ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); - // No such file - auto media = std::make_shared("WebsiteAsEpisode", - QUrl::fromLocalFile(TEST_FILE_PATH + "/testaudio.mp4")); - dut.set_media(media); - // monitor for errors - QSignalSpy error_spy(&dut, SIGNAL(error(QMediaPlayer::Error))); - ASSERT_TRUE(error_spy.isValid()); - - // we have to wait until media is loaded - QSignalSpy media_status_spy( - &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); - ASSERT_TRUE(media_status_spy.isValid()); - media_status_spy.wait(500); - ASSERT_EQ(media_status_spy.count(), 1); - auto media_status_events = media_status_spy.takeFirst(); - ASSERT_EQ( - media_status_events.at(0).toInt(), QMediaPlayer::InvalidMedia); - - dut.play(); - error_spy.wait(500); - - ASSERT_GE(error_spy.count(), 1); // one or more errors - auto signal_params = error_spy.takeFirst(); - EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::NoMedia); - EXPECT_EQ(dut.error(), QMediaPlayer::NoMedia); + ASSERT_EQ(dut.error(), QMediaPlayer::NoError); + ASSERT_EQ(dut.media_status(), QMediaPlayer::NoMedia); + // No such file + auto media = std::make_shared("WebsiteAsEpisode", + QUrl::fromLocalFile(TEST_FILE_PATH + "/testaudio.mp4")); + dut.set_media(media); + // monitor for errors + QSignalSpy error_spy(&dut, SIGNAL(error(QMediaPlayer::Error))); + ASSERT_TRUE(error_spy.isValid()); + + // we have to wait until media is loaded + QSignalSpy media_status_spy( + &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); + ASSERT_TRUE(media_status_spy.isValid()); + media_status_spy.wait(500); + ASSERT_EQ(media_status_spy.count(), 1); + auto media_status_events = media_status_spy.takeFirst(); + ASSERT_EQ(media_status_events.at(0).toInt(), QMediaPlayer::InvalidMedia); + + dut.play(); + error_spy.wait(500); + + ASSERT_GE(error_spy.count(), 1); // one or more errors + auto signal_params = error_spy.takeFirst(); + EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::NoMedia); + EXPECT_EQ(dut.error(), QMediaPlayer::NoMedia); } /*****************************************************************************/ TEST_F(PlayerFixture, setPositionRemote) { @@ -222,7 +221,9 @@ TEST_F(PlayerFixture, setPositionRemote) { &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); QSignalSpy spy_playing( &dut, SIGNAL(playback_state_changed(QMediaPlayer::State))); + QSignalSpy spy_seekable(&dut, SIGNAL(seekable_changed(bool))); ASSERT_TRUE(spy.isValid()); + ASSERT_TRUE(spy_seekable.isValid()); ASSERT_TRUE(spy_playing.isValid()); dut.set_media(remote_audio); @@ -237,10 +238,12 @@ TEST_F(PlayerFixture, setPositionRemote) { spy_playing.wait(200); ASSERT_EQ( spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::PlayingState); + spy_seekable.wait(100); + spy_seekable.wait(400); + ASSERT_GE(spy_seekable.count(), 1); ASSERT_TRUE(remote_audio->is_seekable()); // playing, should be seekable dut.stop(); - spy_playing.wait(200); - ASSERT_EQ(spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::StoppedState); + spy_playing.wait(100); EXPECT_GE(remote_audio->get_position(), 10000); } /*****************************************************************************/ From 6d815d03715455cf307361fb4c395cb838edf048 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Mon, 23 Dec 2019 13:47:10 +0100 Subject: [PATCH 07/26] Do not update position in stopped state QMediaPlayer resets postion to 0 immediately after calling stop() We would lose the saved postion in this case. --- include/PlayableItem.hpp | 13 +- include/mediaplayerproxy.hpp | 17 ++ libsrc/mediaplayer.cpp | 4 +- libsrc/mediaplayerproxy.cpp | 355 ++++++++++++++++----------------- libsrc/playableitem.cpp | 8 + test/test_mediaplayerproxy.cpp | 25 ++- test/test_playableitem.cpp | 2 - 7 files changed, 218 insertions(+), 206 deletions(-) diff --git a/include/PlayableItem.hpp b/include/PlayableItem.hpp index 2ecce1e..e0d7568 100644 --- a/include/PlayableItem.hpp +++ b/include/PlayableItem.hpp @@ -87,13 +87,12 @@ class PlayableItem : public QObject { virtual void set_position(qint64 newVal); /** - * Mark as seekable: position can be updated + * Mark as seekable: position can arbitrarily set + * property of media, assinged by QMediaPlayer */ - void set_seekable(bool seek){ - seekable = seek; - } - bool is_seekable() const{ - return seekable; + void set_seekable(bool seek); + bool is_seekable() const { + return seekable; }; /** @@ -161,7 +160,7 @@ class PlayableItem : public QObject { /** Current position in stream */ qint64 position = 0; - /** Can ressource positon be set ?*/ + /** Media itself is seekable (not a stream)*/ bool seekable{false}; protected: diff --git a/include/mediaplayerproxy.hpp b/include/mediaplayerproxy.hpp index 5038e52..da7e45c 100644 --- a/include/mediaplayerproxy.hpp +++ b/include/mediaplayerproxy.hpp @@ -35,6 +35,17 @@ class MediaPlayerProxy : public MediaPlayer { MediaPlayerProxy(const MediaPlayerProxy& rhs) = delete; MediaPlayerProxy& operator=(const MediaPlayerProxy& rhs) = delete; + /** + * Enable/Disable updates of position + * QMediaPlayer stop for instance resets the media position to 0 + * before anything else in which case we lose the previous position. + * @param enable + */ + void enable_position_update(bool enable); + bool is_position_updateable() const { + return position_updateable; + } + private: virtual bool is_seekable() const override; virtual bool is_muted() const; @@ -56,6 +67,12 @@ class MediaPlayerProxy : public MediaPlayer { virtual void do_play() override; virtual void do_stop() override; + /** + * Position should be updated periodically + * fixes the unwanted behavior that QMediaPlayer emits a postionChanged() to 0 + * before anything else when stop is called + */ + bool position_updateable{false}; /** * Linear volume 0..100% * Initialized because increment/decrement has to work with some value diff --git a/libsrc/mediaplayer.cpp b/libsrc/mediaplayer.cpp index 377775e..08e85e0 100644 --- a/libsrc/mediaplayer.cpp +++ b/libsrc/mediaplayer.cpp @@ -17,8 +17,8 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayer") - /*****************************************************************************/ - bool MediaPlayer::muted() const { +/*****************************************************************************/ +bool MediaPlayer::muted() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; return is_muted(); } diff --git a/libsrc/mediaplayerproxy.cpp b/libsrc/mediaplayerproxy.cpp index 95abb32..a514b8f 100644 --- a/libsrc/mediaplayerproxy.cpp +++ b/libsrc/mediaplayerproxy.cpp @@ -20,261 +20,242 @@ #include using namespace DigitalRooster; -static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy") -; +static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy"); /***********************************************************************/ -MediaPlayerProxy::MediaPlayerProxy() : - backend(std::make_unique()) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - QObject::connect(backend.get(), &QMediaPlayer::mediaChanged, - [=](const QMediaContent &media) { - qCDebug(CLASS_LC) - << "MediaPlayerProxy media_changed()"; - /* - * We know nothing of new media, assume it is not seekable - * otherwise we might overwrite/lose saved position - */ - current_item->set_seekable(false); - emit media_changed(media); - }); - - QObject::connect(backend.get(), &QMediaPlayer::positionChanged, - [=](qint64 position) { - qCDebug(CLASS_LC) - << "MediaPlayerProxy position_changed()" << position; - emit position_changed(position); - /** Only update position of seekable media */ - if (current_item.get() != nullptr - && current_item->is_seekable()) { - current_item->set_position(position); - } - }); - - QObject::connect(backend.get(), - static_cast(&QMediaPlayer::error), - [=](QMediaPlayer::Error err) { - qCWarning(CLASS_LC) - << "MediaPlayerProxy Error" << err; - emit error(err); - }); - - QObject::connect(backend.get(), &QMediaPlayer::mutedChanged, - [=](bool muted) { - emit muted_changed(muted); - }); - - QObject::connect(backend.get(), &QMediaPlayer::stateChanged, - [=](QMediaPlayer::State state) { - qCDebug(CLASS_LC) - << "MediaPlayerProxy playback_state_changed()" << state; - - emit playback_state_changed(state); - }); - - QObject::connect(backend.get(), &QMediaPlayer::durationChanged, - [=](qint64 duration) { - emit duration_changed(duration); - }); - - QObject::connect(backend.get(), &QMediaPlayer::seekableChanged, - [=](bool seekable) { - qCDebug(CLASS_LC) - << "MediaPlayerProxy seekable_changed()" << seekable; - current_item->set_seekable(seekable); - emit seekable_changed(seekable); - /* jump to previously saved position once we know we can seek */ - if(current_item->is_seekable()){ - /* update backend position */ - set_position(current_item->get_position()); - } - }); - - QObject::connect(backend.get(), &QMediaPlayer::mediaStatusChanged, - [=](QMediaPlayer::MediaStatus status) { - qCDebug(CLASS_LC) - << "MediaPlayerProxy media_status_changed()" << status; - emit media_status_changed(status); - }); - - QObject::connect(backend.get(), &QMediaPlayer::metaDataAvailableChanged, - [=](bool available) { - qCDebug(CLASS_LC) - << " metaDataAvailableChanged " << available; - if (available) { - QString title = - backend->metaData(QMediaMetaData::Title).toString(); - QString publisher = backend->metaData( - QMediaMetaData::Publisher).toString(); - - qCDebug(CLASS_LC) - << "\n\tTitle:" << title << "\n\tPublisher:" << publisher - << "\n\tPublisher:" << publisher - << "\n\tAlbumArtist:" - << backend->metaData(QMediaMetaData::AlbumArtist).toString() - << "\n\tAuthor:" - << backend->metaData(QMediaMetaData::Author).toString() - << "\n\tDescription:" - << backend->metaData(QMediaMetaData::Description).toString(); - - if (title != "") { - current_item->set_title(title); - } - if (publisher != "") { - current_item->set_publisher(publisher); - } - } else { - qCDebug(CLASS_LC) - << "No metadata."; - } - }); +MediaPlayerProxy::MediaPlayerProxy() + : backend(std::make_unique()) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QObject::connect(backend.get(), &QMediaPlayer::mediaChanged, + [=](const QMediaContent& media) { + qCDebug(CLASS_LC) << "MediaPlayerProxy media_changed()"; + /* + * We know nothing of new media, assume it is not seekable + * otherwise we might overwrite/lose saved position + */ + current_item->set_seekable(false); + emit media_changed(media); + }); + + QObject::connect( + backend.get(), &QMediaPlayer::positionChanged, [=](qint64 position) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy position_changed()" << position; + emit position_changed(position); + + /** Only update position */ + if (current_item.get() != nullptr && position_updateable) { + current_item->set_position(position); + } + }); + + QObject::connect(backend.get(), + static_cast( + &QMediaPlayer::error), + [=](QMediaPlayer::Error err) { + qCWarning(CLASS_LC) << "MediaPlayerProxy Error" << err; + emit error(err); + }); + + QObject::connect(backend.get(), &QMediaPlayer::mutedChanged, + [=](bool muted) { emit muted_changed(muted); }); + + QObject::connect(backend.get(), &QMediaPlayer::stateChanged, + [=](QMediaPlayer::State state) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy playback_state_changed()" << state; + + emit playback_state_changed(state); + }); + + QObject::connect(backend.get(), &QMediaPlayer::durationChanged, + [=](qint64 duration) { emit duration_changed(duration); }); + + QObject::connect( + backend.get(), &QMediaPlayer::seekableChanged, [=](bool seekable) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy seekable_changed()" << seekable; + current_item->set_seekable(seekable); + enable_position_update(seekable); + emit seekable_changed(seekable); + + /* jump to previously saved position once we know we can seek */ + if (seekable) { + /* update backend position */ + set_position(current_item->get_position()); + } + }); + + QObject::connect(backend.get(), &QMediaPlayer::mediaStatusChanged, + [=](QMediaPlayer::MediaStatus status) { + qCDebug(CLASS_LC) + << "MediaPlayerProxy media_status_changed()" << status; + emit media_status_changed(status); + }); + + QObject::connect(backend.get(), &QMediaPlayer::metaDataAvailableChanged, + [=](bool available) { + qCDebug(CLASS_LC) << " metaDataAvailableChanged " << available; + if (available) { + QString title = + backend->metaData(QMediaMetaData::Title).toString(); + QString publisher = + backend->metaData(QMediaMetaData::Publisher).toString(); + + qCDebug(CLASS_LC) + << "\n\tTitle:" << title << "\n\tPublisher:" << publisher + << "\n\tPublisher:" << publisher << "\n\tAlbumArtist:" + << backend->metaData(QMediaMetaData::AlbumArtist).toString() + << "\n\tAuthor:" + << backend->metaData(QMediaMetaData::Author).toString() + << "\n\tDescription:" + << backend->metaData(QMediaMetaData::Description) + .toString(); + + if (title != "") { + current_item->set_title(title); + } + if (publisher != "") { + current_item->set_publisher(publisher); + } + } else { + qCDebug(CLASS_LC) << "No metadata."; + } + }); } /*****************************************************************************/ void MediaPlayerProxy::do_seek(qint64 incr) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - if (seekable()) - set_position(get_position() + incr); -} -; + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (seekable()) + set_position(get_position() + incr); +}; /*****************************************************************************/ bool MediaPlayerProxy::is_seekable() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->isSeekable(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->isSeekable(); } /*****************************************************************************/ QMediaPlayer::Error MediaPlayerProxy::do_error() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->error(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->error(); } /*****************************************************************************/ qint64 MediaPlayerProxy::do_get_position() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->position(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->position(); } /*****************************************************************************/ QMediaPlayer::MediaStatus MediaPlayerProxy::do_media_status() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->mediaStatus(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->mediaStatus(); } /*****************************************************************************/ qint64 MediaPlayerProxy::do_get_duration() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->duration(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->duration(); } /*****************************************************************************/ void MediaPlayerProxy::do_set_position(qint64 position) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO << position; - backend->setPosition(position); + qCDebug(CLASS_LC) << Q_FUNC_INFO << position; + backend->setPosition(position); } + +/*****************************************************************************/ +void MediaPlayerProxy::enable_position_update(bool enable) { + qCDebug(CLASS_LC) << Q_FUNC_INFO << enable; + position_updateable = enable; +} + /*****************************************************************************/ bool MediaPlayerProxy::is_muted() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->isMuted(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->isMuted(); } /*****************************************************************************/ int MediaPlayerProxy::do_get_volume() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return linear_volume; + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return linear_volume; } /*****************************************************************************/ void MediaPlayerProxy::do_set_media( - std::shared_ptr media) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO << media->get_display_name(); - current_item = media; - auto previous_position = media->get_position(); - - backend->setMedia(QMediaContent(media->get_url())); - if (previous_position != 0) { - qCDebug(CLASS_LC) - << "restarting from position" << previous_position; - set_position(previous_position); - } + std::shared_ptr media) { + qCDebug(CLASS_LC) << Q_FUNC_INFO << media->get_display_name(); + current_item = media; + auto previous_position = media->get_position(); + + backend->setMedia(QMediaContent(media->get_url())); + if (previous_position != 0) { + qCDebug(CLASS_LC) << "restarting from position" << previous_position; + set_position(previous_position); + } } /*****************************************************************************/ -void MediaPlayerProxy::do_set_playlist(QMediaPlaylist *playlist) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - backend->setPlaylist(playlist); +void MediaPlayerProxy::do_set_playlist(QMediaPlaylist* playlist) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + backend->setPlaylist(playlist); } /*****************************************************************************/ void MediaPlayerProxy::do_set_muted(bool muted) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - backend->setMuted(muted); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + backend->setMuted(muted); } /*****************************************************************************/ QMediaPlayer::State MediaPlayerProxy::do_playback_state() const { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - return backend->state(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return backend->state(); } /*****************************************************************************/ void MediaPlayerProxy::do_set_volume(int volume) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO << volume; - if (volume < 0 || volume > 100) { - qCWarning(CLASS_LC) - << "invalid volume (must be 0..100%)"; - return; - } - linear_volume = volume; - emit volume_changed(volume); - - /* backend works with logarithmic volume */ - auto adjusted_volume = QAudio::convertVolume(volume / qreal(100.0), - QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale); - backend->setVolume(qRound(adjusted_volume * 100.0)); + qCDebug(CLASS_LC) << Q_FUNC_INFO << volume; + if (volume < 0 || volume > 100) { + qCWarning(CLASS_LC) << "invalid volume (must be 0..100%)"; + return; + } + linear_volume = volume; + emit volume_changed(volume); + + /* backend works with logarithmic volume */ + auto adjusted_volume = QAudio::convertVolume(volume / qreal(100.0), + QAudio::LogarithmicVolumeScale, QAudio::LinearVolumeScale); + backend->setVolume(qRound(adjusted_volume * 100.0)); } /*****************************************************************************/ void MediaPlayerProxy::do_increment_volume(int increment) { - qCDebug(CLASS_LC) - << Q_FUNC_INFO << increment; - qCDebug(CLASS_LC) - << " current volume" << linear_volume; - set_volume(linear_volume + increment); + qCDebug(CLASS_LC) << Q_FUNC_INFO << increment; + qCDebug(CLASS_LC) << " current volume" << linear_volume; + set_volume(linear_volume + increment); } /*****************************************************************************/ void MediaPlayerProxy::do_pause() { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - backend->pause(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + backend->pause(); } /*****************************************************************************/ void MediaPlayerProxy::do_play() { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - backend->play(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + backend->play(); } /*****************************************************************************/ void MediaPlayerProxy::do_stop() { - qCDebug(CLASS_LC) - << Q_FUNC_INFO; - backend->stop(); + qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* make sure we don't reset current position to 0 on stop + * as QMediaPlayer usually does */ + enable_position_update(false); + backend->stop(); } /*****************************************************************************/ diff --git a/libsrc/playableitem.cpp b/libsrc/playableitem.cpp index a98ce83..3ccdf60 100644 --- a/libsrc/playableitem.cpp +++ b/libsrc/playableitem.cpp @@ -80,6 +80,14 @@ QString PlayableItem::do_get_display_name() const { return display_name; } +/***********************************************************************/ +void PlayableItem::set_seekable(bool seek) { + qCDebug(CLASS_LC) << Q_FUNC_INFO << seek; + seekable = seek; +} + + + /***********************************************************************/ QString PodcastEpisode::do_get_display_name() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; diff --git a/test/test_mediaplayerproxy.cpp b/test/test_mediaplayerproxy.cpp index eae7585..ef6611b 100644 --- a/test/test_mediaplayerproxy.cpp +++ b/test/test_mediaplayerproxy.cpp @@ -217,23 +217,25 @@ TEST_F(PlayerFixture, checkErrorStates) { /*****************************************************************************/ TEST_F(PlayerFixture, setPositionRemote) { remote_audio->set_position(10000); - QSignalSpy spy( + QSignalSpy spy_status( &dut, SIGNAL(media_status_changed(QMediaPlayer::MediaStatus))); QSignalSpy spy_playing( &dut, SIGNAL(playback_state_changed(QMediaPlayer::State))); QSignalSpy spy_seekable(&dut, SIGNAL(seekable_changed(bool))); - ASSERT_TRUE(spy.isValid()); + ASSERT_TRUE(spy_status.isValid()); ASSERT_TRUE(spy_seekable.isValid()); ASSERT_TRUE(spy_playing.isValid()); dut.set_media(remote_audio); ASSERT_FALSE(remote_audio->is_seekable()); // not seekable - spy.wait(500); - ASSERT_FALSE(spy.isEmpty()); - ASSERT_EQ(spy.takeFirst().at(0).toInt(), QMediaPlayer::LoadingMedia); - spy.wait(500); - ASSERT_FALSE(spy.isEmpty()); - ASSERT_EQ(spy.takeFirst().at(0).toInt(), QMediaPlayer::LoadedMedia); + /* normal media-loading cycle: loading->loaded */ + spy_status.wait(600); + ASSERT_FALSE(spy_status.isEmpty()); + ASSERT_EQ(spy_status.takeFirst().at(0).toInt(), QMediaPlayer::LoadingMedia); + spy_status.wait(1000); + ASSERT_FALSE(spy_status.isEmpty()); + ASSERT_EQ(spy_status.takeFirst().at(0).toInt(), QMediaPlayer::LoadedMedia); + /* now start playing */ dut.play(); spy_playing.wait(200); ASSERT_EQ( @@ -242,8 +244,15 @@ TEST_F(PlayerFixture, setPositionRemote) { spy_seekable.wait(400); ASSERT_GE(spy_seekable.count(), 1); ASSERT_TRUE(remote_audio->is_seekable()); // playing, should be seekable + dut.pause(); + spy_playing.wait(100); + /* Media position should not have been reset to < 10000 when media was not + * available */ + EXPECT_GE(remote_audio->get_position(), 10000); dut.stop(); spy_playing.wait(100); + /* Media position should not have been reset to < 10000 when media was not + * available */ EXPECT_GE(remote_audio->get_position(), 10000); } /*****************************************************************************/ diff --git a/test/test_playableitem.cpp b/test/test_playableitem.cpp index b1f654b..302fc15 100644 --- a/test/test_playableitem.cpp +++ b/test/test_playableitem.cpp @@ -86,5 +86,3 @@ TEST(PlayableItem, ListenedChanged) { ASSERT_EQ(spy.count(),1); ASSERT_TRUE(episode.already_listened()); } - - From 2c4d28fd8e171a607a39c5da7ca6197a5c6903e2 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Mon, 23 Dec 2019 14:17:01 +0100 Subject: [PATCH 08/26] Github-actions: upload test-debug-log to artifacts (#17) Allows diagnosis when Tests fail on github actions --- .github/workflows/build_and_test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 98149a5..eafc46b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -5,8 +5,6 @@ on: - develop - master - 'features/**' - branches_ignore: - - 'test*' pull_request: branches: - develop @@ -53,6 +51,12 @@ jobs: run: docker exec build_container cmake --build $BUILD_DIR --parallel - name: Run tests run: docker exec -w $BUILD_DIR build_container bin/DigitalRooster_gtest + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v1 + with: + name: tests.log + path: /tmp/build/Digitalrooster_tests.log - name: Collect coverage run: > docker exec -w $BUILD_DIR build_container From c261edd7c02d5f38c670213f2915468cd18de3db Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sat, 7 Dec 2019 09:54:51 +0100 Subject: [PATCH 09/26] Add methods to add/delete objects form configuration_manager Add/find/delete Alarms, PodcastSources and radio stations form Configuration Manger --- include/configuration_manager.hpp | 71 +++++++++++-- libsrc/configuration_manager.cpp | 99 +++++++++++++++++- qtgui/alarmlistmodel.cpp | 10 +- test/test_settings.cpp | 160 ++++++++++++++++++++++++------ 4 files changed, 296 insertions(+), 44 deletions(-) diff --git a/include/configuration_manager.hpp b/include/configuration_manager.hpp index 6a8f489..70174b1 100644 --- a/include/configuration_manager.hpp +++ b/include/configuration_manager.hpp @@ -69,9 +69,7 @@ class ConfigurationManager : public QObject { * @param configpath path to application configuration * @param cachedir directory to cache data (podcastlist etc) */ - ConfigurationManager( - const QString& configpath, - const QString& cachedir); + ConfigurationManager(const QString& configpath, const QString& cachedir); virtual ~ConfigurationManager() = default; @@ -123,6 +121,14 @@ class ConfigurationManager : public QObject { return get_iradio_list(); } + /** + * Get a internet radio station identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of podcast + * @return station + */ + const PlayableItem* get_stream_source(const QUuid& id) const; + /** * get all podcast sources */ @@ -138,6 +144,14 @@ class ConfigurationManager : public QObject { */ PodcastSource* get_podcast_source_by_index(int index) const; + /** + * Get a single podcast source identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of podcast + * @return source + */ + const PodcastSource* get_podcast_source(const QUuid& id) const; + /** * Removes a podcast source entry form list * @throws std::out_of_range if not found @@ -152,6 +166,14 @@ class ConfigurationManager : public QObject { return get_alarm_list(); } + /** + * Get a alarm identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of podcast + * @return station + */ + const Alarm* get_alarm(const QUuid& id) const; + /** * Weather configuration object */ @@ -195,8 +217,8 @@ class ConfigurationManager : public QObject { * Where to store cache files * @return application_cache_dir.dirName() */ - QString get_cache_path(){ - return get_cache_dir_name(); + QString get_cache_path() { + return get_cache_dir_name(); }; /** @@ -205,6 +227,12 @@ class ConfigurationManager : public QObject { */ void add_radio_station(std::shared_ptr src); + /** + * Append new PodcastSource to list + * @param podcast source + */ + void add_podcast_source(std::shared_ptr podcast); + /** * Append new alarm to list * @param alarm @@ -214,9 +242,23 @@ class ConfigurationManager : public QObject { /** * Delete an alarm identified by ID from the list of alarms * @param id of alarm - * @return 0 if alarm was deleted, -1 otherwise + * @throws std::out_of_range if not found */ - int delete_alarm(const QUuid& id); + void delete_alarm(const QUuid& id); + + /** + * Delete a internet radio station identified by id form the list + * @param id unique id of radio station + * @throws std::out_of_range if not found + */ + void delete_radio_station(const QUuid& id); + + /** + * Delete a podcast source identified by id form the list of sources + * @param id unique id of podcast source + * @throws std::out_of_range if not found + */ + void delete_podcast_source(const QUuid& id); public slots: /** @@ -260,7 +302,22 @@ public slots: } signals: + /** + * Any configuration item changed + */ void configuration_changed(); + /** + * podcast list was changed (added/deleted items) + */ + void podcast_sources_changed(); + /** + * alarm list was changed (added/deleted items) + */ + void alarms_changed(); + /** + * radio list was changed (added/deleted items) + */ + void stations_changed(); private: /** diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index a493d28..e94264f 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -66,8 +66,9 @@ ConfigurationManager::ConfigurationManager( */ if (!is_writable_directory(cachedir) && !create_writable_directory(cachedir)) { - qCWarning(CLASS_LC) << "Failed using " << get_cache_dir_name() - << "as cache using default:" << DEFAULT_CACHE_DIR_PATH; + qCWarning(CLASS_LC) + << "Failed using " << get_cache_dir_name() + << "as cache using default:" << DEFAULT_CACHE_DIR_PATH; application_cache_dir.setPath(DEFAULT_CACHE_DIR_PATH); QDir().mkpath(DEFAULT_CACHE_DIR_PATH); } @@ -309,12 +310,66 @@ void ConfigurationManager::add_radio_station( std::shared_ptr src) { qCDebug(CLASS_LC) << Q_FUNC_INFO; this->stream_sources.push_back(src); + dataChanged(); + emit stations_changed(); +} + +/*****************************************************************************/ +const PlayableItem* ConfigurationManager::get_stream_source( + const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto item = std::find_if(stream_sources.begin(), stream_sources.end(), + [&](const std::shared_ptr item) { + return item->get_id() == id; + }); + if (item == stream_sources.end()) { + throw std::out_of_range(""); + } + return item->get(); +} + +/*****************************************************************************/ +void ConfigurationManager::add_podcast_source( + std::shared_ptr src) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + this->podcast_sources.push_back(src); + dataChanged(); + emit podcast_sources_changed(); +} + +/*****************************************************************************/ +const PodcastSource* ConfigurationManager::get_podcast_source( + const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto item = std::find_if(podcast_sources.begin(), podcast_sources.end(), + [&](const std::shared_ptr item) { + return item->get_id() == id; + }); + if (item == podcast_sources.end()) { + throw std::out_of_range(""); + } + return item->get(); } + /*****************************************************************************/ void ConfigurationManager::add_alarm(std::shared_ptr alm) { qCDebug(CLASS_LC) << Q_FUNC_INFO; this->alarms.push_back(alm); dataChanged(); + emit alarms_changed(); +} + +/*****************************************************************************/ +const Alarm* ConfigurationManager::get_alarm(const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto item = std::find_if( + alarms.begin(), alarms.end(), [&](const std::shared_ptr item) { + return item->get_id() == id; + }); + if (item == alarms.end()) { + throw std::out_of_range(""); + } + return item->get(); } /*****************************************************************************/ @@ -551,7 +606,7 @@ void ConfigurationManager::remove_podcast_source_by_index(int index) { } /*****************************************************************************/ -int ConfigurationManager::delete_alarm(const QUuid& id) { +void ConfigurationManager::delete_alarm(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; auto old_end = alarms.end(); alarms.erase(std::remove_if(alarms.begin(), alarms.end(), @@ -560,10 +615,44 @@ int ConfigurationManager::delete_alarm(const QUuid& id) { }), alarms.end()); if (old_end == alarms.end()) { - return -1; + throw std::out_of_range(""); + } + dataChanged(); + emit alarms_changed(); +}; + +/*****************************************************************************/ +void ConfigurationManager::delete_podcast_source(const QUuid& id) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto old_end = podcast_sources.end(); + podcast_sources.erase( + std::remove_if(podcast_sources.begin(), podcast_sources.end(), + [&](const std::shared_ptr item) { + return item->get_id() == id; + }), + podcast_sources.end()); + if (old_end == podcast_sources.end()) { + throw std::out_of_range(""); + } + dataChanged(); + emit podcast_sources_changed(); +}; + +/*****************************************************************************/ +void ConfigurationManager::delete_radio_station(const QUuid& id) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto old_end = stream_sources.end(); + stream_sources.erase( + std::remove_if(stream_sources.begin(), stream_sources.end(), + [&](const std::shared_ptr item) { + return item->get_id() == id; + }), + stream_sources.end()); + if (old_end == stream_sources.end()) { + throw std::out_of_range(""); } dataChanged(); - return 0; + emit stations_changed(); }; /*****************************************************************************/ diff --git a/qtgui/alarmlistmodel.cpp b/qtgui/alarmlistmodel.cpp index 6cb533e..a37b882 100644 --- a/qtgui/alarmlistmodel.cpp +++ b/qtgui/alarmlistmodel.cpp @@ -132,9 +132,13 @@ bool AlarmListModel::removeRows( int AlarmListModel::delete_alarm(qint64 row) { qCDebug(CLASS_LC) << Q_FUNC_INFO << row; beginRemoveRows(QModelIndex(), row, row); - auto alarm = cm->get_alarms().at(row); - if(alarm){ - cm->delete_alarm(alarm->get_id()); + try { + auto alarm = cm->get_alarms().at(row); + if (alarm) { + cm->delete_alarm(alarm->get_id()); + } + } catch (std::out_of_range& exc) { + qCWarning(CLASS_LC) << Q_FUNC_INFO << " Alarm not found! "; } endRemoveRows(); return 0; diff --git a/test/test_settings.cpp b/test/test_settings.cpp index 3facfd6..9c92b64 100644 --- a/test/test_settings.cpp +++ b/test/test_settings.cpp @@ -28,7 +28,7 @@ using namespace DigitalRooster; class SettingsFixture : public virtual ::testing::Test { public: SettingsFixture() - : filename(DEFAULT_CONFIG_FILE_PATH+"_SettingsFixture") + : filename(DEFAULT_CONFIG_FILE_PATH + "_SettingsFixture") , cache_dir(DEFAULT_CACHE_DIR_PATH) { } @@ -189,6 +189,133 @@ TEST_F(SettingsFixture, addRadioStation_write) { ASSERT_EQ(stream->get_display_name(), QString("foo")); } +/*****************************************************************************/ +TEST_F(SettingsFixture, add_podcast_source) { + QSignalSpy spy(cm.get(), SIGNAL(podcast_sources_changed())); + ASSERT_TRUE(spy.isValid()); + auto ps = std::make_shared( + QUrl("https://alternativlos.org/alternativlos.rss"), + QDir(cm->get_cache_path())); + auto size_before = cm->get_podcast_sources().size(); + cm->add_podcast_source(ps); + ASSERT_EQ(spy.count(), 1); + ASSERT_EQ(cm->get_podcast_sources().size(), size_before+1); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_podcast_source_throws) { + EXPECT_THROW( + cm->get_podcast_source(QUuid::createUuid()), std::out_of_range); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_podcast_source_ok) { + auto& v = cm->get_podcast_sources(); + auto uid = v[0]->get_id(); + auto item = cm->get_podcast_source(uid); + ASSERT_EQ(item,v[0].get()); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_podcast_source) { + auto& v = cm->get_podcast_sources(); + auto size_before=v.size(); + auto uid = v[0]->get_id(); + cm->delete_podcast_source(uid); + ASSERT_EQ(cm->get_podcast_sources().size(), size_before-1); + EXPECT_THROW( + cm->get_podcast_source(uid), std::out_of_range); +} +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_podcast_throws) { + EXPECT_THROW( + cm->delete_podcast_source(QUuid::createUuid()), std::out_of_range); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, add_radio_station) { + QSignalSpy spy(cm.get(), SIGNAL(stations_changed())); + ASSERT_TRUE(spy.isValid()); + auto radio = std::make_shared(); + auto size_before = cm->get_stream_sources().size(); + cm->add_radio_station(radio); + ASSERT_EQ(spy.count(), 1); + ASSERT_EQ(cm->get_stream_sources().size(), size_before+1); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_radio_station_throws) { + EXPECT_THROW( + cm->get_stream_source(QUuid::createUuid()), std::out_of_range); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_radio_station_ok) { + auto& v = cm->get_stream_sources(); + auto uid = v[0]->get_id(); + auto item = cm->get_stream_source(uid); + ASSERT_EQ(item,v[0].get()); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_radio_station) { + auto& v = cm->get_stream_sources(); + auto size_before=v.size(); + auto uid = v[0]->get_id(); + cm->delete_radio_station(uid); + ASSERT_EQ(cm->get_stream_sources().size(), size_before-1); + EXPECT_THROW( + cm->get_stream_source(uid), std::out_of_range); +} +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_radio_throws) { + EXPECT_THROW( + cm->delete_radio_station(QUuid::createUuid()), std::out_of_range); +} + + +/*****************************************************************************/ +TEST_F(SettingsFixture, add_alarm) { + QSignalSpy spy(cm.get(), SIGNAL(alarms_changed())); + ASSERT_TRUE(spy.isValid()); + auto alm = std::make_shared(); + auto size_before = cm->get_alarms().size(); + cm->add_alarm(alm); + ASSERT_EQ(spy.count(), 1); + ASSERT_EQ(cm->get_alarms().size(), size_before+1); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_alarm_throws) { + EXPECT_THROW( + cm->get_alarm(QUuid::createUuid()), std::out_of_range); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, get_alarm_ok) { + auto& v = cm->get_alarms(); + auto uid = v[0]->get_id(); + auto item = cm->get_alarm(uid); + ASSERT_EQ(item,v[0].get()); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_alarm) { + auto& v = cm->get_alarms(); + auto size_before=v.size(); + auto uid = v[0]->get_id(); + cm->delete_alarm(uid); + ASSERT_EQ(cm->get_alarms().size(), size_before-1); + EXPECT_THROW( + cm->get_alarm(uid), std::out_of_range); +} + +/*****************************************************************************/ +TEST_F(SettingsFixture, delete_alarm_throws) { + EXPECT_THROW( + cm->delete_alarm(QUuid::createUuid()), std::out_of_range); +} + /*****************************************************************************/ TEST_F(SettingsFixture, read_2podcasts) { auto& v = cm->get_podcast_sources(); @@ -196,7 +323,7 @@ TEST_F(SettingsFixture, read_2podcasts) { } /*****************************************************************************/ -TEST_F(SettingsFixture, deletePodcastById) { +TEST_F(SettingsFixture, deletePodcastByIndex) { auto& v = cm->get_podcast_sources(); ASSERT_EQ(2, v.size()); cm->remove_podcast_source_by_index(0); @@ -271,31 +398,6 @@ TEST_F(SettingsFixture, streamsourceid) { ASSERT_EQ((*res)->get_url(), QString("http://swr2.de")); } -/*****************************************************************************/ -TEST_F(SettingsFixture, addAlarm) { - auto al = std::make_shared(); - auto size_before = cm->get_alarms().size(); - cm->add_alarm(al); - ASSERT_EQ(cm->get_alarms().size(), size_before + 1); -} - -/*****************************************************************************/ -TEST_F(SettingsFixture, deleteAlarm) { - auto al = std::make_shared(); - auto id = al->get_id(); - auto size_before = cm->get_alarms().size(); - cm->add_alarm(al); - cm->delete_alarm(id); - ASSERT_EQ(cm->get_alarms().size(), size_before); -} - -/*****************************************************************************/ -TEST_F(SettingsFixture, deleteAlarmNonExist) { - auto size_before = cm->get_alarms().size(); - ASSERT_EQ(cm->delete_alarm(QUuid("XXX")), -1); - ASSERT_EQ(cm->get_alarms().size(), size_before); -} - /*****************************************************************************/ TEST_F(SettingsFixture, alarm_daily) { auto& v = cm->get_alarms(); @@ -391,8 +493,8 @@ TEST(ConfigManager, DefaultForNotWritableCache) { TEST(ConfigManager, DefaultForNotWritableConfig) { QFile default_conf_file(DEFAULT_CONFIG_FILE_PATH); ASSERT_TRUE(default_conf_file.remove()); - ConfigurationManager cm(QString("/dev/foobar.json"), - DEFAULT_CACHE_DIR_PATH); + ConfigurationManager cm( + QString("/dev/foobar.json"), DEFAULT_CACHE_DIR_PATH); ASSERT_TRUE(default_conf_file.exists()); } From 00869ccb2fde74577d0da548cb3ee66ea6f8459b Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sat, 7 Dec 2019 10:18:35 +0100 Subject: [PATCH 10/26] refactored common functions to template moved duplicate code in delete_radio_station, delete_podcast_source delete_stream_source to template function --- include/PodcastSource.hpp | 1 + include/httpclient.hpp | 2 +- libsrc/configuration_manager.cpp | 96 ++++++++++++++------------------ 3 files changed, 43 insertions(+), 56 deletions(-) diff --git a/include/PodcastSource.hpp b/include/PodcastSource.hpp index f1c6713..b04e181 100644 --- a/include/PodcastSource.hpp +++ b/include/PodcastSource.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include diff --git a/include/httpclient.hpp b/include/httpclient.hpp index 86b4002..6b4d57a 100644 --- a/include/httpclient.hpp +++ b/include/httpclient.hpp @@ -14,10 +14,10 @@ #ifndef _HTTPCLIENT_HPP_ #define _HTTPCLIENT_HPP_ +#include #include #include #include -#include class QSslError; diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index e94264f..d7a9456 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -47,6 +47,33 @@ bool DigitalRooster::create_writable_directory(const QString& dirname) { return is_writable_directory(dirname); } +/*****************************************************************************/ +template +T* find_by_id(const QVector>& container, const QUuid& id) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto item = std::find_if(container.begin(), container.end(), + [&](const std::shared_ptr item) { return item->get_id() == id; }); + if (item == container.end()) { + throw std::out_of_range(Q_FUNC_INFO); + } + return item->get(); +} + +/*****************************************************************************/ +template +void delete_by_id(QVector>& container, const QUuid& id) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto old_end = container.end(); + container.erase(std::remove_if(container.begin(), container.end(), + [&](const std::shared_ptr item) { + return item->get_id() == id; + }), + container.end()); + if (old_end == container.end()) { + throw std::out_of_range(""); + } +} + /*****************************************************************************/ ConfigurationManager::ConfigurationManager( const QString& configpath, const QString& cachedir) @@ -318,14 +345,8 @@ void ConfigurationManager::add_radio_station( const PlayableItem* ConfigurationManager::get_stream_source( const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto item = std::find_if(stream_sources.begin(), stream_sources.end(), - [&](const std::shared_ptr item) { - return item->get_id() == id; - }); - if (item == stream_sources.end()) { - throw std::out_of_range(""); - } - return item->get(); + /* Find by id throws - just pass it on to the client */ + return find_by_id(stream_sources,id); } /*****************************************************************************/ @@ -341,14 +362,8 @@ void ConfigurationManager::add_podcast_source( const PodcastSource* ConfigurationManager::get_podcast_source( const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto item = std::find_if(podcast_sources.begin(), podcast_sources.end(), - [&](const std::shared_ptr item) { - return item->get_id() == id; - }); - if (item == podcast_sources.end()) { - throw std::out_of_range(""); - } - return item->get(); + /* Find by id throws - just pass it on to the client */ + return find_by_id(podcast_sources,id); } /*****************************************************************************/ @@ -362,14 +377,8 @@ void ConfigurationManager::add_alarm(std::shared_ptr alm) { /*****************************************************************************/ const Alarm* ConfigurationManager::get_alarm(const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto item = std::find_if( - alarms.begin(), alarms.end(), [&](const std::shared_ptr item) { - return item->get_id() == id; - }); - if (item == alarms.end()) { - throw std::out_of_range(""); - } - return item->get(); + /* Find by id throws - just pass it on to the client */ + return find_by_id(alarms,id); } /*****************************************************************************/ @@ -608,15 +617,8 @@ void ConfigurationManager::remove_podcast_source_by_index(int index) { /*****************************************************************************/ void ConfigurationManager::delete_alarm(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto old_end = alarms.end(); - alarms.erase(std::remove_if(alarms.begin(), alarms.end(), - [&](const std::shared_ptr item) { - return item->get_id() == id; - }), - alarms.end()); - if (old_end == alarms.end()) { - throw std::out_of_range(""); - } + /* delete may throw - just pass it on to the client */ + delete_by_id(alarms,id); dataChanged(); emit alarms_changed(); }; @@ -624,35 +626,19 @@ void ConfigurationManager::delete_alarm(const QUuid& id) { /*****************************************************************************/ void ConfigurationManager::delete_podcast_source(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto old_end = podcast_sources.end(); - podcast_sources.erase( - std::remove_if(podcast_sources.begin(), podcast_sources.end(), - [&](const std::shared_ptr item) { - return item->get_id() == id; - }), - podcast_sources.end()); - if (old_end == podcast_sources.end()) { - throw std::out_of_range(""); - } + /* delete may throw - just pass it on to the client */ + delete_by_id(podcast_sources,id); dataChanged(); - emit podcast_sources_changed(); + emit alarms_changed(); }; /*****************************************************************************/ void ConfigurationManager::delete_radio_station(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto old_end = stream_sources.end(); - stream_sources.erase( - std::remove_if(stream_sources.begin(), stream_sources.end(), - [&](const std::shared_ptr item) { - return item->get_id() == id; - }), - stream_sources.end()); - if (old_end == stream_sources.end()) { - throw std::out_of_range(""); - } + /* delete may throw - just pass it on to the client */ + delete_by_id(stream_sources,id); dataChanged(); - emit stations_changed(); + emit alarms_changed(); }; /*****************************************************************************/ From 3f3fc376f2df6f940cc4e8db856fb3a931d29a94 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sat, 7 Dec 2019 13:20:03 +0100 Subject: [PATCH 11/26] Serialization of configuration objects All objects in digitalrooster.json can be serialized / deserialized directly with member functions PodcastSource uses PodcastSerializer to store/restore data as optional dependency. --- CMakeLists.txt | 1 - include/PlayableItem.hpp | 29 +++++ include/PodcastSource.hpp | 78 +++++++----- include/alarm.hpp | 24 +++- include/appconstants.hpp | 2 +- include/configuration_manager.hpp | 39 ++---- include/httpclient.hpp | 2 + include/podcast_serializer.hpp | 132 +++++++++++++------- include/weather.hpp | 70 ++++++++++- libsrc/PodcastSource.cpp | 110 +++++++++-------- libsrc/alarm.cpp | 86 +++++++++++-- libsrc/configuration_manager.cpp | 149 ++++++----------------- libsrc/playableitem.cpp | 61 +++++++++- libsrc/podcast_serializer.cpp | 194 +++++++++++++++++------------- libsrc/weather.cpp | 53 ++++++-- test/CMakeLists.txt | 12 +- test/cm_mock.hpp | 10 +- test/mock_clock.hpp | 6 +- test/serializer_mock.hpp | 34 ++++++ test/test.cpp | 1 + test/test_alarm.cpp | 32 ++++- test/test_alarmmonitor.cpp | 4 +- test/test_podcast_reader.cpp | 2 +- test/test_podcast_serializer.cpp | 136 ++++++++++++++------- test/test_podcastsource.cpp | 121 ++++++++++++++----- test/test_settings.cpp | 48 +++++--- test/test_update_task.cpp | 4 +- test/test_weather.cpp | 105 +++++++++++++--- 28 files changed, 1048 insertions(+), 497 deletions(-) create mode 100644 test/serializer_mock.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3723e41..531affd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,7 +130,6 @@ configure_file( ) #Find includes in corresponding build directories -set(CMAKE_INCLUDE_CURRENT_DIR ON) include_directories(${PROJECT_INCLUDE_DIR} ${GENERATED_DIR}) #------------------------------------------------------------------------------- diff --git a/include/PlayableItem.hpp b/include/PlayableItem.hpp index e0d7568..487b759 100644 --- a/include/PlayableItem.hpp +++ b/include/PlayableItem.hpp @@ -16,10 +16,13 @@ #include #include #include +#include #include #include #include +#include + namespace DigitalRooster { /** @@ -120,7 +123,19 @@ class PlayableItem : public QObject { QUuid get_id() const { return id; } + /** + * Create PlayableItem/Radio Station from JSON JSonObject + * @param json_radio json representation + * @return Station object - default initialized if fields are missing + */ + static std::shared_ptr from_json_object( + const QJsonObject& json_radio); + /** + * JSon Representation of Internet Radio station + * @return + */ + QJsonObject to_json_object(); signals: /** * Human readable identifier changed (title, name, publisher etc) @@ -242,6 +257,20 @@ class PodcastEpisode : public PlayableItem { * @param len >= 0 */ void set_duration(qint64 len); + /** + * Create PlayableItem/Radio Station from JSON JSonObject + * @param json_episode episode as json object + * @return Station object - default initialized if fields are missing + */ + static std::shared_ptr from_json_object( + const QJsonObject& json_episode); + + /** + * JSon Representation of Internet Radio station + * @param episode to serialize + * @return + */ + QJsonObject to_json_object() const; signals: void description_changed(const QString& desc); diff --git a/include/PodcastSource.hpp b/include/PodcastSource.hpp index b04e181..6a2b00c 100644 --- a/include/PodcastSource.hpp +++ b/include/PodcastSource.hpp @@ -21,13 +21,14 @@ #include #include #include +#include #include #include #include #include "PlayableItem.hpp" #include "UpdateTask.hpp" - +#include "podcast_serializer.hpp" namespace DigitalRooster { class HttpClient; // forward declaration @@ -48,11 +49,9 @@ class PodcastSource : public QObject { /** * Preconfigured Podcast Source * @param url Feed URL - * @param cache_dir directory for cache files * @param uid unique id for this podcast */ - PodcastSource(const QUrl& url, const QDir& cache_dir, - QUuid uid = QUuid::createUuid()); + PodcastSource(const QUrl& url, QUuid uid = QUuid::createUuid()); /** * Destructor to delete icon_downloader nicely @@ -65,6 +64,14 @@ class PodcastSource : public QObject { */ void set_update_task(std::unique_ptr&& ut); + /** + * Serializer stores and restores data to/from cache file + * A PodcastSerializer is responsible for only one PodcastSource, + * -> PodcastSource takes ownership! + * @param pser + */ + void set_serializer(std::unique_ptr&& pser); + /** * Add an episode to episodes */ @@ -91,8 +98,8 @@ class PodcastSource : public QObject { void set_cache_dir(const QString& dirname); /** - * cached pocast source information in this file - * @return file path + * cached podcast source information in this file + * @return file name (without directory) */ QString get_cache_file_name() const; @@ -243,27 +250,31 @@ class PodcastSource : public QObject { * clear local information on podcast episodes */ void purge_episodes(); - -public slots: /** - * Triggers immediate update from web + * Create PodcastSource from JSON JSonObject + * @param json representation + * @return PodcastSource - default initialized if fields are missing */ - void refresh(); + static std::shared_ptr from_json_object( + const QJsonObject& json); /** - * purges local cache, removes all episodes and updates from internet + * Serialize as JSON Object - only contains information that is not + * updated through RSS feed and cached. + * @return QJsonObject */ - void purge(); + QJsonObject to_json_object() const; +public slots: /** - * save in memory inforamtion to cache file + * Triggers immediate update from web */ - void store(); + void refresh(); /** - * restore information from cache file + * purges local cache, removes all episodes and updates from internet */ - void restore(); + void purge(); /** * Properties of an episode e.g. title etc. has changed and a @@ -273,15 +284,29 @@ public slots: signals: /** - * The episodes list or any other member has been updated + * Title has changed + * Signal GUI */ - void dataChanged(); - void titleChanged(); + /** + * Description has been updated + * Signal GUI + */ void descriptionChanged(); + /** + * Icon image has been updated + * Signal GUI + */ void icon_changed(); + + /** + * The episodes list or any other member has been updated + * trigger a write + */ + void dataChanged(); + /** * A new episodes has been added * @param count updated number of episodes @@ -319,11 +344,6 @@ public slots: */ QDateTime last_updated; - /** - * Timer tor write configuration to disk - */ - QTimer writeTimer; - /** * Website of RSS feed channel (not the rss xml URI but additional * information) @@ -340,11 +360,6 @@ public slots: */ QString image_file_path; - /** - * Cache directory - */ - const QDir& cache_dir; - /** * show max_episodes in the list */ @@ -361,6 +376,11 @@ public slots: */ std::unique_ptr updater = nullptr; + /** + * Optional Serializer to cache information on disk + */ + std::unique_ptr serializer = nullptr; + /** * Optional Icon Downloader - only used to refresh icon */ diff --git a/include/alarm.hpp b/include/alarm.hpp index 1d21eab..466e7d2 100644 --- a/include/alarm.hpp +++ b/include/alarm.hpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include + #include #include @@ -114,7 +117,7 @@ class Alarm : public QObject { * Volume for this Alarm * @return */ - int get_volume() { + int get_volume() const { return volume; } /** @@ -129,7 +132,7 @@ class Alarm : public QObject { * Duration for alarm to stop automatically * @return time in minutes */ - std::chrono::minutes get_timeout() { + std::chrono::minutes get_timeout() const{ return timeout; } /** @@ -165,6 +168,21 @@ class Alarm : public QObject { void update_media_url(const QUrl& url); QUrl get_media_url() const; + /** + * JSon Representation of Alarm + * @param alarm object to serialize + * @return + */ + QJsonObject to_json_object() const; + + /** + * Create alarm from JSON JSonObject + * @param json json representation + * @return Alarm object - default initialized if fields are missing + */ + static std::shared_ptr from_json_object( + const QJsonObject& json_alarm); + public slots: /** * enable alarm to play next time @@ -227,7 +245,7 @@ public slots: /** * Default volume for alarm */ - int volume = 40; + int volume = DEFAULT_ALARM_VOLUME; }; /** diff --git a/include/appconstants.hpp b/include/appconstants.hpp index fc5f7be..14e03e1 100644 --- a/include/appconstants.hpp +++ b/include/appconstants.hpp @@ -71,7 +71,7 @@ const QString KEY_ID("id"); /** * property key for all URIs */ -const QString KEY_URI("uri"); +const QString KEY_URI("url"); /** * property key for all nice names diff --git a/include/configuration_manager.hpp b/include/configuration_manager.hpp index 70174b1..953b6bd 100644 --- a/include/configuration_manager.hpp +++ b/include/configuration_manager.hpp @@ -24,31 +24,10 @@ #include "PodcastSource.hpp" #include "alarm.hpp" #include "appconstants.hpp" +#include "weather.hpp" namespace DigitalRooster { -/** - * Simple POD for openweathermaps configuration - * with sensible default values - */ -struct WeatherConfig { - /* Base uri for OpenWeatherMaps API */ - QString base_uri{"http://api.openweathermap.org/data/2.5/weather?"}; - /** - * location id - * from http://bulk.openweathermap.org/sample/city.list.json.gz - * e.g. 'Esslingen,de' = id 2928751, Porto Alegre=3452925 - */ - QString cityid = {"2928751"}; - /** Openweathermap API Key */ - QString apikey = {"a904431b4e0eae431bcc1e075c761abb"}; - /** metric, imperial, */ - QString units = {"metric"}; - /* language for description 'en', 'de'...*/ - QString language = {"en"}; - /** Update Interval for wheather information */ - std::chrono::seconds update_interval{3600LL}; -}; /** * Reads JSON configuration @@ -177,7 +156,7 @@ class ConfigurationManager : public QObject { /** * Weather configuration object */ - const WeatherConfig& get_weather_config() { + const WeatherConfig* get_weather_config() { return get_weather_cfg(); } @@ -335,11 +314,6 @@ public slots: */ QVector> alarms; - /** - * Weather configuration - */ - WeatherConfig weather_cfg; - /** * Duration for alarm to stop automatically */ @@ -373,6 +347,11 @@ public slots: */ QMetaObject::Connection fwConn; + /** + * Weather configuration + */ + std::unique_ptr weather_cfg; + /** * Configuration directory, writable, created if it doesn't exist */ @@ -478,8 +457,8 @@ public slots: /** * Weather configuration object */ - virtual WeatherConfig& get_weather_cfg() { - return weather_cfg; + virtual const WeatherConfig* get_weather_cfg() { + return weather_cfg.get(); } /** diff --git a/include/httpclient.hpp b/include/httpclient.hpp index 6b4d57a..190a47e 100644 --- a/include/httpclient.hpp +++ b/include/httpclient.hpp @@ -18,8 +18,10 @@ #include #include #include +#include class QSslError; +class QNetworkReply; namespace DigitalRooster { diff --git a/include/podcast_serializer.hpp b/include/podcast_serializer.hpp index 7bc03c5..836b215 100644 --- a/include/podcast_serializer.hpp +++ b/include/podcast_serializer.hpp @@ -1,6 +1,7 @@ /****************************************************************************** * \filename - * \brief writes a podcast source to filesystem or restores its configuration + * \brief Serialization/Deserialization of PodcastSources and PodcastEpisodes + * from filesystem and/or JSON * * \details * @@ -13,91 +14,138 @@ #ifndef _PODCASTSERIALIZER_HPP_ #define _PODCASTSERIALIZER_HPP_ +#include #include #include #include #include +#include #include +#include + +#include "PlayableItem.hpp" namespace DigitalRooster { class PodcastSource; + /** * Serialization/Deserialization of PodcastSources and PodcastEpisodes * from filesystem */ -class PodcastSerializer : QObject { +class PodcastSerializer : public QObject { Q_OBJECT public: /** - * serializes podcast source to filesystem - * @param ps podcastsource to write - * @param file_path file to write + * Constructor + * @param app_cache_dir + * @param source optional PodcastSource can be set by + * \ref set_podcast_source later + * @param delay duration after which write occurs */ - void store_to_file(PodcastSource* ps, const QString& file_path); + explicit PodcastSerializer(const QDir& app_cache_dir, + PodcastSource* source = nullptr, + std::chrono::milliseconds delay = std::chrono::milliseconds(1000)); /** - * serializes podcast source to filesystem using - * default file path - * @param ps podcastsource to write + * Update PodcastSource */ - void store_to_file(PodcastSource* ps); + void set_podcast_source(PodcastSource* source); /** - * restore configuration of podcastsource form filesystem - * @param ps podcastsource to restore - * @param file_path file to read + * Need destructor to stop timer */ - void read_from_file(PodcastSource* ps, const QString& file_path); + ~PodcastSerializer(); + PodcastSerializer(const PodcastSerializer&) = delete; + PodcastSerializer(PodcastSerializer&&) = delete; + PodcastSerializer& operator=(const PodcastSerializer&) = delete; + PodcastSerializer& operator=(PodcastSerializer&&) = delete; +public slots: /** - * read configuration of podcast source to filesystem using - * default file path - * @param ps podcastsource to write + * Delete local cache file */ - void read_from_file(PodcastSource* ps); + void delete_cached_info(); /** - * parse file and configure podcastsource accordingly - * @param tl_obj top level object = PodcastSource representation - * @param ps podcast source to configure + * Triggers a delayed write */ - void parse_podcast_source_from_json(QJsonObject& tl_obj, PodcastSource* ps); + void delayed_write(); /** - * Read a single podcast episode object from file - * NVI to allow for test-mocking - * @param ep_obj JSON representation of a podcast Episode - * @return PodcastEpisode object + * Restore data in podcast source form file + * if data in file is more recent/complete + */ + void restore_info(); + /** + * Immediately write data to cache file */ - std::shared_ptr parse_episode_from_json( - const QJsonObject& ep_obj); + void write(); +private: /** - * Create a JSON Object representation of a \ref PodcastEpisode - * @param episode PodcastEpisode to serialize - * @return JSON Object representation + * Podcast Source to serialize/de-serialize */ - QJsonObject json_from_episode(const PodcastEpisode* episode); + PodcastSource* ps; /** - * Create a JSON Object representation of a PodcastSource - * @param ps the PodcastSource to serialize - * @return JSON Object representation + * Timer for delayed write operations */ - QJsonObject json_from_podcast_source(const PodcastSource* ps); + QTimer writeTimer; -private: /** - * Implementation of parse_episode_from_json - * @param ep_obj JSON representation of a podcast Episode - * @return PodcastEpisode object + * Cache directory */ - std::shared_ptr parse_episode_json_impl( - const QJsonObject& ep_obj); + const QDir& cache_dir; + + /** + * NVI implementation of delete_cached_info() + */ + virtual void delete_cache(); + /** + * NVI implementation of write() + */ + virtual void write_cache(); }; +/** + * serializes podcast source to filesystem + * @param ps podcastsource to write + * @param file_path file to write + */ +void store_to_file(PodcastSource* ps, const QString& file_path); + +/** + * restore configuration of podcastsource form filesystem + * @param ps podcastsource to restore + * @param file_path file to read + */ +void read_from_file(PodcastSource* ps, const QString& file_path); + +/** + * parse file and configure podcastsource accordingly + * @param tl_obj top level object = PodcastSource representation + * @param ps podcast source to configure + */ +void parse_podcast_source_from_json(const QJsonObject& tl_obj, PodcastSource* ps); + +/** + * Reads episodes array from \ref json and updates \ref ps + * if episode already exists we update the position information + * @param json object with cached information of podcast source and episodes + * @param ps PodcastSource to update + */ +void read_episodes_cache(const QJsonObject& json, PodcastSource* ps); + +/** + * Create a JSON Object representation of a PodcastSource + * including all cached/dynamic information form RSS feed + * @param ps the PodcastSource to serialize + * @return JSON Object representation + */ +QJsonObject json_from_podcast_source(const PodcastSource* ps); + /** * Exception thrown if serialized podcast source is corrupted, */ diff --git a/include/weather.hpp b/include/weather.hpp index d2704b5..ba4f03e 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -13,7 +13,6 @@ #ifndef _WEATHER_HPP_ #define _WEATHER_HPP_ -#include #include #include #include @@ -21,12 +20,76 @@ #include #include #include +#include namespace DigitalRooster { class ConfigurationManager; -struct WeatherConfig; +/** + * Configuration of weather source as found in application config file + * read by ConfigurationManager \ref ConfigurationManager::get_weather_cfg + */ +class WeatherConfig { +public: + /** + * Constructor with 'relevant' information + * @param token + * @param cityid + * @param interval 1hour + */ + WeatherConfig( + const QString& token =QString() , + const QString& cityid =QString() , + const std::chrono::seconds& interval = std::chrono::seconds(3600)); + + /** + * create Weatherconfig form Json configuration + * @param json jobject + * @return + */ + static std::unique_ptr from_json_object(const QJsonObject& json); + /** + * Serialize as JSON Object - only contains information that is not + * updated through RSS feed and cached. + * @return QJsonObject + */ + QJsonObject to_json_object() const; + + /** + * Openweather City id / location id + * @return \ref location_id + */ + const QString& get_location_id() const { + return location_id; + } + /** + * 'Secret' api-token for openweather api + * @return token string + */ + const QString& get_api_token() const{ + return api_token; + } + /** + * update interval to poll openweather api + * @return seconds + */ + const std::chrono::seconds get_update_interval() { + return update_interval; + } +private: + /** Openweathermap API Key */ + QString api_token; + /** + * location id + * from http://bulk.openweathermap.org/sample/city.list.json.gz + * e.g. 'Esslingen,de' = id 2928751, Porto Alegre=3452925 + */ + QString location_id; + /** Update Interval for wheather information */ + std::chrono::seconds update_interval; +}; + /** * Periodically downloads weather info from Openweathermaps @@ -175,9 +238,8 @@ public slots: * @param cfg configuration with location, units etc. * @return uri e.g. api.openweathermap.org/data/2.5/weather?zip=94040,us */ -QUrl create_weather_uri(const WeatherConfig& cfg); +QUrl create_weather_uri(const WeatherConfig* cfg); } // namespace DigitalRooster #endif /* _WEATHER_HPP_ */ - diff --git a/libsrc/PodcastSource.cpp b/libsrc/PodcastSource.cpp index 4dc46e5..bd44e11 100644 --- a/libsrc/PodcastSource.cpp +++ b/libsrc/PodcastSource.cpp @@ -29,23 +29,15 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.PodcastSource"); /*****************************************************************************/ -PodcastSource::PodcastSource(const QUrl& url, const QDir& cache_dir, QUuid uid) +PodcastSource::PodcastSource(const QUrl& url, QUuid uid) : id(uid) - , rss_feed_uri(url) - , cache_dir(cache_dir) { + , rss_feed_uri(url) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - // read initial settings from file if it exists - restore(); - // write timer will take care for delayed writes - writeTimer.setInterval(std::chrono::seconds(5)); - writeTimer.setSingleShot(true); - connect(&writeTimer, SIGNAL(timeout()), this, SLOT(store())); } /*****************************************************************************/ PodcastSource::~PodcastSource() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - writeTimer.stop(); if (download_cnx) disconnect(download_cnx); @@ -56,7 +48,6 @@ PodcastSource::~PodcastSource() { } } - /*****************************************************************************/ void PodcastSource::add_episode(std::shared_ptr newep) { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -80,7 +71,6 @@ void PodcastSource::add_episode(std::shared_ptr newep) { /* get notified if any data changes */ connect(newep.get(), SIGNAL(data_changed()), this, SLOT(episode_info_changed())); - writeTimer.start(); // start delayed write emit episodes_count_changed(episodes.size()); } else { qCDebug(CLASS_LC) << " > " << newep->get_guid() << "already in list"; @@ -101,14 +91,13 @@ void PodcastSource::set_description(QString newVal) { qCDebug(CLASS_LC) << Q_FUNC_INFO; description = newVal; emit descriptionChanged(); - writeTimer.start(); // start delayed write + emit dataChanged(); } /*****************************************************************************/ void PodcastSource::set_last_updated(QDateTime newVal) { qCDebug(CLASS_LC) << Q_FUNC_INFO; last_updated = newVal; - writeTimer.start(); // start delayed write } /*****************************************************************************/ @@ -116,7 +105,6 @@ void PodcastSource::set_link(QUrl newVal) { qCDebug(CLASS_LC) << Q_FUNC_INFO; link = newVal; emit dataChanged(); - writeTimer.start(); // start delayed write } /*****************************************************************************/ @@ -131,7 +119,7 @@ void PodcastSource::set_title(QString newTitle) { qCDebug(CLASS_LC) << Q_FUNC_INFO; title = newTitle; emit titleChanged(); - writeTimer.start(); // start delayed write + emit dataChanged(); } /*****************************************************************************/ @@ -143,7 +131,7 @@ QString PodcastSource::get_cache_file_name() const { /*****************************************************************************/ QString PodcastSource::get_cache_file_impl() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - return cache_dir.filePath(get_id().toString()); + return get_id().toString(); } /*****************************************************************************/ @@ -155,6 +143,17 @@ void PodcastSource::set_update_task(std::unique_ptr&& ut) { updater->set_update_interval(update_interval); } +/*****************************************************************************/ +void PodcastSource::set_serializer(std::unique_ptr&& pser) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + // We just take ownership + serializer = std::move(pser); + serializer->set_podcast_source(this); + // Setup connections + connect(this, &PodcastSource::dataChanged, serializer.get(), + &PodcastSerializer::delayed_write); +} + /*****************************************************************************/ QVector PodcastSource::get_episodes_names() { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -165,28 +164,6 @@ QVector PodcastSource::get_episodes_names() { return ret; } -/*****************************************************************************/ -void DigitalRooster::PodcastSource::restore() { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - PodcastSerializer serializer; - try { - serializer.read_from_file(this); - } catch (std::exception& exc) { - qCWarning(CLASS_LC) << "restore failed" << exc.what(); - } -} - -/*****************************************************************************/ -void DigitalRooster::PodcastSource::store() { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - PodcastSerializer serializer; - try { - serializer.store_to_file(this); - } catch (std::exception& exc) { - qCWarning(CLASS_LC) << "store failed" << exc.what(); - } -} - /*****************************************************************************/ void PodcastSource::refresh() { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -211,13 +188,6 @@ void PodcastSource::purge_icon_cache() { /*****************************************************************************/ void PodcastSource::purge_episodes() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - - QFile cache_file(get_cache_file_name()); - if (!cache_file.remove()) { - qCWarning(CLASS_LC) - << " removing cache_file failed " << get_cache_file_name() << ":" - << cache_file.errorString(); - } episodes.clear(); emit episodes_count_changed(episodes.size()); } @@ -227,12 +197,16 @@ void PodcastSource::purge() { qCDebug(CLASS_LC) << Q_FUNC_INFO; purge_episodes(); purge_icon_cache(); + // Remove cache file + if(serializer){ + serializer->delete_cached_info(); + } } /*****************************************************************************/ void PodcastSource::episode_info_changed() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - writeTimer.start(); // start delayed write + emit dataChanged(); } /*****************************************************************************/ @@ -276,6 +250,7 @@ void PodcastSource::set_image_url(const QUrl& uri) { qCDebug(CLASS_LC) << Q_FUNC_INFO; icon_url = uri; emit icon_changed(); + emit dataChanged(); } /*****************************************************************************/ void PodcastSource::set_image_file_path(const QString& path) { @@ -314,7 +289,46 @@ void PodcastSource::store_image(QByteArray data) { disconnect(download_cnx); icon_downloader.get()->deleteLater(); icon_downloader.release(); // let QT manage the object - writeTimer.start(); // start delayed write +} + +/*****************************************************************************/ +std::shared_ptr PodcastSource::from_json_object( + const QJsonObject& json) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + auto uid = QUuid::fromString( + json[KEY_ID].toString(QUuid::createUuid().toString())); + QUrl url(json[KEY_URI].toString()); + if (!url.isValid()) { + throw std::invalid_argument("invalid URL for podcast"); + } + + auto ps = std::make_shared(url, uid); + + auto title = json[KEY_TITLE].toString(); + auto desc = json[KEY_DESCRIPTION].toString(); + auto img_url = json[KEY_ICON_URL].toString(); + auto img_cached = json[KEY_IMAGE_CACHE].toString(); + ps->set_title(title); + ps->set_description(desc); + ps->set_image_url(img_url); + + ps->set_update_interval( + std::chrono::seconds(json[KEY_UPDATE_INTERVAL].toInt(3600))); + ps->set_update_task(std::make_unique(ps.get())); + return ps; +} + +/*****************************************************************************/ +QJsonObject PodcastSource::to_json_object() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonObject json; + json[KEY_ID] = get_id().toString(); + json[KEY_URI] = get_url().toString(); + json[KEY_TITLE] = get_title(); + json[KEY_UPDATE_INTERVAL] = + static_cast(get_update_interval().count()); + return json; } /*****************************************************************************/ diff --git a/libsrc/alarm.cpp b/libsrc/alarm.cpp index dfccd38..c0773ab 100644 --- a/libsrc/alarm.cpp +++ b/libsrc/alarm.cpp @@ -84,9 +84,10 @@ const QTime& Alarm::get_time() const { /*****************************************************************************/ -static const std::vector> period_to_string = { - {Alarm::Daily, KEY_ALARM_DAILY}, {Alarm::Workdays, KEY_ALARM_WORKDAYS}, - {Alarm::Weekend, KEY_ALARM_WEEKEND}, {Alarm::Once, KEY_ALARM_ONCE}}; +static const std::vector> + period_to_string = {{Alarm::Daily, KEY_ALARM_DAILY}, + {Alarm::Workdays, KEY_ALARM_WORKDAYS}, + {Alarm::Weekend, KEY_ALARM_WEEKEND}, {Alarm::Once, KEY_ALARM_ONCE}}; /*****************************************************************************/ void Alarm::update_media_url(const QUrl& url) { @@ -97,29 +98,94 @@ void Alarm::update_media_url(const QUrl& url) { } /*****************************************************************************/ - Alarm::Period DigitalRooster::json_string_to_alarm_period( const QString& literal) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; auto res = std::find_if(period_to_string.begin(), period_to_string.end(), [&](const std::pair& item) { return item.second == literal; }); - if (res == period_to_string.end()) - throw(std::invalid_argument(std::string("can't resolve argument"))); - + if (res == period_to_string.end()) { + throw(std::invalid_argument( + std::string("Invalid value for AlarmPeriod! :") + + literal.toStdString())); + } return res->first; } /*****************************************************************************/ QString DigitalRooster::alarm_period_to_json_string( const Alarm::Period period) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; auto res = std::find_if(period_to_string.begin(), period_to_string.end(), [&](const std::pair& item) { return item.first == period; }); - if (res == period_to_string.end()) - throw(std::invalid_argument(std::string("can't resolve argument"))); - + if (res == period_to_string.end()) { + qCWarning(CLASS_LC) << "Invalid Period passed (" << period << ")" << + "This should never happen - aborting"; + abort(); + } return res->second; } + +/*****************************************************************************/ +std::shared_ptr Alarm::from_json_object( + const QJsonObject& json_alarm) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (json_alarm.isEmpty()) { + throw std::invalid_argument("Empty Alarm JSON object!"); + } + + QUrl url(json_alarm[KEY_URI].toString()); + if (!url.isValid()) { + qCWarning(CLASS_LC) << "Invalid URI " << json_alarm[KEY_URI].toString(); + throw std::invalid_argument("Alarm URL invalid!"); + } + auto period = json_string_to_alarm_period( + json_alarm[KEY_ALARM_PERIOD].toString(KEY_ALARM_DAILY)); + auto timepoint = + QTime::fromString(json_alarm[KEY_TIME].toString(), "hh:mm"); + if(!timepoint.isValid()){ + qCWarning(CLASS_LC) << "Invalid Time " << json_alarm[KEY_TIME].toString(); + throw std::invalid_argument("Alarm Time invalid!"); + } + + auto enabled = json_alarm[KEY_ENABLED].toBool(true); + auto media = QUrl(json_alarm[KEY_URI].toString()); + auto id = QUuid::fromString( + json_alarm[KEY_ID].toString(QUuid::createUuid().toString())); + /* + * Create alarm with essential information + */ + auto alarm = std::make_shared(media, timepoint, period, enabled, id); + + /* Set volume if configured */ + if (json_alarm.contains(KEY_VOLUME)) { + auto volume = json_alarm[KEY_VOLUME].toInt(DEFAULT_ALARM_VOLUME); + alarm->set_volume(volume); + } + /* if no specific alarm timeout is given take application default */ + if (json_alarm.contains(KEY_ALARM_TIMEOUT)) { + auto timeout = + json_alarm[KEY_ALARM_TIMEOUT].toInt(DEFAULT_ALARM_TIMEOUT.count()); + alarm->set_timeout(std::chrono::minutes(timeout)); + } + return alarm; +} + +/*****************************************************************************/ +QJsonObject Alarm::to_json_object() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonObject alarmcfg; + alarmcfg[KEY_ID] = this->get_id().toString(); + alarmcfg[KEY_ALARM_PERIOD] = + alarm_period_to_json_string(this->get_period()); + alarmcfg[KEY_TIME] = this->get_time().toString("hh:mm"); + alarmcfg[KEY_VOLUME] = this->get_volume(); + alarmcfg[KEY_URI] = this->get_media()->get_url().toString(); + alarmcfg[KEY_ENABLED] = this->is_enabled(); + return alarmcfg; +} + /*****************************************************************************/ diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index d7a9456..0d922bf 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -82,6 +82,7 @@ ConfigurationManager::ConfigurationManager( , volume(DEFAULT_VOLUME) , brightness_sb(DEFAULT_BRIGHTNESS) , brightness_act(DEFAULT_BRIGHTNESS) + , weather_cfg(new WeatherConfig) , config_file(configpath) , application_cache_dir(cachedir) , wpa_socket_name(WPA_CONTROL_SOCKET_PATH) { @@ -201,19 +202,16 @@ void ConfigurationManager::read_radio_streams_from_file( qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray stations = appconfig[DigitalRooster::KEY_GROUP_IRADIO_SOURCES].toArray(); - for (const auto json_station : stations) { - auto station = json_station.toObject(); - QString name(station[KEY_NAME].toString()); - QUrl url(station[KEY_URI].toString()); - auto uid = QUuid::fromString( - station[KEY_ID].toString(QUuid::createUuid().toString())); - if (!url.isValid()) { + for (const auto& json_station : stations) { + try { + auto station = + PlayableItem::from_json_object(json_station.toObject()); + stream_sources.push_back(station); + } catch (std::invalid_argument& exc) { qCWarning(CLASS_LC) - << "Invalid URI " << station[KEY_URI].toString(); + << "cannot create station form JSON " << exc.what(); continue; } - stream_sources.push_back( - std::make_shared(name, url, uid)); } /* Sort alphabetically */ std::sort(stream_sources.begin(), stream_sources.end(), @@ -231,27 +229,19 @@ void ConfigurationManager::read_podcasts_from_file( QJsonArray podcasts = appconfig[DigitalRooster::KEY_GROUP_PODCAST_SOURCES].toArray(); for (const auto pc : podcasts) { - auto jo = pc.toObject(); + auto ps = PodcastSource::from_json_object(pc.toObject()); + auto serializer = std::make_unique( + application_cache_dir, ps.get()); + // populate podcast source from cached info + serializer->restore_info(); + // Move ownership to Podcast Source and setup signal/slot connections + ps->set_serializer(std::move(serializer)); - QUrl url(jo[KEY_URI].toString()); - if (!url.isValid()) { - qCWarning(CLASS_LC) << "Invalid URI " << jo[KEY_URI].toString(); - continue; - } - auto uid = QUuid::fromString( - jo[KEY_ID].toString(QUuid::createUuid().toString())); - auto ps = - std::make_shared(url, application_cache_dir, uid); - auto title = jo[KEY_NAME].toString(); - ps->set_title(title); - ps->set_update_interval( - std::chrono::seconds(jo[KEY_UPDATE_INTERVAL].toInt(3600))); ps->set_update_task(std::make_unique(ps.get())); // Get notifications if name etc. changes connect(ps.get(), &PodcastSource::dataChanged, this, &ConfigurationManager::dataChanged); - podcast_sources.push_back(ps); } std::sort(podcast_sources.begin(), podcast_sources.end(), @@ -268,44 +258,15 @@ void ConfigurationManager::read_alarms_from_file(const QJsonObject& appconfig) { QJsonArray alarm_config = appconfig[DigitalRooster::KEY_GROUP_ALARMS].toArray(); for (const auto al : alarm_config) { - auto json_alarm = al.toObject(); - QUrl url(json_alarm[KEY_URI].toString()); - if (!url.isValid()) { - qCWarning(CLASS_LC) - << "Invalid URI " << json_alarm[KEY_URI].toString(); - continue; - } - // sane default for periodicity - auto period = Alarm::Daily; try { - period = json_string_to_alarm_period( - json_alarm[KEY_ALARM_PERIOD].toString(KEY_ALARM_DAILY)); + auto alarm = Alarm::from_json_object(al.toObject()); + connect( + alarm.get(), SIGNAL(dataChanged()), this, SLOT(dataChanged())); + alarms.push_back(alarm); } catch (std::invalid_argument& exc) { - qCWarning(CLASS_LC) << "invalid periodicity entry! " << exc.what(); + qCWarning(CLASS_LC) + << "Invalid JSON values for Alarm" << exc.what(); } - - auto enabled = json_alarm[KEY_ENABLED].toBool(true); - auto media = QUrl(json_alarm[KEY_URI].toString()); - - auto timepoint = - QTime::fromString(json_alarm[KEY_TIME].toString(), "hh:mm"); - auto id = QUuid::fromString( - json_alarm[KEY_ID].toString(QUuid::createUuid().toString())); - /* - * Create alarm with essential information - */ - auto alarm = - std::make_shared(media, timepoint, period, enabled, id); - - auto volume = json_alarm[KEY_VOLUME].toInt(DEFAULT_ALARM_VOLUME); - alarm->set_volume(volume); - /* if no specific alarm timeout is given take application default */ - auto timeout = - json_alarm[KEY_ALARM_TIMEOUT].toInt(global_alarm_timeout.count()); - alarm->set_timeout(std::chrono::minutes(timeout)); - - connect(alarm.get(), SIGNAL(dataChanged()), this, SLOT(dataChanged())); - alarms.push_back(alarm); } qCDebug(CLASS_LC) << "read" << alarms.size() << "alarms"; } @@ -318,17 +279,10 @@ void ConfigurationManager::read_weather_from_file( return; } QJsonObject json_weather = appconfig[KEY_WEATHER].toObject(); - if (!json_weather[KEY_WEATHER_API_KEY].isNull()) { - weather_cfg.apikey = json_weather[KEY_WEATHER_API_KEY].toString(); - } else { - qCWarning(CLASS_LC) << "No openweathermaps API Key configured goto " - "https://openweathermap.org and get one!"; - } - - if (!json_weather[KEY_WEATHER_LOCATION_ID].isNull()) { - weather_cfg.cityid = json_weather[KEY_WEATHER_LOCATION_ID].toString(); - } else { - qCWarning(CLASS_LC) << "No weather location ID configured!"; + try { + weather_cfg = WeatherConfig::from_json_object(json_weather); + } catch (std::invalid_argument& exc) { + qCWarning(CLASS_LC) << "cannot parse weather config!"; } } @@ -346,7 +300,7 @@ const PlayableItem* ConfigurationManager::get_stream_source( const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* Find by id throws - just pass it on to the client */ - return find_by_id(stream_sources,id); + return find_by_id(stream_sources, id); } /*****************************************************************************/ @@ -363,7 +317,7 @@ const PodcastSource* ConfigurationManager::get_podcast_source( const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* Find by id throws - just pass it on to the client */ - return find_by_id(podcast_sources,id); + return find_by_id(podcast_sources, id); } /*****************************************************************************/ @@ -378,7 +332,7 @@ void ConfigurationManager::add_alarm(std::shared_ptr alm) { const Alarm* ConfigurationManager::get_alarm(const QUuid& id) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* Find by id throws - just pass it on to the client */ - return find_by_id(alarms,id); + return find_by_id(alarms, id); } /*****************************************************************************/ @@ -439,49 +393,27 @@ void ConfigurationManager::store_current_config() { QJsonArray podcasts; for (const auto& ps : podcast_sources) { - QJsonObject psconfig; - psconfig[KEY_NAME] = ps->get_title(); - psconfig[KEY_URI] = ps->get_url().toString(); - psconfig[KEY_ID] = ps->get_id().toString(); + QJsonObject psconfig = ps->to_json_object(); podcasts.append(psconfig); } appconfig[KEY_GROUP_PODCAST_SOURCES] = podcasts; QJsonArray iradios; for (const auto& iradiostream : stream_sources) { - QJsonObject irconfig; - irconfig[KEY_NAME] = iradiostream->get_display_name(); - irconfig[KEY_URI] = iradiostream->get_url().toString(); - irconfig[KEY_ID] = iradiostream->get_id().toString(); + auto irconfig = iradiostream->to_json_object(); iradios.append(irconfig); } appconfig[KEY_GROUP_IRADIO_SOURCES] = iradios; QJsonArray alarms_json; for (const auto& alarm : alarms) { - QJsonObject alarmcfg; - alarmcfg[KEY_ID] = alarm->get_id().toString(); - try { - alarmcfg[KEY_ALARM_PERIOD] = - alarm_period_to_json_string(alarm->get_period()); - } catch (std::invalid_argument& exc) { - qCCritical(CLASS_LC) << " invalid period " << alarm->get_period() - << " using default " << KEY_ALARM_ONCE; - alarmcfg[KEY_ALARM_PERIOD] = KEY_ALARM_ONCE; - } - alarmcfg[KEY_TIME] = alarm->get_time().toString("hh:mm"); - alarmcfg[KEY_VOLUME] = alarm->get_volume(); - alarmcfg[KEY_URI] = alarm->get_media()->get_url().toString(); - alarmcfg[KEY_ENABLED] = alarm->is_enabled(); + QJsonObject alarmcfg = alarm->to_json_object(); alarms_json.append(alarmcfg); } appconfig[KEY_GROUP_ALARMS] = alarms_json; /* Store Weather information*/ - QJsonObject json_weather; - json_weather[KEY_WEATHER_API_KEY] = weather_cfg.apikey; - json_weather[KEY_WEATHER_LOCATION_ID] = weather_cfg.cityid; - appconfig[KEY_WEATHER] = json_weather; + appconfig[KEY_WEATHER] = weather_cfg->to_json_object(); /* global application configuration */ appconfig[KEY_ALARM_TIMEOUT] = @@ -530,6 +462,7 @@ void ConfigurationManager::write_config_file(const QJsonObject& appconfig) { /*****************************************************************************/ void ConfigurationManager::create_default_configuration() { qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* Alarm */ auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/128/mp3/stream.mp3"), QTime::fromString("06:30", "hh:mm"), Alarm::Workdays); @@ -537,18 +470,16 @@ void ConfigurationManager::create_default_configuration() { /* Podcasts */ podcast_sources.push_back(std::make_shared( - QUrl("http://armscontrolwonk.libsyn.com/rss"), application_cache_dir)); + QUrl("http://armscontrolwonk.libsyn.com/rss"))); podcast_sources.push_back(std::make_shared( - QUrl("https://rss.acast.com/mydadwroteaporno"), application_cache_dir)); + QUrl("https://rss.acast.com/mydadwroteaporno"))); podcast_sources.push_back(std::make_shared( - QUrl("https://alternativlos.org/alternativlos.rss"), - application_cache_dir)); + QUrl("https://alternativlos.org/alternativlos.rss"))); podcast_sources.push_back(std::make_shared( - QUrl("http://www.podcastone.com/podcast?categoryID2=1225"), - application_cache_dir)); + QUrl("http://www.podcastone.com/podcast?categoryID2=1225"))); /* Radio Streams */ stream_sources.push_back(std::make_shared("Deutschlandfunk", @@ -618,7 +549,7 @@ void ConfigurationManager::remove_podcast_source_by_index(int index) { void ConfigurationManager::delete_alarm(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* delete may throw - just pass it on to the client */ - delete_by_id(alarms,id); + delete_by_id(alarms, id); dataChanged(); emit alarms_changed(); }; @@ -627,7 +558,7 @@ void ConfigurationManager::delete_alarm(const QUuid& id) { void ConfigurationManager::delete_podcast_source(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* delete may throw - just pass it on to the client */ - delete_by_id(podcast_sources,id); + delete_by_id(podcast_sources, id); dataChanged(); emit alarms_changed(); }; @@ -636,7 +567,7 @@ void ConfigurationManager::delete_podcast_source(const QUuid& id) { void ConfigurationManager::delete_radio_station(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* delete may throw - just pass it on to the client */ - delete_by_id(stream_sources,id); + delete_by_id(stream_sources, id); dataChanged(); emit alarms_changed(); }; diff --git a/libsrc/playableitem.cpp b/libsrc/playableitem.cpp index 3ccdf60..2d15878 100644 --- a/libsrc/playableitem.cpp +++ b/libsrc/playableitem.cpp @@ -16,6 +16,7 @@ #include #include "PlayableItem.hpp" +#include "appconstants.hpp" using namespace DigitalRooster; @@ -88,6 +89,30 @@ void PlayableItem::set_seekable(bool seek) { +/***********************************************************************/ +std::shared_ptr PlayableItem::from_json_object( + const QJsonObject& json_radio) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QString name(json_radio[KEY_NAME].toString()); + QUrl url(json_radio[KEY_URI].toString()); + auto uid = QUuid::fromString( + json_radio[KEY_ID].toString(QUuid::createUuid().toString())); + if (!url.isValid()) { + throw std::invalid_argument("invalid URL for RadioStation"); + } + return std::make_shared(name, url, uid); +} + +/***********************************************************************/ +QJsonObject PlayableItem::to_json_object() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonObject irconfig; + irconfig[KEY_NAME] = this->get_display_name(); + irconfig[KEY_URI] = this->get_url().toString(); + irconfig[KEY_ID] = this->get_id().toString(); + return irconfig; +} + /***********************************************************************/ QString PodcastEpisode::do_get_display_name() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -185,7 +210,6 @@ bool PodcastEpisode::already_listened() const { /***********************************************************************/ void PodcastEpisode::set_publication_date(const QDateTime& date) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - if (!date.isValid()) { qCCritical(CLASS_LC) << "invalid QDateTime!"; return; @@ -196,3 +220,38 @@ void PodcastEpisode::set_publication_date(const QDateTime& date) { } /***********************************************************************/ +std::shared_ptr PodcastEpisode::from_json_object( + const QJsonObject& json_episode) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + auto title = json_episode[KEY_TITLE].toString(); + auto media_url = QUrl(json_episode[KEY_URI].toString()); + auto ep = std::make_shared(title, media_url); + auto duration = json_episode[KEY_DURATION].toInt(1); + ep->set_duration(duration); + auto position = json_episode[KEY_POSITION].toInt(0); + ep->set_position(position); + + ep->set_publication_date( + QDateTime::fromString(json_episode[KEY_PUBLISHED].toString())); + ep->set_publisher(json_episode[KEY_PUBLISHER].toString()); + ep->set_description(json_episode[KEY_DESCRIPTION].toString()); + /* pubisher assinged id, can be url format hence a string not a QUuid */ + ep->set_guid(json_episode[KEY_ID].toString()); + return ep; +} + +/***********************************************************************/ +QJsonObject PodcastEpisode::to_json_object() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonObject ep_obj; + ep_obj[KEY_TITLE] = get_title(); + ep_obj[KEY_URI] = get_url().toString(); + ep_obj[KEY_DURATION] = get_duration(); + ep_obj[KEY_POSITION] = get_position(); + ep_obj[KEY_ID] = get_guid(); + ep_obj[KEY_PUBLISHED] = get_publication_date().toString(); + ep_obj[KEY_DESCRIPTION] = get_description(); + ep_obj[KEY_PUBLISHER] = get_publisher(); + return ep_obj; +} diff --git a/libsrc/podcast_serializer.cpp b/libsrc/podcast_serializer.cpp index 7e97eaa..9bed126 100644 --- a/libsrc/podcast_serializer.cpp +++ b/libsrc/podcast_serializer.cpp @@ -18,19 +18,94 @@ #include "appconstants.hpp" #include "podcast_serializer.hpp" #include "timeprovider.hpp" -using namespace DigitalRooster; +using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.PodcastSerializer"); /*****************************************************************************/ -void PodcastSerializer::store_to_file( +PodcastSerializer::PodcastSerializer(const QDir& app_cache_dir, + PodcastSource* source, std::chrono::milliseconds delay) + : ps(source) + , cache_dir(app_cache_dir) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + // write timer will take care for delayed writes + writeTimer.setInterval(delay); + writeTimer.setSingleShot(true); + connect(&writeTimer, &QTimer::timeout, this, &PodcastSerializer::write); +} + +/*****************************************************************************/ +PodcastSerializer::~PodcastSerializer() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + writeTimer.stop(); +} + +/*****************************************************************************/ +void PodcastSerializer::restore_info() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (ps != nullptr) { + auto cache_file = cache_dir.filePath(ps->get_id().toString()); + try { + read_from_file(ps, cache_file); + } catch (std::system_error& exc) { + qCWarning(CLASS_LC) << "Cache file not found" << cache_file; + } + } +} + +/*****************************************************************************/ +void PodcastSerializer::set_podcast_source(PodcastSource* source) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + ps = source; +} + +/*****************************************************************************/ +void PodcastSerializer::write() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (ps != nullptr) { + write_cache(); + } +} + +/*****************************************************************************/ +void PodcastSerializer::write_cache(){ + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto cache_file = cache_dir.filePath(ps->get_id().toString()); + store_to_file(ps, cache_file); +} + +/*****************************************************************************/ +void PodcastSerializer::delete_cached_info() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (ps != nullptr) { + delete_cache(); + } +} + +/*****************************************************************************/ +void PodcastSerializer::delete_cache(){ + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QFile cache_file(cache_dir.filePath(ps->get_id().toString())); + cache_file.remove(); +} + +/*****************************************************************************/ +void PodcastSerializer::delayed_write() { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (!writeTimer.isActive()) { + writeTimer.start(); // start delayed write + } +} + +/*****************************************************************************/ +void DigitalRooster::store_to_file( PodcastSource* ps, const QString& file_path) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonObject ps_obj = json_from_podcast_source(ps); QJsonArray episodes; for (const auto& episode : ps->get_episodes()) { - episodes.append(json_from_episode(episode.get())); + episodes.append(episode->to_json_object()); } ps_obj[KEY_EPISODES] = episodes; @@ -47,13 +122,7 @@ void PodcastSerializer::store_to_file( } /*****************************************************************************/ -void PodcastSerializer::store_to_file(PodcastSource* ps) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - store_to_file(ps, ps->get_cache_file_name()); -} - -/*****************************************************************************/ -void PodcastSerializer::read_from_file( +void DigitalRooster::read_from_file( PodcastSource* ps, const QString& file_path) { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -68,6 +137,7 @@ void PodcastSerializer::read_from_file( QJsonObject tl_obj = json_doc.object(); if (!tl_obj.isEmpty()) { parse_podcast_source_from_json(tl_obj, ps); + read_episodes_cache(tl_obj, ps); } else { qCWarning(CLASS_LC) << "empty json document read from" << file_path; throw PodcastSourceJSonCorrupted("Document empty!"); @@ -75,19 +145,40 @@ void PodcastSerializer::read_from_file( } /*****************************************************************************/ -void PodcastSerializer::read_from_file(PodcastSource* ps) { +void DigitalRooster::read_episodes_cache( + const QJsonObject& tl_obj, PodcastSource* ps) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - read_from_file(ps, ps->get_cache_file_name()); + auto episodes_json_array = tl_obj[KEY_EPISODES].toArray(); + if (episodes_json_array.isEmpty()) { + qCWarning(CLASS_LC) + << "JSON for PodcastSource does not contain episodes"; + return; + } + /* + * in either case read individual podcast episiode settings to at least set + * episode positions + */ + for (const auto& ep : episodes_json_array) { + auto ep_ptr = PodcastEpisode::from_json_object(ep.toObject()); + auto existing_ep = ps->get_episode_by_id(ep_ptr->get_guid()); + if (existing_ep) { + // found -> update position + existing_ep->set_position(ep_ptr->get_position()); + } else { + // not found -> add + ps->add_episode(ep_ptr); + } + } } /*****************************************************************************/ -void PodcastSerializer::parse_podcast_source_from_json( - QJsonObject& tl_obj, PodcastSource* ps) { +void DigitalRooster::parse_podcast_source_from_json( + const QJsonObject& tl_obj, PodcastSource* ps) { qCDebug(CLASS_LC) << Q_FUNC_INFO; // read podcast source configuration properties auto ts_str = tl_obj[KEY_TIMESTAMP].toString(); QDateTime timestamp; - if (ts_str != "") { + if (!ts_str.isEmpty()) { timestamp = QDateTime::fromString(ts_str); } if (!timestamp.isValid()) { @@ -109,59 +200,13 @@ void PodcastSerializer::parse_podcast_source_from_json( ps->set_image_url(img_url); ps->set_image_file_path(img_cached); } - auto episodes_json_array = tl_obj[KEY_EPISODES].toArray(); - if (episodes_json_array.isEmpty()) { - qCWarning(CLASS_LC) - << "JSON for PodcastSource does not contain episodes"; - return; - } - /* - * in either case read individual podcast episiode settings to at least set - * episode positions - */ - for (const auto& ep : episodes_json_array) { - auto ep_ptr = parse_episode_from_json(ep.toObject()); - auto existing_ep = ps->get_episode_by_id(ep_ptr->get_guid()); - if (existing_ep) { // found -> update position - existing_ep->set_position(ep_ptr->get_position()); - } else { // not found -> add - ps->add_episode(ep_ptr); - } - } -} - -/*****************************************************************************/ -std::shared_ptr PodcastSerializer::parse_episode_from_json( - const QJsonObject& ep_obj) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - return parse_episode_json_impl(ep_obj); -} - -/*****************************************************************************/ -QJsonObject PodcastSerializer::json_from_episode( - const PodcastEpisode* episode) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - QJsonObject ep_obj; - - ep_obj[KEY_TITLE] = episode->get_title(); - ep_obj[KEY_URI] = episode->get_url().toString(); - ep_obj[KEY_DURATION] = episode->get_duration(); - ep_obj[KEY_POSITION] = episode->get_position(); - ep_obj[KEY_ID] = episode->get_guid(); - - ep_obj[KEY_PUBLISHED] = episode->get_publication_date().toString(); - ep_obj[KEY_DESCRIPTION] = episode->get_description(); - ep_obj[KEY_PUBLISHER] = episode->get_publisher(); - return ep_obj; } /*****************************************************************************/ -QJsonObject DigitalRooster::PodcastSerializer::json_from_podcast_source( - const PodcastSource* ps) { +QJsonObject DigitalRooster::json_from_podcast_source(const PodcastSource* ps) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - QJsonObject ps_obj; + QJsonObject ps_obj = ps->to_json_object(); ps_obj[KEY_TIMESTAMP] = wallclock->now().toString(); - ps_obj[KEY_TITLE] = ps->get_title(); ps_obj[KEY_DESCRIPTION] = ps->get_description(); ps_obj[KEY_ICON_URL] = ps->get_image_url().toString(); ps_obj[KEY_IMAGE_CACHE] = ps->get_image_file_path(); @@ -169,26 +214,3 @@ QJsonObject DigitalRooster::PodcastSerializer::json_from_podcast_source( } /*****************************************************************************/ -std::shared_ptr PodcastSerializer::parse_episode_json_impl( - const QJsonObject& ep_obj) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - - auto title = ep_obj[KEY_TITLE].toString(); - auto media_url = QUrl(ep_obj[KEY_URI].toString()); - auto ep = std::make_shared(title, media_url); - ep->set_title(title); - auto duration = ep_obj[KEY_DURATION].toInt(1); - ep->set_duration(duration); - auto position = ep_obj[KEY_POSITION].toInt(0); - ep->set_position(position); - - ep->set_publication_date( - QDateTime::fromString(ep_obj[KEY_PUBLISHED].toString())); - ep->set_publisher(ep_obj[KEY_PUBLISHER].toString()); - ep->set_description(ep_obj[KEY_DESCRIPTION].toString()); - /* pubisher assinged id, can be url format hence a string not a QUuid */ - ep->set_guid(ep_obj[KEY_ID].toString()); - return ep; -} - -/*****************************************************************************/ diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index 2036c74..933c102 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -79,17 +79,16 @@ void Weather::parse_response(QByteArray content) { } /*****************************************************************************/ -QUrl DigitalRooster::create_weather_uri(const WeatherConfig& cfg) { +QUrl DigitalRooster::create_weather_uri(const WeatherConfig* cfg) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - QString uri_str(cfg.base_uri); - uri_str += "id="; - uri_str += cfg.cityid; - uri_str += "&units="; - uri_str += cfg.units; - uri_str += "&appid="; - uri_str += cfg.apikey; - // qDebug() << uri_str; - return QUrl(uri_str); + QString request_str({"http://api.openweathermap.org/data/2.5/weather?"}); + request_str.reserve(512); + request_str += "id="; + request_str += cfg->get_location_id(); + request_str += "&units=metric"; + request_str += "&appid="; + request_str += cfg->get_api_token(); + return QUrl(request_str); } /*****************************************************************************/ void Weather::parse_city(const QJsonObject& o) { @@ -122,4 +121,38 @@ void Weather::parse_condition(const QJsonObject& o) { emit condition_changed(condition); emit icon_changed(icon_id); } + +/*****************************************************************************/ +WeatherConfig::WeatherConfig(const QString& token, const QString& location, + const std::chrono::seconds& interval) + : api_token(token) + , location_id(location) + , update_interval(interval) { +} + +/*****************************************************************************/ +QJsonObject WeatherConfig::to_json_object() const{ + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonObject j; + j[KEY_UPDATE_INTERVAL] = static_cast(update_interval.count()); + j[KEY_WEATHER_LOCATION_ID] = location_id; + j[KEY_WEATHER_API_KEY] = api_token; + return j; +} + +/*****************************************************************************/ +std::unique_ptr WeatherConfig::from_json_object(const QJsonObject& json) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + if (json[KEY_WEATHER_LOCATION_ID].toString().isEmpty()) { + throw std::invalid_argument("Json Weather has no location id"); + } + if (json[KEY_WEATHER_API_KEY].toString().isEmpty()) { + throw std::invalid_argument("Json Weather has no API key !"); + } + auto interval = + std::chrono::seconds(json[KEY_UPDATE_INTERVAL].toInt(3600LL)); + + return std::make_unique(json[KEY_WEATHER_API_KEY].toString(), + json[KEY_WEATHER_LOCATION_ID].toString(), interval); +} /*****************************************************************************/ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index cc3ed51..e3ac102 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -233,12 +233,18 @@ TARGET_LINK_LIBRARIES( IF(TEST_COVERAGE) IF(NOT MSVC) - #Remove googletest, autogenerated QT code and headers for testcovera - SET(COVERAGE_EXCLUDES '*GTestExternal/*' "*_autogen*" '/usr/*') + #Remove googletest, autogenerated QT code and headers for testcoverage + SET(COVERAGE_EXCLUDES + '*GTestExternal/*' + '*PistacheExternal/*' + "*_autogen*" + '/usr/*' + ) SETUP_TARGET_FOR_COVERAGE( NAME ${COMPONENT_NAME}_coverage # New target name EXECUTABLE ${COMPONENT_NAME} -j 4 # Executable in PROJECT_BINARY_DIR - DEPENDENCIES ${COMPONENT_NAME}) + DEPENDENCIES ${COMPONENT_NAME} + ) ELSE(NOT MSVC) SET_PROPERTY(TARGET ${COMPONENT_NAME} APPEND PROPERTY LINK_FLAGS /PROFILE) diff --git a/test/cm_mock.hpp b/test/cm_mock.hpp index 6f90e92..34c3daf 100644 --- a/test/cm_mock.hpp +++ b/test/cm_mock.hpp @@ -10,8 +10,8 @@ * *****************************************************************************/ -#include #include +#include #include #include "gmock/gmock.h" @@ -30,14 +30,12 @@ class CmMock : public DigitalRooster::ConfigurationManager { qRegisterMetaType>( "std::shared_ptr"); - weather_cfg.cityid = "2172797"; // Cairns, AU - weather_cfg.language = "de"; - weather_cfg.units = "metric"; + }; MOCK_METHOD0( get_alarm_list, QVector>&()); - MOCK_METHOD0(get_weather_cfg, DigitalRooster::WeatherConfig&()); + MOCK_METHOD0(get_weather_cfg, const DigitalRooster::WeatherConfig*()); MOCK_CONST_METHOD0(do_get_brightness_sb, int()); MOCK_CONST_METHOD0(do_get_brightness_act, int()); @@ -50,5 +48,5 @@ class CmMock : public DigitalRooster::ConfigurationManager { QVector> alarms; - DigitalRooster::WeatherConfig weather_cfg; + std::unique_ptr weather_cfg; }; diff --git a/test/mock_clock.hpp b/test/mock_clock.hpp index 0da1285..7debf9f 100644 --- a/test/mock_clock.hpp +++ b/test/mock_clock.hpp @@ -10,6 +10,8 @@ * ******************************************************************************/ +#ifndef _INCLUDE_MOCK_CLOCK_ +#define _INCLUDE_MOCK_CLOCK_ // Mock Wallclock to test weekends & workdays #include "timeprovider.hpp" @@ -17,4 +19,6 @@ class MockClock : public DigitalRooster::TimeProvider { public: MOCK_METHOD0(get_time, QDateTime()); }; -/*****************************************************************************/ \ No newline at end of file +/*****************************************************************************/ + +#endif /*_INCLUDE_MOCK_CLOCK_*/ diff --git a/test/serializer_mock.hpp b/test/serializer_mock.hpp new file mode 100644 index 0000000..ede2c00 --- /dev/null +++ b/test/serializer_mock.hpp @@ -0,0 +1,34 @@ +/******************************************************************************* + * \filename + * \brief Allows mocking of the wallclock during test + * + * \details + * + * \copyright (c) 2018 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + ******************************************************************************/ + +#ifndef TEST_SERIALIZER_MOCK_HPP_ +#define TEST_SERIALIZER_MOCK_HPP_ +#include +#include +#include + +#include "podcast_serializer.hpp" + +/******************************************************************************/ +class SerializerMock : public DigitalRooster::PodcastSerializer { +public: + SerializerMock(const QDir& app_cache_dir, + DigitalRooster::PodcastSource* source = nullptr, + std::chrono::milliseconds delay = std::chrono::milliseconds(100)) + : PodcastSerializer(app_cache_dir, source, delay){}; + MOCK_METHOD0(write_cache, void()); + MOCK_METHOD0(delete_cache, void()); +}; + + +/*****************************************************************************/ +#endif /* TEST_SERIALIZER_MOCK_HPP_ */ diff --git a/test/test.cpp b/test/test.cpp index 4dd4717..ca1a236 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -74,3 +74,4 @@ int main(int argc, char** argv) { app.exec(); return ret; } +/*****************************************************************************/ diff --git a/test/test_alarm.cpp b/test/test_alarm.cpp index 604980c..5c8bb83 100644 --- a/test/test_alarm.cpp +++ b/test/test_alarm.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -30,7 +31,7 @@ using ::testing::AtLeast; /*****************************************************************************/ TEST(Alarm, defaultVolume) { Alarm al; - ASSERT_EQ(al.get_volume(), 40); + ASSERT_EQ(al.get_volume(), DEFAULT_ALARM_VOLUME); } /*****************************************************************************/ @@ -79,7 +80,7 @@ TEST(Alarm, fullConstructorEnabled) { /*****************************************************************************/ TEST(StringToPeriodEnum, mapping_bad) { - EXPECT_THROW(json_string_to_alarm_period("Foobar"), std::exception); + EXPECT_THROW(json_string_to_alarm_period("Foobar"), std::invalid_argument); } /*****************************************************************************/ @@ -93,6 +94,7 @@ TEST(Alarm, defaultTimeout) { TEST(Alarm, updatedTimeout) { Alarm al(QUrl("http://st01.dlf.de/dlf/01/128/mp3/stream.mp3"), QTime::currentTime().addSecs(-3600)); + ASSERT_EQ(al.get_timeout(), DEFAULT_ALARM_TIMEOUT); al.set_timeout(std::chrono::minutes(5)); ASSERT_EQ(al.get_timeout().count(), 5); } @@ -114,3 +116,29 @@ TEST(Alarm, periodChangeEmits) { QList arguments = spy_period_string.takeFirst(); ASSERT_EQ(arguments.at(0).toString(), QString("daily")); } +/*****************************************************************************/ +TEST(Alarm, construct_from_json) { + QString json_string(R"( + { + "id": "{247c4f9d-9626-4061-a8cc-1bd2249a0a20}", + "period": "workdays", + "time": "06:30", + "enabled": false, + "url": "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3", + "volume": 40, + "alarmTimeout": 45 + } + )"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + auto alarm = Alarm::from_json_object(jdoc.object()); + + ASSERT_EQ(alarm->get_id().toString(), + QString("{247c4f9d-9626-4061-a8cc-1bd2249a0a20}")); + ASSERT_EQ(alarm->get_period(), Alarm::Workdays); + ASSERT_EQ(alarm->get_time(), QTime::fromString("06:30", "hh:mm")); + ASSERT_FALSE(alarm->is_enabled()); + ASSERT_EQ(alarm->get_media_url(), + QUrl("http://st01.dlf.de/dlf/01/128/mp3/stream.mp3")); + ASSERT_EQ(alarm->get_timeout(), std::chrono::minutes(45)); + ASSERT_EQ(alarm->get_volume(), 40); +} diff --git a/test/test_alarmmonitor.cpp b/test/test_alarmmonitor.cpp index b96027a..e405f54 100644 --- a/test/test_alarmmonitor.cpp +++ b/test/test_alarmmonitor.cpp @@ -66,7 +66,7 @@ TEST(AlarmMonitor, triggersFallbackForError) { EXPECT_CALL(*(player.get()), do_play()).Times(2); EXPECT_CALL(*(player.get()), do_set_media(_)).Times(1); - EXPECT_CALL(*(player.get()), do_set_volume(40)).Times(1); + EXPECT_CALL(*(player.get()), do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); // Fallback behavior EXPECT_CALL(*(player.get()), do_set_volume(50)).Times(1); @@ -113,7 +113,7 @@ TEST(AlarmMonitor, noFallBackIfStoppedNormally) { EXPECT_CALL(*(player.get()), do_play()).Times(1); EXPECT_CALL(*(player.get()), do_set_media(_)).Times(1); - EXPECT_CALL(*(player.get()), do_set_volume(40)).Times(1); + EXPECT_CALL(*(player.get()), do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); EXPECT_CALL(*(player.get()), do_error()).Times(AtLeast(1)).WillRepeatedly(Return(QMediaPlayer::NoError)); mon.alarm_triggered(alm); diff --git a/test/test_podcast_reader.cpp b/test/test_podcast_reader.cpp index 9571349..50a0bfa 100644 --- a/test/test_podcast_reader.cpp +++ b/test/test_podcast_reader.cpp @@ -26,7 +26,7 @@ class PodcastReaderFixture : public virtual ::testing::Test { PodcastReaderFixture() : file(TEST_FILE_PATH + "/alternativlos.rss") , cache_dir(DEFAULT_CACHE_DIR_PATH) - , ps(QUrl("https://alternativlos.org/alternativlos.rss"), cache_dir) { + , ps(QUrl("https://alternativlos.org/alternativlos.rss")) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << file.errorString(); throw std::system_error( diff --git a/test/test_podcast_serializer.cpp b/test/test_podcast_serializer.cpp index 723d8cd..be2aaf4 100644 --- a/test/test_podcast_serializer.cpp +++ b/test/test_podcast_serializer.cpp @@ -24,6 +24,7 @@ #include "PodcastSource.hpp" #include "appconstants.hpp" #include "podcast_serializer.hpp" +#include "serializer_mock.hpp" using namespace DigitalRooster; using namespace ::testing; @@ -33,8 +34,8 @@ using ::testing::Invoke; /******************************************************************************/ class PodcastSourceMock : public DigitalRooster::PodcastSource { public: - explicit PodcastSourceMock(const QDir& cachedir) - : PodcastSource(QUrl(), cachedir){}; + explicit PodcastSourceMock() + : PodcastSource(QUrl()){}; MOCK_CONST_METHOD0(get_cache_file_impl, QString()); MOCK_CONST_METHOD0(get_description, const QString&()); @@ -50,9 +51,7 @@ class PodcastSourceMock : public DigitalRooster::PodcastSource { /******************************************************************************/ class PodcastSourceMock_episodes : public PodcastSourceMock { public: - explicit PodcastSourceMock_episodes(const QDir& cachedir) - : PodcastSourceMock(cachedir){}; - + explicit PodcastSourceMock_episodes(){}; MOCK_CONST_METHOD0( get_episodes_impl, const QVector>&()); }; @@ -62,15 +61,15 @@ class SerializerFixture : public ::testing::Test { public: SerializerFixture() : cache_dir(DEFAULT_CACHE_DIR_PATH) - , psmock(cache_dir) - , ps(QUrl(), cache_dir){}; + , ps(QUrl()){}; // Make our own clock to be the wallclock void SetUp() { mc = std::make_shared(); DigitalRooster::wallclock = std::static_pointer_cast(mc); - + // Make sure cache path exists + cache_dir.mkpath("."); episode1_json[KEY_URI] = episode1_url.toString(); episode1_json[KEY_TITLE] = episode1_title; episode1_json[KEY_DURATION] = episode1_duration; @@ -120,16 +119,12 @@ class SerializerFixture : public ::testing::Test { /******************************************************************************/ TEST_F(SerializerFixture, FileNotFoundThrows) { - PodcastSerializer dut; - EXPECT_CALL(psmock, get_cache_file_impl()) - .Times(1) - .WillOnce(Return(QString("/tmp/some_non_existent_file"))); - ASSERT_THROW(dut.read_from_file(&psmock), std::system_error); + QString test_file_name("/tmp/some_non_existent_file"); + ASSERT_THROW(read_from_file(&psmock, test_file_name), std::system_error); } /******************************************************************************/ TEST_F(SerializerFixture, PodcastSourceSerialization) { - PodcastSerializer dut; EXPECT_CALL(psmock, get_description()) .Times(1) .WillOnce(ReturnRef(expected_desc)); @@ -143,24 +138,22 @@ TEST_F(SerializerFixture, PodcastSourceSerialization) { .Times(1) .WillOnce(Return(expected_timestamp)); - auto json_obj = dut.json_from_podcast_source(&psmock); + auto json_obj = json_from_podcast_source(&psmock); ASSERT_EQ(json_obj[KEY_DESCRIPTION].toString(), expected_desc); ASSERT_EQ( json_obj[KEY_TIMESTAMP].toString(), expected_timestamp.toString()); - ASSERT_EQ( - json_obj[KEY_ICON_URL].toString(), expected_image_url.toString()); + ASSERT_EQ(json_obj[KEY_ICON_URL].toString(), expected_image_url.toString()); ASSERT_EQ(json_obj[KEY_TITLE].toString(), expected_title); } /******************************************************************************/ TEST_F(SerializerFixture, PodcastEpisodeSerialization) { - PodcastSerializer dut; PodcastEpisode episode(episode1_title, episode1_url); episode.set_duration(episode1_duration); // duration before position episode.set_position(episode1_position); // can't set position if duration=0 episode.set_guid(episode1_guid); - auto json_obj = dut.json_from_episode(&episode); + auto json_obj = episode.to_json_object(); ASSERT_EQ(json_obj[KEY_TITLE].toString(), episode1_title); ASSERT_EQ(json_obj[KEY_URI].toString(), episode1_url.toString()); ASSERT_EQ(json_obj[KEY_POSITION].toInt(), episode1_position); @@ -170,7 +163,6 @@ TEST_F(SerializerFixture, PodcastEpisodeSerialization) { /******************************************************************************/ TEST_F(SerializerFixture, PodcastSourceFromJson_PsWasNeverUpdated) { - PodcastSerializer dut; QDateTime invalid_date; EXPECT_CALL(psmock, get_last_updated()) @@ -180,7 +172,7 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_PsWasNeverUpdated) { QJsonObject json_ps; json_ps[KEY_TITLE] = expected_title; json_ps[KEY_TIMESTAMP] = expected_timestamp.toString(); - dut.parse_podcast_source_from_json(json_ps, &psmock); + parse_podcast_source_from_json(json_ps, &psmock); ASSERT_EQ(psmock.get_title_nonmock(), expected_title); } @@ -188,7 +180,6 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_PsWasNeverUpdated) { /******************************************************************************/ TEST_F(SerializerFixture, PodcastSourceFromJson_PsRecentlyUpdated) { - PodcastSerializer dut; // Recently updated PodcastSource with title should not be changed if // file is older psmock.set_title(expected_title); @@ -202,7 +193,7 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_PsRecentlyUpdated) { QJsonObject json_ps; json_ps[KEY_TITLE] = QString("some_old_title"); json_ps[KEY_TIMESTAMP] = expected_timestamp.toString(); - dut.parse_podcast_source_from_json(json_ps, &psmock); + parse_podcast_source_from_json(json_ps, &psmock); ASSERT_EQ(psmock.get_title_nonmock(), expected_title); ASSERT_EQ(psmock.get_episode_count(), 0); // nothing added @@ -210,7 +201,6 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_PsRecentlyUpdated) { /******************************************************************************/ TEST_F(SerializerFixture, PodcastSourceFromJson_Add2Episodes) { - PodcastSerializer dut; QDateTime invalid_date; EXPECT_CALL(psmock, get_last_updated()) @@ -220,12 +210,15 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_Add2Episodes) { QJsonObject json_ps; json_ps[KEY_TITLE] = expected_title; json_ps[KEY_TIMESTAMP] = expected_timestamp.toString(); + QJsonArray episodes_array; episodes_array.append(episode1_json); episodes_array.append(episode2_json); json_ps[KEY_EPISODES] = episodes_array; - dut.parse_podcast_source_from_json(json_ps, &psmock); + parse_podcast_source_from_json(json_ps, &psmock); + read_episodes_cache(json_ps, &psmock); + ASSERT_EQ(psmock.get_episode_count(), 2); ASSERT_EQ( psmock.get_episode_by_id(episode1_guid)->get_title(), episode1_title); @@ -235,7 +228,6 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_Add2Episodes) { /******************************************************************************/ TEST_F(SerializerFixture, PodcastSourceFromJson_UpdateEpisodePosition) { - PodcastSerializer dut; QDateTime invalid_date; EXPECT_CALL(psmock, get_last_updated()) @@ -251,14 +243,14 @@ TEST_F(SerializerFixture, PodcastSourceFromJson_UpdateEpisodePosition) { episodes_array.append(episode1_json); json_ps[KEY_EPISODES] = episodes_array; - dut.parse_podcast_source_from_json(json_ps, &psmock); + parse_podcast_source_from_json(json_ps, &psmock); + read_episodes_cache(json_ps, &psmock); ASSERT_EQ(psmock.get_episode_count(), 1); ASSERT_EQ(psmock.get_episode_by_id(episode1_guid)->get_position(), 130); } /******************************************************************************/ TEST_F(SerializerFixture, ReadFromFileInvalidTimestamp) { - PodcastSerializer dut; QString test_file_name("some_test_file.json"); /* create file to read */ QFile test_file(test_file_name); @@ -278,17 +270,12 @@ TEST_F(SerializerFixture, ReadFromFileInvalidTimestamp) { " ] \n" "} \n"; test_file.close(); - - EXPECT_CALL(psmock, get_cache_file_impl()) - .Times(1) - .WillOnce(Return(test_file_name)); - ASSERT_THROW(dut.read_from_file(&psmock), + ASSERT_THROW(read_from_file(&psmock, test_file_name), DigitalRooster::PodcastSourceJSonCorrupted); } /******************************************************************************/ TEST_F(SerializerFixture, ReadFromFileDocumentEmpty) { - PodcastSerializer dut; QString test_file_name("some_test_file.json"); /* create file to read */ QFile test_file(test_file_name); @@ -300,11 +287,11 @@ TEST_F(SerializerFixture, ReadFromFileDocumentEmpty) { EXPECT_CALL(psmock, get_cache_file_impl()) .WillRepeatedly(Return(test_file_name)); - ASSERT_THROW(dut.read_from_file(&psmock), + ASSERT_THROW(read_from_file(&psmock, test_file_name), DigitalRooster::PodcastSourceJSonCorrupted); try { - dut.read_from_file(&psmock); + read_from_file(&psmock, test_file_name); } catch (PodcastSourceJSonCorrupted& exc) { EXPECT_STREQ(exc.what(), "Document empty!"); } @@ -312,7 +299,6 @@ TEST_F(SerializerFixture, ReadFromFileDocumentEmpty) { /******************************************************************************/ TEST_F(SerializerFixture, ReadFromFile) { - PodcastSerializer dut; QString test_file_name("some_test_file.json"); /* create file to read */ QFile test_file(test_file_name); @@ -322,7 +308,7 @@ TEST_F(SerializerFixture, ReadFromFile) { " \"title\": \"foo\", \n" " \"description\": \"some fancy description\", \n" " \"icon-cached\": \"/foo/bar/baz.jpg\",\n" - " \"icon\": \"http://some.remote.com/baz.jpg\",\n" + " \"icon\": \"http://some.remote.com/baz.jpg\",\n" " \"timestamp\": \"Fr Feb 15 14:21:57 2019\", \n" " \"Episodes\": [ \n" " { \n" @@ -343,12 +329,12 @@ TEST_F(SerializerFixture, ReadFromFile) { "} \n"; test_file.close(); - dut.read_from_file(&ps, test_file_name); + read_from_file(&ps, test_file_name); ASSERT_EQ(ps.get_description(), QString("some fancy description")); ASSERT_EQ(ps.get_title(), QString("foo")); ASSERT_EQ(ps.get_image_url(), QUrl("http://some.remote.com/baz.jpg")); - ASSERT_EQ(ps.get_image_file_path(),QString("/foo/bar/baz.jpg")); + ASSERT_EQ(ps.get_image_file_path(), QString("/foo/bar/baz.jpg")); ASSERT_EQ(ps.get_episode_count(), 2); auto ep = ps.get_episode_by_id("ZZ-XX-ABV"); ASSERT_TRUE(ep); @@ -358,8 +344,7 @@ TEST_F(SerializerFixture, ReadFromFile) { /******************************************************************************/ TEST_F(SerializerFixture, FullRoundTrip) { - PodcastSerializer dut; - PodcastSourceMock_episodes psmock_episodes(cache_dir); + PodcastSourceMock_episodes psmock_episodes; QString test_file_name("FullRoundTrip_test_file.json"); QVector> ep_vec; for (int i = 0; i < 3; i++) { @@ -392,8 +377,8 @@ TEST_F(SerializerFixture, FullRoundTrip) { .WillOnce(Return(expected_timestamp)) .WillRepeatedly(Return(expected_timestamp.addSecs(3))); - dut.store_to_file(&psmock_episodes); - dut.read_from_file(&ps, test_file_name); + store_to_file(&psmock_episodes, test_file_name); + read_from_file(&ps, test_file_name); ASSERT_EQ(ps.get_title(), expected_title); ASSERT_EQ(ps.get_description(), expected_desc); @@ -404,3 +389,66 @@ TEST_F(SerializerFixture, FullRoundTrip) { } /******************************************************************************/ +TEST_F(SerializerFixture, purge_deletes_cache_file) { + QString test_file_name = + cache_dir.filePath("{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}"); + /* create file to read */ + QFile test_file(test_file_name); + test_file.open(QIODevice::ReadWrite | QIODevice::Text); + QTextStream stream(&test_file); + stream << R"( + { + "id": "{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}", + "description": "Some Description", + "icon": "https://some.remote.url/test.jpg", + "icon-cached": "/tmp/local_cache/foo.jpg", + "timestamp": "Thu Nov 14 19:48:55 2019", + "url": "https://alternativlos.org/alternativlos.rss", + "title": "MyTitle" + })"; + test_file.close(); + ASSERT_TRUE(test_file.exists()); + + PodcastSource source(QUrl("https://alternativlos.org/alternativlos.rss"), + QUuid::fromString(QString("{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}"))); + auto serializer = std::make_unique( + cache_dir, &source, std::chrono::milliseconds(50)); + + serializer->restore_info(); // populate podcastSource + source.set_serializer(std::move(serializer)); + ASSERT_EQ(source.get_title(), QString("MyTitle")); + ASSERT_TRUE(test_file.exists()); + source.purge(); + ASSERT_FALSE(test_file.exists()); +} + +/******************************************************************************/ +TEST_F(SerializerFixture, bad_cache_nothrow) { + EXPECT_CALL(*(mc.get()), get_time()) + .Times(1) + .WillOnce(Return(expected_timestamp)); + + QDir cache_dir_bad("/some/nonexistent/cache/dir"); + auto serializer = std::make_unique( + cache_dir_bad, &ps, std::chrono::seconds(1)); + ASSERT_NO_THROW(serializer->restore_info()); + ps.set_title("NewTitle"); + ASSERT_NO_THROW(serializer->write()); +} +/******************************************************************************/ +TEST(Serializer, parse_bad_timestamp) { + QString json_string(R"( + { + "id": "{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}", + "description": "Some Description", + "icon": "https://some.remote.url/test.jpg", + "icon-cached": "/tmp/local_cache/foo.jpg", + "timestamp": "19:48:55 13.05.2019", + "title": "MyTitle" + })"); + PodcastSource ps(QUrl("http://some.url")); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + EXPECT_THROW(parse_podcast_source_from_json(jdoc.object(), &ps), + PodcastSourceJSonCorrupted); +} +/******************************************************************************/ diff --git a/test/test_podcastsource.cpp b/test/test_podcastsource.cpp index 2fe926d..fa30c38 100644 --- a/test/test_podcastsource.cpp +++ b/test/test_podcastsource.cpp @@ -9,19 +9,22 @@ * SPDX-License-Identifier: GPL-3.0-or-later} * *****************************************************************************/ -#include +#include +#include #include -#include #include +#include #include // std::system_error #include #include #include -#include "appconstants.hpp" #include "PlayableItem.hpp" #include "PodcastSource.hpp" +#include "appconstants.hpp" + +#include "serializer_mock.hpp" using namespace DigitalRooster; @@ -32,8 +35,7 @@ class PodcastSourceFixture : public virtual ::testing::Test { PodcastSourceFixture() : cache_dir(DEFAULT_CACHE_DIR_PATH) , uid(QUuid::createUuid()) - , ps(QUrl("https://alternativlos.org/alternativlos.rss"), cache_dir, - uid) { + , ps(QUrl("https://alternativlos.org/alternativlos.rss"), uid) { } ~PodcastSourceFixture() { @@ -53,6 +55,7 @@ class PodcastSourceFixture : public virtual ::testing::Test { PodcastSource ps; }; + /******************************************************************************/ TEST_F(PodcastSourceFixture, dont_add_twice) { auto pi = @@ -79,10 +82,14 @@ TEST_F(PodcastSourceFixture, add_episodeEmitsCountChanged) { auto pi1 = std::make_shared("TheName", QUrl("http://foo.bar")); QSignalSpy spy(&ps, SIGNAL(episodes_count_changed(int))); + QSignalSpy spy_data(&ps, SIGNAL(dataChanged())); + ASSERT_TRUE(spy.isValid()); + ASSERT_TRUE(spy_data.isValid()); + ps.add_episode(pi1); - spy.wait(300); ASSERT_EQ(spy.count(), 1); + ASSERT_EQ(spy_data.count(), 0); // should not emit dataChanged // arguments of first signal, here only 1 int // QSignalSpy inherits from QList> QList arguments = spy.takeFirst(); @@ -133,26 +140,27 @@ TEST_F(PodcastSourceFixture, set_updater) { /******************************************************************************/ TEST_F(PodcastSourceFixture, getFileName) { auto filename = ps.get_cache_file_name(); - QString expected_filename(cache_dir.filePath(uid.toString())); - + QString expected_filename(uid.toString()); ASSERT_EQ(filename, expected_filename); } /******************************************************************************/ -TEST_F(PodcastSourceFixture, storeAndPurgeworks) { - auto filename = ps.get_cache_file_name(); - QFile cachefile(filename); - ASSERT_FALSE(cachefile.exists()); +TEST_F(PodcastSourceFixture, emit_Description_Title_Changed) { + QSignalSpy spy_desc(&ps, SIGNAL(descriptionChanged())); + QSignalSpy spy_data(&ps, SIGNAL(dataChanged())); + QSignalSpy spy_title(&ps, SIGNAL(titleChanged())); + ASSERT_TRUE(spy_desc.isValid()); + ASSERT_TRUE(spy_data.isValid()); + ASSERT_TRUE(spy_title.isValid()); + + ps.set_title("NewTitle"); ps.set_description("MyDescription"); - auto first = - std::make_shared("TheName", QUrl("http://foo.bar")); - ps.add_episode(first); - ps.store(); - ASSERT_TRUE(cachefile.exists()); - ps.purge(); - ASSERT_FALSE(cachefile.exists()); - ASSERT_EQ(ps.get_episode_count(), 0); + + ASSERT_EQ(spy_desc.count(), 1); + ASSERT_EQ(spy_data.count(), 2); + ASSERT_EQ(spy_title.count(), 1); } + /******************************************************************************/ TEST_F(PodcastSourceFixture, storeIconCache) { auto image_url = QUrl( @@ -168,7 +176,7 @@ TEST_F(PodcastSourceFixture, storeIconCache) { ASSERT_EQ(spy.count(), 1); // Second time the local cache should be returned auto expeced_local_url = QUrl::fromLocalFile(cache_dir.filePath(file_name)); - ASSERT_EQ(ps.get_icon(),expeced_local_url); + ASSERT_EQ(ps.get_icon(), expeced_local_url); ASSERT_TRUE(cache_file.exists()); ASSERT_TRUE(cache_file.remove()); @@ -196,12 +204,67 @@ TEST_F(PodcastSourceFixture, purgeIconCache) { } /******************************************************************************/ -TEST(PodcastSource, store_bad_nothrow) { - QDir cache_dir_bad("/some/nonexistent/cache/dir"); - PodcastSource ps(QUrl("http://foo.bar"), cache_dir_bad); - ps.set_description("MyDescription"); - auto first = - std::make_shared("TheName", QUrl("http://foo.bar")); - ps.add_episode(first); - ASSERT_NO_THROW(ps.store()); +TEST_F(PodcastSourceFixture, purge_with_serializer) { + auto serializer = std::make_unique(cache_dir, &ps); + EXPECT_CALL(*(serializer.get()), delete_cache()).Times(1); + QSignalSpy spy(&ps, SIGNAL(episodes_count_changed(int))); + ps.set_serializer(std::move(serializer)); + ps.purge(); + EXPECT_EQ(spy.count(), 1); + auto argslist = spy.takeFirst(); + EXPECT_EQ(argslist[0].toInt(), 0); +} + +/******************************************************************************/ +TEST_F(PodcastSourceFixture, episode_position_triggers_delayed_write) { + auto serializer = std::make_unique( + cache_dir, &ps, std::chrono::milliseconds(50)); + EXPECT_CALL(*(serializer.get()), write_cache()).Times(1); + QSignalSpy spy(&ps, SIGNAL(dataChanged())); + ps.set_serializer(std::move(serializer)); + auto episode = std::make_shared( + "TestEpisode", QUrl("http://some.url")); + episode->set_duration(100000); + ps.add_episode(episode); + episode->set_position(4000); + spy.wait(1000); + EXPECT_EQ(spy.count(), 1); +} + +/******************************************************************************/ +TEST(PodcastSource, from_json_object_good) { + QString json_string(R"( + { + "id": "{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}", + "description": "Some Description", + "icon": "https://some.remote.url/test.jpg", + "icon-cached": "/tmp/local_cache/foo.jpg", + "timestamp": "Thu Nov 14 19:48:55 2019", + "url": "https://alternativlos.org/alternativlos.rss", + "title": "MyTitle" + })"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + auto ps = PodcastSource::from_json_object(jdoc.object()); + EXPECT_EQ(ps->get_id(), QString("5c81821d-17fc-44d5-ae45-5ab24ffd1d50}")); + EXPECT_EQ(ps->get_title(), QString("MyTitle")); + EXPECT_EQ( + ps->get_url(), QUrl("https://alternativlos.org/alternativlos.rss")); + EXPECT_EQ(ps->get_description(), QString("Some Description")); + EXPECT_EQ(ps->get_icon(), QString("https://some.remote.url/test.jpg")); +} + +/******************************************************************************/ +TEST(PodcastSource, from_json_object_bad_url) { + QString json_string(R"( + { + "id": "{5c81821d-17fc-44d5-ae45-5ab24ffd1d50}", + "description": "Some Description", + "icon": "https://some.remote.url/test.jpg", + "icon-cached": "/tmp/local_cache/foo.jpg", + "timestamp": "Thu Nov 14 19:48:55 2019", + "title": "MyTitle" + })"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + EXPECT_THROW( + PodcastSource::from_json_object(jdoc.object()), std::invalid_argument); } diff --git a/test/test_settings.cpp b/test/test_settings.cpp index 9c92b64..eea8e19 100644 --- a/test/test_settings.cpp +++ b/test/test_settings.cpp @@ -105,6 +105,7 @@ class SettingsFixture : public virtual ::testing::Test { al1[KEY_ALARM_PERIOD] = "daily"; al1[KEY_ENABLED] = true; al1[KEY_ID] = "1a4bf6bd-7e67-4b40-80fd-b13e2524fc74"; + alarms.append(al1); QJsonObject al2; al2[KEY_TIME] = "07:00"; @@ -112,6 +113,7 @@ class SettingsFixture : public virtual ::testing::Test { al2[KEY_ALARM_PERIOD] = "workdays"; al2[KEY_ENABLED] = true; al2[KEY_ID] = "12eb4390-6abf-4626-be48-f11fe20f45cf"; + alarms.append(al2); QJsonObject al3; al3[KEY_TIME] = "09:00"; @@ -119,6 +121,7 @@ class SettingsFixture : public virtual ::testing::Test { al3[KEY_ALARM_PERIOD] = "weekend"; al3[KEY_ENABLED] = false; al3[KEY_ID] = "62ab05d7-d9ab-4254-8bfd-47bfdc74417a"; + alarms.append(al3); QJsonObject al4; al4[KEY_TIME] = "13:00"; @@ -126,6 +129,7 @@ class SettingsFixture : public virtual ::testing::Test { al4[KEY_ALARM_PERIOD] = "once"; al4[KEY_ENABLED] = true; al4[KEY_ID] = "fa3ce587-ab02-4328-9c68-4ee5e3626c86"; + alarms.append(al4); QJsonObject al5; al5[KEY_TIME] = "17:00"; @@ -133,13 +137,24 @@ class SettingsFixture : public virtual ::testing::Test { al5[KEY_ALARM_PERIOD] = "Manchmal"; al5[KEY_ENABLED] = true; al5[KEY_ID] = "694485e9-ac44-46f5-bc45-730a7a0ac387"; - - alarms.append(al1); - alarms.append(al2); - alarms.append(al3); - alarms.append(al4); alarms.append(al5); + QJsonObject al6; + al6[KEY_TIME] = "25:34"; + al6[KEY_URI] = "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3"; + al6[KEY_ALARM_PERIOD] = "once"; + al6[KEY_ENABLED] = true; + al6[KEY_ID] = "694485e9-ac44-46f5-bc45-730a7a0a2387"; + alarms.append(al6); + + QJsonObject al7; + al7[KEY_TIME] = "12:34"; + al7[KEY_URI] = ""; + al7[KEY_ALARM_PERIOD] = "once"; + al7[KEY_ENABLED] = true; + al7[KEY_ID] = "694485e9-ac44-46f5-bc45-730a7a0a2387"; + alarms.append(al7); + root[KEY_GROUP_ALARMS] = alarms; } @@ -194,8 +209,7 @@ TEST_F(SettingsFixture, add_podcast_source) { QSignalSpy spy(cm.get(), SIGNAL(podcast_sources_changed())); ASSERT_TRUE(spy.isValid()); auto ps = std::make_shared( - QUrl("https://alternativlos.org/alternativlos.rss"), - QDir(cm->get_cache_path())); + QUrl("https://alternativlos.org/alternativlos.rss")); auto size_before = cm->get_podcast_sources().size(); cm->add_podcast_source(ps); ASSERT_EQ(spy.count(), 1); @@ -357,7 +371,10 @@ TEST(StringToPeriodEnum, mapping_good) { /*****************************************************************************/ TEST_F(SettingsFixture, alarm_count) { auto& v = cm->get_alarms(); - ASSERT_EQ(v.size(), 5); + // Alarm 5 has an unknown peridicity string "Manchmal" + // Alarm 6 has an invalid Timestamp string "25:34" + // Alarm 7 has an invalid URL + ASSERT_EQ(v.size(), 4); } /*****************************************************************************/ @@ -431,14 +448,6 @@ TEST_F(SettingsFixture, alarm_once) { ASSERT_TRUE(v[3]->is_enabled()); } -/*****************************************************************************/ -TEST_F(SettingsFixture, alarm_once_default) { - auto& v = cm->get_alarms(); - // Alarm 5 has an unknown peridicity string "Manchmal" it should default to - // Daily - ASSERT_EQ(v[4]->get_period(), Alarm::Daily); -} - /*****************************************************************************/ TEST_F(SettingsFixture, emitConfigChanged) { auto number_of_alarms = cm->get_alarms().size(); @@ -492,7 +501,8 @@ TEST(ConfigManager, DefaultForNotWritableCache) { /*****************************************************************************/ TEST(ConfigManager, DefaultForNotWritableConfig) { QFile default_conf_file(DEFAULT_CONFIG_FILE_PATH); - ASSERT_TRUE(default_conf_file.remove()); + // Delete it should it exist.. + default_conf_file.remove(); ConfigurationManager cm( QString("/dev/foobar.json"), DEFAULT_CACHE_DIR_PATH); ASSERT_TRUE(default_conf_file.exists()); @@ -501,13 +511,13 @@ TEST(ConfigManager, DefaultForNotWritableConfig) { /*****************************************************************************/ TEST_F(SettingsFixture, GetweatherConfigApiToken) { auto cfg = cm->get_weather_config(); - ASSERT_EQ(cfg.apikey, QString("d77bd1ca2fd77ce4e1cdcdd5f8b7206c")); + ASSERT_EQ(cfg->get_api_token(), QString("d77bd1ca2fd77ce4e1cdcdd5f8b7206c")); } /*****************************************************************************/ TEST_F(SettingsFixture, GetweatherConfigCityId) { auto cfg = cm->get_weather_config(); - ASSERT_EQ(cfg.cityid, QString("3452925")); + ASSERT_EQ(cfg->get_location_id(), QString("3452925")); } /*****************************************************************************/ diff --git a/test/test_update_task.cpp b/test/test_update_task.cpp index da4b34f..cf49150 100644 --- a/test/test_update_task.cpp +++ b/test/test_update_task.cpp @@ -26,7 +26,7 @@ TEST(TestDownload, parsed) { QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); PodcastSource ps( - QUrl("https://alternativlos.org/alternativlos.rss"), cache_dir); + QUrl("https://alternativlos.org/alternativlos.rss")); QSignalSpy spy(&ps, SIGNAL(titleChanged())); UpdateTask task(&ps); ASSERT_TRUE(spy.wait()); @@ -39,7 +39,7 @@ TEST(TestDownload, donotReEmitEpisodesChanged) { QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); PodcastSource ps( - QUrl("https://alternativlos.org/alternativlos.rss"), cache_dir); + QUrl("https://alternativlos.org/alternativlos.rss")); QSignalSpy spy(&ps, SIGNAL(titleChanged())); UpdateTask task(&ps); spy.wait(1000); diff --git a/test/test_weather.cpp b/test/test_weather.cpp index 471659b..fb601be 100644 --- a/test/test_weather.cpp +++ b/test/test_weather.cpp @@ -36,33 +36,57 @@ class WeatherFile : public virtual ::testing::Test { make_error_code(std::errc::no_such_file_or_directory), weatherFile.errorString().toStdString()); } - } + }; protected: QFile weatherFile; }; + /*****************************************************************************/ -TEST(Weather, GetConfigForDownloadAfterTimerExpired) { +TEST(Weather, RefreshEmitsSignal) { auto cm = std::make_shared(); + auto weather_cfg = DigitalRooster::WeatherConfig( + QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); EXPECT_CALL(*(cm.get()), get_weather_cfg()) - .Times(1) - .WillRepeatedly(ReturnRef(cm->weather_cfg)); + .Times(AtLeast(1)) + .WillRepeatedly(Return(&weather_cfg)); Weather dut(cm); - dut.set_update_interval(seconds(1)); + QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); + ASSERT_TRUE(spy.isValid()); + spy.wait(500); + dut.refresh(); + spy.wait(1500); // make sure we have enough time to download info + ASSERT_EQ(spy.count(), 2); +} +/*****************************************************************************/ +TEST(Weather, GetConfigForDownloadAfterTimerExpired) { + auto cm = std::make_shared(); + auto weather_cfg = DigitalRooster::WeatherConfig( + QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); + EXPECT_CALL(*(cm.get()), get_weather_cfg()) + .Times(AtLeast(1)) + .WillRepeatedly(Return(&weather_cfg)); + + Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); ASSERT_TRUE(spy.isValid()); + dut.set_update_interval(seconds(1)); + ASSERT_EQ(dut.get_update_interval(), std::chrono::seconds(1)); + spy.wait(500); // first download spy.wait(1500); // only to make sure time elapses during test - // ASSERT_EQ(spy.count(), 1); } /*****************************************************************************/ TEST_F(WeatherFile, ParseTemperatureFromFile) { auto cm = std::make_shared(); + auto weather_cfg = + DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); + EXPECT_CALL(*(cm.get()), get_weather_cfg()) .Times(1) - .WillRepeatedly(ReturnRef(cm->weather_cfg)); + .WillRepeatedly(Return(&weather_cfg)); Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); @@ -74,11 +98,14 @@ TEST_F(WeatherFile, ParseTemperatureFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, GetCityFromFile) { auto cm = std::make_shared(); + auto weather_cfg = + DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); + EXPECT_CALL(*(cm.get()), get_weather_cfg()) .Times(1) - .WillRepeatedly(ReturnRef(cm->weather_cfg)); - - Weather dut(cm); + .WillRepeatedly(Return(&weather_cfg)); + + Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); dut.parse_response(weatherFile.readAll()); @@ -90,9 +117,12 @@ TEST_F(WeatherFile, GetCityFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, ParseConditionFromFile) { auto cm = std::make_shared(); + auto weather_cfg = + DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); + EXPECT_CALL(*(cm.get()), get_weather_cfg()) .Times(1) - .WillRepeatedly(ReturnRef(cm->weather_cfg)); + .WillRepeatedly(Return(&weather_cfg)); Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(condition_changed(const QString&))); @@ -105,15 +135,62 @@ TEST_F(WeatherFile, ParseConditionFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, IconURI) { auto cm = std::make_shared(); + auto weather_cfg = + DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); + EXPECT_CALL(*(cm.get()), get_weather_cfg()) .Times(1) - .WillRepeatedly(ReturnRef(cm->weather_cfg)); + .WillRepeatedly(Return(&weather_cfg)); Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); dut.parse_response(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_weather_icon_url(), - QUrl("http://openweathermap.org/img/w/02d.png")); + ASSERT_EQ(dut.get_weather_icon_url(), + QUrl("http://openweathermap.org/img/w/02d.png")); +} +/*****************************************************************************/ +TEST(WeatherCfg, fromJsonGood) { + auto json_string = QString(R"( + { + "API-Key": "Secret", + "locationID": "ABCD", + "updateInterval": 123 + } + )"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + + auto dut = WeatherConfig::from_json_object(jdoc.object()); + ASSERT_EQ(dut->get_api_token(), QString("Secret")); + ASSERT_EQ(dut->get_location_id(), QString("ABCD")); + ASSERT_EQ(dut->get_update_interval(), std::chrono::seconds(123)); } +/*****************************************************************************/ +TEST(WeatherCfg, throwEmptyLocation) { + auto json_string = QString(R"( + { + "API-Key": "Secret", + "locationID": "", + "updateInterval": 123 + } + )"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + + ASSERT_THROW( + WeatherConfig::from_json_object(jdoc.object()), std::invalid_argument); +} +/*****************************************************************************/ +TEST(WeatherCfg, throwNoApiToken) { + auto json_string = QString(R"( + { + "locationID": "ABCD", + "updateInterval": 123 + } + )"); + auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); + + ASSERT_THROW( + WeatherConfig::from_json_object(jdoc.object()), std::invalid_argument); +} +/*****************************************************************************/ From dafc9a8fb598fc48675a995023b41bc4f1d2251d Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 15 Dec 2019 14:43:19 +0100 Subject: [PATCH 12/26] Fix Codacity findings --- include/PodcastSource.hpp | 2 +- include/configuration_manager.hpp | 10 +++++----- libsrc/PodcastSource.cpp | 4 +--- libsrc/configuration_manager.cpp | 18 +++++++++--------- libsrc/weather.cpp | 1 + test/serializer_mock.hpp | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/include/PodcastSource.hpp b/include/PodcastSource.hpp index 6a2b00c..1a4e908 100644 --- a/include/PodcastSource.hpp +++ b/include/PodcastSource.hpp @@ -51,7 +51,7 @@ class PodcastSource : public QObject { * @param url Feed URL * @param uid unique id for this podcast */ - PodcastSource(const QUrl& url, QUuid uid = QUuid::createUuid()); + explicit PodcastSource(const QUrl& url, QUuid uid = QUuid::createUuid()); /** * Destructor to delete icon_downloader nicely diff --git a/include/configuration_manager.hpp b/include/configuration_manager.hpp index 953b6bd..f803bfe 100644 --- a/include/configuration_manager.hpp +++ b/include/configuration_manager.hpp @@ -404,24 +404,24 @@ public slots: virtual void parse_json(const QByteArray& json); /** - * Fills the vector stream_sources with entries form settings file + * Fills the vector stream_sources */ - virtual void read_radio_streams_from_file(const QJsonObject& appconfig); + virtual void read_radio_streams(const QJsonObject& appconfig); /** * Read all podcast sources form configuration file */ - virtual void read_podcasts_from_file(const QJsonObject& appconfig); + virtual void read_podcasts(const QJsonObject& appconfig); /** * Read Alarm objects */ - virtual void read_alarms_from_file(const QJsonObject& appconfig); + virtual void read_alarms(const QJsonObject& appconfig); /** * Read weatherconfig */ - virtual void read_weather_from_file(const QJsonObject& appconfig); + virtual void read_weather(const QJsonObject& appconfig); /** * Store settings permanently to file diff --git a/libsrc/PodcastSource.cpp b/libsrc/PodcastSource.cpp index bd44e11..696b059 100644 --- a/libsrc/PodcastSource.cpp +++ b/libsrc/PodcastSource.cpp @@ -302,9 +302,7 @@ std::shared_ptr PodcastSource::from_json_object( if (!url.isValid()) { throw std::invalid_argument("invalid URL for podcast"); } - auto ps = std::make_shared(url, uid); - auto title = json[KEY_TITLE].toString(); auto desc = json[KEY_DESCRIPTION].toString(); auto img_url = json[KEY_ICON_URL].toString(); @@ -312,7 +310,7 @@ std::shared_ptr PodcastSource::from_json_object( ps->set_title(title); ps->set_description(desc); ps->set_image_url(img_url); - + ps->set_image_file_path(img_cached); ps->set_update_interval( std::chrono::seconds(json[KEY_UPDATE_INTERVAL].toInt(3600))); ps->set_update_task(std::make_unique(ps.get())); diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index 0d922bf..73ef2e1 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -15,13 +15,13 @@ #include #include #include + #include #include #include #include "alarm.hpp" #include "appconstants.hpp" - #include "UpdateTask.hpp" #include "configuration_manager.hpp" @@ -190,14 +190,14 @@ void ConfigurationManager::parse_json(const QByteArray& json) { auto wpa_sock = appconfig[KEY_WPA_SOCKET_NAME]; wpa_socket_name = wpa_sock.toString(WPA_CONTROL_SOCKET_PATH); - read_radio_streams_from_file(appconfig); - read_podcasts_from_file(appconfig); - read_alarms_from_file(appconfig); - read_weather_from_file(appconfig); + read_radio_streams(appconfig); + read_podcasts(appconfig); + read_alarms(appconfig); + read_weather(appconfig); } /*****************************************************************************/ -void ConfigurationManager::read_radio_streams_from_file( +void ConfigurationManager::read_radio_streams( const QJsonObject& appconfig) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray stations = @@ -223,7 +223,7 @@ void ConfigurationManager::read_radio_streams_from_file( } /*****************************************************************************/ -void ConfigurationManager::read_podcasts_from_file( +void ConfigurationManager::read_podcasts( const QJsonObject& appconfig) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray podcasts = @@ -253,7 +253,7 @@ void ConfigurationManager::read_podcasts_from_file( } /*****************************************************************************/ -void ConfigurationManager::read_alarms_from_file(const QJsonObject& appconfig) { +void ConfigurationManager::read_alarms(const QJsonObject& appconfig) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray alarm_config = appconfig[DigitalRooster::KEY_GROUP_ALARMS].toArray(); @@ -272,7 +272,7 @@ void ConfigurationManager::read_alarms_from_file(const QJsonObject& appconfig) { } /*****************************************************************************/ -void ConfigurationManager::read_weather_from_file( +void ConfigurationManager::read_weather( const QJsonObject& appconfig) { if (appconfig[KEY_WEATHER].isNull()) { qCWarning(CLASS_LC) << "no weather configuration found!"; diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index 933c102..d81f028 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -88,6 +88,7 @@ QUrl DigitalRooster::create_weather_uri(const WeatherConfig* cfg) { request_str += "&units=metric"; request_str += "&appid="; request_str += cfg->get_api_token(); + request_str += "&lang=en"; //default english return QUrl(request_str); } /*****************************************************************************/ diff --git a/test/serializer_mock.hpp b/test/serializer_mock.hpp index ede2c00..c5e173b 100644 --- a/test/serializer_mock.hpp +++ b/test/serializer_mock.hpp @@ -21,7 +21,7 @@ /******************************************************************************/ class SerializerMock : public DigitalRooster::PodcastSerializer { public: - SerializerMock(const QDir& app_cache_dir, + explicit SerializerMock(const QDir& app_cache_dir, DigitalRooster::PodcastSource* source = nullptr, std::chrono::milliseconds delay = std::chrono::milliseconds(100)) : PodcastSerializer(app_cache_dir, source, delay){}; From e1021be057aef3971e3c708a206f2b0d4dde5d7a Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 15 Dec 2019 14:52:00 +0100 Subject: [PATCH 13/26] Fix set_positon on remote media When media was not loaded quickly enough set_positon did not only have no effect. but positon of PodcastEpisode was actually reset to 0. --- include/PlayableItem.hpp | 2 +- libsrc/mediaplayerproxy.cpp | 15 +++++++++++++++ test/test_mediaplayerproxy.cpp | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/PlayableItem.hpp b/include/PlayableItem.hpp index 487b759..a35467c 100644 --- a/include/PlayableItem.hpp +++ b/include/PlayableItem.hpp @@ -15,8 +15,8 @@ #include #include -#include #include +#include #include #include #include diff --git a/libsrc/mediaplayerproxy.cpp b/libsrc/mediaplayerproxy.cpp index a514b8f..2dba4c7 100644 --- a/libsrc/mediaplayerproxy.cpp +++ b/libsrc/mediaplayerproxy.cpp @@ -22,8 +22,12 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy"); +<<<<<<< HEAD /***********************************************************************/ +======= +/****************************************************************************/ +>>>>>>> Fix set_positon on remote media MediaPlayerProxy::MediaPlayerProxy() : backend(std::make_unique()) { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -43,9 +47,14 @@ MediaPlayerProxy::MediaPlayerProxy() qCDebug(CLASS_LC) << "MediaPlayerProxy position_changed()" << position; emit position_changed(position); +<<<<<<< HEAD /** Only update position */ if (current_item.get() != nullptr && position_updateable) { +======= + /** Only update position of seekable media */ + if (current_item.get() != nullptr && current_item->is_seekable()) { +>>>>>>> Fix set_positon on remote media current_item->set_position(position); } }); @@ -77,11 +86,17 @@ MediaPlayerProxy::MediaPlayerProxy() qCDebug(CLASS_LC) << "MediaPlayerProxy seekable_changed()" << seekable; current_item->set_seekable(seekable); +<<<<<<< HEAD enable_position_update(seekable); emit seekable_changed(seekable); /* jump to previously saved position once we know we can seek */ if (seekable) { +======= + emit seekable_changed(seekable); + /* jump to previously saved position once we know we can seek */ + if (current_item->is_seekable()) { +>>>>>>> Fix set_positon on remote media /* update backend position */ set_position(current_item->get_position()); } diff --git a/test/test_mediaplayerproxy.cpp b/test/test_mediaplayerproxy.cpp index ef6611b..cbd0d35 100644 --- a/test/test_mediaplayerproxy.cpp +++ b/test/test_mediaplayerproxy.cpp @@ -250,9 +250,15 @@ TEST_F(PlayerFixture, setPositionRemote) { * available */ EXPECT_GE(remote_audio->get_position(), 10000); dut.stop(); +<<<<<<< HEAD spy_playing.wait(100); /* Media position should not have been reset to < 10000 when media was not * available */ +======= + spy_playing.wait(200); + ASSERT_EQ( + spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::StoppedState); +>>>>>>> Fix set_positon on remote media EXPECT_GE(remote_audio->get_position(), 10000); } /*****************************************************************************/ From 2a77a4e73124abda40e8e82e03b8bbaa59e06e3c Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 15 Dec 2019 14:59:48 +0100 Subject: [PATCH 14/26] rebase develop --- libsrc/mediaplayer.cpp | 2 +- libsrc/mediaplayerproxy.cpp | 18 +----------------- test/test_mediaplayerproxy.cpp | 8 +------- 3 files changed, 3 insertions(+), 25 deletions(-) diff --git a/libsrc/mediaplayer.cpp b/libsrc/mediaplayer.cpp index 08e85e0..bd14c3d 100644 --- a/libsrc/mediaplayer.cpp +++ b/libsrc/mediaplayer.cpp @@ -15,7 +15,7 @@ using namespace DigitalRooster; -static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayer") +static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayer"); /*****************************************************************************/ bool MediaPlayer::muted() const { diff --git a/libsrc/mediaplayerproxy.cpp b/libsrc/mediaplayerproxy.cpp index 2dba4c7..609adfb 100644 --- a/libsrc/mediaplayerproxy.cpp +++ b/libsrc/mediaplayerproxy.cpp @@ -22,12 +22,7 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.MediaPlayerProxy"); -<<<<<<< HEAD -/***********************************************************************/ - -======= /****************************************************************************/ ->>>>>>> Fix set_positon on remote media MediaPlayerProxy::MediaPlayerProxy() : backend(std::make_unique()) { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -47,14 +42,9 @@ MediaPlayerProxy::MediaPlayerProxy() qCDebug(CLASS_LC) << "MediaPlayerProxy position_changed()" << position; emit position_changed(position); -<<<<<<< HEAD - /** Only update position */ - if (current_item.get() != nullptr && position_updateable) { -======= /** Only update position of seekable media */ - if (current_item.get() != nullptr && current_item->is_seekable()) { ->>>>>>> Fix set_positon on remote media + if (current_item && position_updateable) { current_item->set_position(position); } }); @@ -86,17 +76,11 @@ MediaPlayerProxy::MediaPlayerProxy() qCDebug(CLASS_LC) << "MediaPlayerProxy seekable_changed()" << seekable; current_item->set_seekable(seekable); -<<<<<<< HEAD enable_position_update(seekable); emit seekable_changed(seekable); /* jump to previously saved position once we know we can seek */ if (seekable) { -======= - emit seekable_changed(seekable); - /* jump to previously saved position once we know we can seek */ - if (current_item->is_seekable()) { ->>>>>>> Fix set_positon on remote media /* update backend position */ set_position(current_item->get_position()); } diff --git a/test/test_mediaplayerproxy.cpp b/test/test_mediaplayerproxy.cpp index cbd0d35..6cd96dd 100644 --- a/test/test_mediaplayerproxy.cpp +++ b/test/test_mediaplayerproxy.cpp @@ -214,6 +214,7 @@ TEST_F(PlayerFixture, checkErrorStates) { EXPECT_EQ(signal_params.at(0).toInt(), QMediaPlayer::NoMedia); EXPECT_EQ(dut.error(), QMediaPlayer::NoMedia); } + /*****************************************************************************/ TEST_F(PlayerFixture, setPositionRemote) { remote_audio->set_position(10000); @@ -250,15 +251,8 @@ TEST_F(PlayerFixture, setPositionRemote) { * available */ EXPECT_GE(remote_audio->get_position(), 10000); dut.stop(); -<<<<<<< HEAD - spy_playing.wait(100); /* Media position should not have been reset to < 10000 when media was not * available */ -======= - spy_playing.wait(200); - ASSERT_EQ( - spy_playing.takeFirst().at(0).toInt(), QMediaPlayer::StoppedState); ->>>>>>> Fix set_positon on remote media EXPECT_GE(remote_audio->get_position(), 10000); } /*****************************************************************************/ From 10a4c76ef39766b719eb9b5e2caaaf888d31b2f2 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Mon, 10 Feb 2020 20:15:04 +0100 Subject: [PATCH 15/26] Feature/technical debt 1 (#19) Pay first installment on technical debt - Introduce interfaces instead of making classes dependent on global ConfigurationManager. - Do not own ConfigurationManager or interface by std::shared_ptr. There is no use in shared ownership here. A simple reference is enough. - Initialize Alarm with empty media. - Cleanup includes Signed-off-by: Thomas Ruschival --- include/IAlarmStore.hpp | 56 +++++++ include/IBrightnessStore.hpp | 45 ++++++ include/IPodcastStore.hpp | 77 ++++++++++ include/IStationStore.hpp | 58 +++++++ include/ITimeoutStore.hpp | 49 ++++++ include/IWeatherConfigStore.hpp | 106 +++++++++++++ include/alarm.hpp | 25 ++-- include/alarmdispatcher.hpp | 14 +- include/alarmmonitor.hpp | 12 +- include/brightnesscontrol.hpp | 11 +- include/configuration_manager.hpp | 241 ++++++++---------------------- include/mediaplayer.hpp | 6 + include/podcast_serializer.hpp | 2 - include/sleeptimer.hpp | 13 +- include/weather.hpp | 73 +-------- libsrc/alarm.cpp | 39 +++-- libsrc/alarmdispatcher.cpp | 20 +-- libsrc/alarmmonitor.cpp | 22 +-- libsrc/brightnesscontrol.cpp | 15 +- libsrc/configuration_manager.cpp | 212 ++++++++++++++++---------- libsrc/sleeptimer.cpp | 18 ++- libsrc/weather.cpp | 20 +-- qtgui/alarmlistmodel.cpp | 79 +++++----- qtgui/alarmlistmodel.hpp | 22 ++- qtgui/iradiolistmodel.cpp | 37 ++--- qtgui/iradiolistmodel.hpp | 23 +-- qtgui/main.cpp | 64 ++++---- qtgui/podcastepisodemodel.cpp | 14 +- qtgui/podcastepisodemodel.hpp | 24 ++- qtgui/podcastsourcemodel.cpp | 32 ++-- qtgui/podcastsourcemodel.hpp | 27 ++-- qtgui/qml/AlarmEditDialog.qml | 2 +- test/cm_mock.hpp | 13 +- test/test_alarmdispatcher.cpp | 155 +++++++++---------- test/test_alarmmonitor.cpp | 43 +++--- test/test_brightness.cpp | 32 ++-- test/test_settings.cpp | 104 +++++++------ test/test_sleeptimer.cpp | 31 ++-- test/test_weather.cpp | 58 ++++--- 39 files changed, 1086 insertions(+), 808 deletions(-) create mode 100644 include/IAlarmStore.hpp create mode 100644 include/IBrightnessStore.hpp create mode 100644 include/IPodcastStore.hpp create mode 100644 include/IStationStore.hpp create mode 100644 include/ITimeoutStore.hpp create mode 100644 include/IWeatherConfigStore.hpp diff --git a/include/IAlarmStore.hpp b/include/IAlarmStore.hpp new file mode 100644 index 0000000..5374f26 --- /dev/null +++ b/include/IAlarmStore.hpp @@ -0,0 +1,56 @@ +/****************************************************************************** + * \filename + * \brief Interface for creating/update and deletion of alams + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_IALARMSTORE_HPP_ +#define INCLUDE_IALARMSTORE_HPP_ + +#include +#include + +namespace DigitalRooster { +class Alarm; + +class IAlarmStore { +public: + /** + * Append new alarm to list + * @param alarm + */ + virtual void add_alarm(std::shared_ptr alarm) = 0; + /** + * Delete an alarm identified by ID from the list of alarms + * @param id of alarm + * @throws std::out_of_range if not found + */ + virtual void delete_alarm(const QUuid& id) = 0; + + /** + * Get a alarm identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of Alarm + * @return Alarm + */ + virtual const Alarm* get_alarm(const QUuid& id) const = 0; + + /** + * get Alarm List + */ + virtual const QVector>& get_alarms() const = 0; + /** + * virtual destructor + */ + virtual ~IAlarmStore(){}; +}; + + +} // namespace DigitalRooster + +#endif /* INCLUDE_IALARMSTORE_HPP_ */ diff --git a/include/IBrightnessStore.hpp b/include/IBrightnessStore.hpp new file mode 100644 index 0000000..4f9ddfa --- /dev/null +++ b/include/IBrightnessStore.hpp @@ -0,0 +1,45 @@ +/****************************************************************************** + * \filename + * \brief Interface for update/access of Brightness Settings + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_IBRIGHTNESSTORE_HPP_ +#define INCLUDE_IBRIGHTNESSTORE_HPP_ + +#include + +namespace DigitalRooster { +/** + * Interface for addition, access of Brightness Settings + * actual storage \ref DigitalRooster::ConfigurationManager + */ +class IBrightnessStore { +public: + virtual int get_standby_brightness() const =0; + virtual int get_active_brightness() const =0; + /** + * user changed standby brightness + * @param brightness new volume settings (0..100) + */ + virtual void set_standby_brightness(int brightness) =0; + + /** + * user changed standby brightness + * @param brightness new volume settings (0..100) + */ + virtual void set_active_brightness(int brightness) =0; + + /** + * virtual destructor + */ + virtual ~IBrightnessStore(){}; +}; +} // namespace DigitalRooster + +#endif /* INCLUDE_IBRIGHTNESSTORE_HPP_ */ diff --git a/include/IPodcastStore.hpp b/include/IPodcastStore.hpp new file mode 100644 index 0000000..132595d --- /dev/null +++ b/include/IPodcastStore.hpp @@ -0,0 +1,77 @@ +/****************************************************************************** + * \filename + * \brief Interface for creating/update and deletion Internet radio stations + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_IPODSCASTSTORE_HPP_ +#define INCLUDE_IPODSCASTSTORE_HPP_ + +#include +#include + +namespace DigitalRooster { +class PodcastSource; + +/** + * Interface for addition, access and deletion of Podcast sources of + * actual storage \ref DigitalRooster::ConfigurationManager + */ +class IPodcastStore { +public: + /** + * Append new PodcastSource to list + * @param podcast source + */ + virtual void add_podcast_source(std::shared_ptr podcast) = 0; + + /** + * Delete a podcast source identified by id form the list of sources + * @param id unique id of podcast source + * @throws std::out_of_range if not found + */ + virtual void delete_podcast_source(const QUuid& id) = 0; + /** + * Get a single podcast source identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of podcast + * @return source + */ + virtual const PodcastSource* get_podcast_source(const QUuid& id) const = 0; + + /** + * get all podcast sources + */ + virtual const QVector>& + get_podcast_sources() const = 0; + + /** + * Get a single podcast source identified by index + * @throws std::out_of_range if not found + * @param index in vector + * @return PodastSource + */ + virtual PodcastSource* get_podcast_source_by_index(int index) const = 0; + + /** + * Removes a podcast source entry form list + * @throws std::out_of_range if not found + * @param index in vector + */ + virtual void remove_podcast_source_by_index(int index) = 0; + + /** + * virtual destructor + */ + virtual ~IPodcastStore(){}; +}; + + +} // namespace DigitalRooster + +#endif /* INCLUDE_IPODSCASTSTORE_HPP_ */ diff --git a/include/IStationStore.hpp b/include/IStationStore.hpp new file mode 100644 index 0000000..e3ab100 --- /dev/null +++ b/include/IStationStore.hpp @@ -0,0 +1,58 @@ +/****************************************************************************** + * \filename + * \brief Interface for creating/update and deletion Internet radio stations + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_ISTATIONSTORE_HPP_ +#define INCLUDE_ISTATIONSTORE_HPP_ + +#include +#include + +namespace DigitalRooster { +class PlayableItem; + +class IStationStore { +public: + /** + * Append the radio stream to list - duplicates will not be checked + * @param src the new stream source - we take ownership + */ + virtual void add_radio_station(std::shared_ptr src) = 0; + /** + * Delete a internet radio station identified by id form the list + * @param id unique id of radio station + * @throws std::out_of_range if not found + */ + virtual void delete_radio_station(const QUuid& id) = 0; + + /** + * get all radio stream sources + */ + virtual const QVector>& + get_stations() const = 0; + + /** + * Get a internet radio station identified by ID + * @throws std::out_of_range if not found + * @param id unique ID of podcast + * @return station + */ + virtual const PlayableItem* get_station(const QUuid& id) const = 0; + + /** + * virtual destructor + */ + virtual ~IStationStore(){}; +}; + + +} // namespace DigitalRooster + +#endif /* INCLUDE_ISTATIONSTORE_HPP_ */ diff --git a/include/ITimeoutStore.hpp b/include/ITimeoutStore.hpp new file mode 100644 index 0000000..87a7f83 --- /dev/null +++ b/include/ITimeoutStore.hpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * \filename + * \brief Interface for creating/update and deletion Application Timeout + * Settings + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_ITIMEOUTSTORE_HPP_ +#define INCLUDE_ITIMEOUTSTORE_HPP_ + +#include + +namespace DigitalRooster { +/** + * Interface for addition, access of Timeout Settings + * actual storage \ref DigitalRooster::ConfigurationManager + */ +class ITimeOutStore { +public: + /** + * Access configuration when Alarm should stop automatically + * @return default alarm timeout + */ + virtual std::chrono::minutes get_alarm_timeout() const = 0; + + /** + * Minutes after which DigitalRooster goes in standby + * @return \ref sleep_timeout + */ + virtual std::chrono::minutes get_sleep_timeout() const = 0; + + /** + * Update sleep timeout Minutes after which DigitalRooster goes in standby + * @param timeout \ref sleep_timeout + */ + virtual void set_sleep_timeout(std::chrono::minutes timeout) = 0; + /** + * virtual destructor + */ + virtual ~ITimeOutStore(){}; +}; +} // namespace DigitalRooster + +#endif /* INCLUDE_ITIMEOUTSTORE_HPP_ */ diff --git a/include/IWeatherConfigStore.hpp b/include/IWeatherConfigStore.hpp new file mode 100644 index 0000000..d0ececb --- /dev/null +++ b/include/IWeatherConfigStore.hpp @@ -0,0 +1,106 @@ +/****************************************************************************** + * \filename + * \brief Interface for access to WeatherConfig + * + * \details + * + * \copyright (c) 2020 Thomas Ruschival + * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later + * SPDX-License-Identifier: GPL-3.0-or-later} + * + *****************************************************************************/ +#ifndef INCLUDE_IWEATHERCONFIGSTORE_HPP_ +#define INCLUDE_IWEATHERCONFIGSTORE_HPP_ + +#include + +#include +#include +#include + +namespace DigitalRooster { +/** + * Configuration of weather source as found in application config file + * read by ConfigurationManager \ref ConfigurationManager::get_weather_config + */ +class WeatherConfig { +public: + /** + * Constructor with 'relevant' information + * @param token + * @param cityid + * @param interval 1hour + */ + WeatherConfig(const QString& token = QString(), + const QString& cityid = QString(), + const std::chrono::seconds& interval = std::chrono::seconds(3600)); + + /** + * create Weatherconfig form Json configuration + * @param json jobject + * @return + */ + static WeatherConfig from_json_object(const QJsonObject& json); + + /** + * Serialize as JSON Object - only contains information that is not + * updated through RSS feed and cached. + * @return QJsonObject + */ + QJsonObject to_json_object() const; + + /** + * Openweather City id / location id + * @return \ref location_id + */ + const QString& get_location_id() const { + return location_id; + } + /** + * 'Secret' api-token for openweather api + * @return token string + */ + const QString& get_api_token() const { + return api_token; + } + /** + * update interval to poll openweather api + * @return seconds + */ + const std::chrono::seconds get_update_interval() { + return update_interval; + } + +private: + /** Openweathermap API Key */ + QString api_token; + /** + * location id + * from http://bulk.openweathermap.org/sample/city.list.json.gz + * e.g. 'Esslingen,de' = id 2928751, Porto Alegre=3452925 + */ + QString location_id; + /** Update Interval for wheather information */ + std::chrono::seconds update_interval; +}; + +/** + * Interface for addition, access of Weather Settings + * actual storage \ref DigitalRooster::ConfigurationManager + */ +class IWeatherConfigStore { +public: + /** + * Read current WeatherConfig + * @return weatherconfig + */ + virtual const WeatherConfig& get_weather_config() const = 0; + + /** + * virtual destructor + */ + virtual ~IWeatherConfigStore(){}; +}; +} // namespace DigitalRooster + +#endif /* INCLUDE_IWEATHERCONFIGSTORE_HPP_ */ diff --git a/include/alarm.hpp b/include/alarm.hpp index 466e7d2..b4b9333 100644 --- a/include/alarm.hpp +++ b/include/alarm.hpp @@ -14,11 +14,11 @@ #define _ALARM_HPP_ #include +#include #include +#include #include #include -#include -#include #include #include @@ -69,14 +69,15 @@ class Alarm : public QObject { const QUuid& uid = QUuid::createUuid(), QObject* parent = nullptr); /** - * Need Default constructor to register with QML + * Need Default constructor to create empty Alarm form QML + * Will be initialized to: + * : id(QUuid::createUuid()) + * , media(std::make_shared("Alarm", QUrl())) + * , period(Alarm::Daily) + * , enabled(true) + * , timeout(DEFAULT_ALARM_TIMEOUT){} */ - Alarm() - : id(QUuid::createUuid()) - , media(nullptr) - , period(Alarm::Daily) - , enabled(true) - , timeout(DEFAULT_ALARM_TIMEOUT){}; + Alarm(); /** * unique id for alarm @@ -132,7 +133,7 @@ class Alarm : public QObject { * Duration for alarm to stop automatically * @return time in minutes */ - std::chrono::minutes get_timeout() const{ + std::chrono::minutes get_timeout() const { return timeout; } /** @@ -149,9 +150,7 @@ class Alarm : public QObject { std::shared_ptr get_media() const { return media; } - void set_media(std::shared_ptr new_media) { - media = new_media; - } + void set_media(std::shared_ptr new_media); /** * is this alarm set diff --git a/include/alarmdispatcher.hpp b/include/alarmdispatcher.hpp index 4c680a4..e9e7db6 100644 --- a/include/alarmdispatcher.hpp +++ b/include/alarmdispatcher.hpp @@ -18,11 +18,9 @@ #include #include -#include "alarm.hpp" -#include "mediaplayerproxy.hpp" - namespace DigitalRooster { -class ConfigurationManager; +class Alarm; +class IAlarmStore; /** * Monitors changes in alarm configuration and dispatches alarms when due @@ -32,10 +30,10 @@ class AlarmDispatcher : public QObject { public: /** * Constructor for AlarmDispatcher - * @param confman - * @param parent + * @param store Interface to get/update/delete alarms + * @param parent QObject hierarchy manage lifetime */ - AlarmDispatcher(std::shared_ptr confman, + explicit AlarmDispatcher(IAlarmStore& store, QObject* parent = nullptr); /** @@ -72,7 +70,7 @@ public slots: /** * Central configuration and data handler */ - std::shared_ptr cm; + IAlarmStore& cm; /** * Timer for periodic polling */ diff --git a/include/alarmmonitor.hpp b/include/alarmmonitor.hpp index e9a2ad7..758212c 100644 --- a/include/alarmmonitor.hpp +++ b/include/alarmmonitor.hpp @@ -19,10 +19,9 @@ #include #include -#include "alarm.hpp" -#include "mediaplayer.hpp" - namespace DigitalRooster { +class MediaPlayer; +class Alarm; /** * Supervision of alarm behavior. Makes sure I wake up even if the original @@ -48,8 +47,9 @@ class AlarmMonitor : public QObject { * @param fallback_timeout grace period to wait until fallback is triggered * @param parent */ - AlarmMonitor(std::shared_ptr player, - std::chrono::milliseconds fallback_timeout = std::chrono::milliseconds(10000), + AlarmMonitor(DigitalRooster::MediaPlayer& player, + std::chrono::milliseconds fallback_timeout = std::chrono::milliseconds( + 10000), QObject* parent = nullptr); /** @@ -78,7 +78,7 @@ public slots: /** * PlayerBackend that receives the Alarms */ - std::shared_ptr mpp; + MediaPlayer& mpp; /** * Timer to trigger fallback behavior if player did not start to play diff --git a/include/brightnesscontrol.hpp b/include/brightnesscontrol.hpp index bada401..23a3986 100644 --- a/include/brightnesscontrol.hpp +++ b/include/brightnesscontrol.hpp @@ -16,11 +16,8 @@ #include #include -#include "powercontrol.hpp" - namespace DigitalRooster { - -class ConfigurationManager; +class IBrightnessStore; /** * Controls display brightness settings @@ -36,7 +33,7 @@ class BrightnessControl : public QObject { * Constructor * @param confman configuration */ - explicit BrightnessControl(std::shared_ptr confman); + explicit BrightnessControl(IBrightnessStore& store); ~BrightnessControl() = default; /** @@ -86,9 +83,9 @@ public slots: private: /** - * Central configuration and data handler + * configuration and data handler */ - std::shared_ptr cm; + IBrightnessStore& cm; /** * Current brightness setting (linear) diff --git a/include/configuration_manager.hpp b/include/configuration_manager.hpp index f803bfe..9e492d7 100644 --- a/include/configuration_manager.hpp +++ b/include/configuration_manager.hpp @@ -16,23 +16,36 @@ #include #include #include +#include #include #include -#include "PlayableItem.hpp" -#include "PodcastSource.hpp" -#include "alarm.hpp" #include "appconstants.hpp" -#include "weather.hpp" +/* Implemented Interfaces */ +#include "IAlarmStore.hpp" +#include "IBrightnessStore.hpp" +#include "IPodcastStore.hpp" +#include "IStationStore.hpp" +#include "ITimeoutStore.hpp" +#include "IWeatherConfigStore.hpp" namespace DigitalRooster { - +// forward decl +class PlayableItem; +class PodcastSource; +class Alarm; /** - * Reads JSON configuration + * Reads JSON configuration and provides API to configuration objects */ -class ConfigurationManager : public QObject { +class ConfigurationManager : public QObject, + public IAlarmStore, + public IStationStore, + public IPodcastStore, + public ITimeOutStore, + public IBrightnessStore, + public IWeatherConfigStore { Q_OBJECT Q_PROPERTY(QString revision READ get_revision CONSTANT) Q_PROPERTY(QString buildtime READ get_build CONSTANT) @@ -77,108 +90,54 @@ class ConfigurationManager : public QObject { return do_get_volume(); } - /** - * User set and stored brightness for standby mode (form config file) - * @return brightness - */ - int get_standby_brightness() const { - return do_get_brightness_sb(); - } - - /** - * User set and stored brightness for standby mode (form config file) - * @return brightness - */ - int get_active_brightness() const { - return do_get_brightness_act(); - } - - /** - * get all radio stream sources - */ - const QVector>& get_stream_sources() { - return get_iradio_list(); - } - - /** - * Get a internet radio station identified by ID - * @throws std::out_of_range if not found - * @param id unique ID of podcast - * @return station - */ - const PlayableItem* get_stream_source(const QUuid& id) const; - - /** - * get all podcast sources - */ - const QVector>& get_podcast_sources() { - return get_podcast_list(); - } - - /** - * Get a single podcast source identified by index - * @throws std::out_of_range if not found - * @param index in vector - * @return PodastSource - */ - PodcastSource* get_podcast_source_by_index(int index) const; - - /** - * Get a single podcast source identified by ID - * @throws std::out_of_range if not found - * @param id unique ID of podcast - * @return source - */ - const PodcastSource* get_podcast_source(const QUuid& id) const; - - /** - * Removes a podcast source entry form list - * @throws std::out_of_range if not found - * @param index in vector - */ - void remove_podcast_source_by_index(int index); - - /** - * get all radio stream sources + /* + * Implementation of IAlarmStore */ - const QVector>& get_alarms() { - return get_alarm_list(); - } + void add_alarm(std::shared_ptr alarm) override; + void delete_alarm(const QUuid& id) override; + const Alarm* get_alarm(const QUuid& id) const override; + const QVector>& get_alarms() const override; - /** - * Get a alarm identified by ID - * @throws std::out_of_range if not found - * @param id unique ID of podcast - * @return station + /* + * Implementation of IStationStore */ - const Alarm* get_alarm(const QUuid& id) const; + virtual void add_radio_station(std::shared_ptr src) override; + virtual void delete_radio_station(const QUuid& id) override; + const PlayableItem* get_station(const QUuid& id) const override; + virtual const QVector>& + get_stations() const override; - /** - * Weather configuration object + /* + * Implementation of IPodcastStore */ - const WeatherConfig* get_weather_config() { - return get_weather_cfg(); - } + virtual void add_podcast_source( + std::shared_ptr podcast) override; + virtual void delete_podcast_source(const QUuid& id) override; + virtual const PodcastSource* get_podcast_source( + const QUuid& id) const override; + virtual const QVector>& + get_podcast_sources() const override; + virtual PodcastSource* get_podcast_source_by_index( + int index) const override; + virtual void remove_podcast_source_by_index(int index) override; - /** - * Access configuration when Alarm should stop automatically - * @return default alarm timeout + /* + * Implementation of ITimeoutStore */ - virtual std::chrono::minutes get_alarm_timeout() const { - return global_alarm_timeout; - } + virtual std::chrono::minutes get_alarm_timeout() const override; + virtual std::chrono::minutes get_sleep_timeout() const override; + virtual void set_sleep_timeout(std::chrono::minutes timeout) override; - /** - * Minutes after which DigitalRooster goes in standby - * @return \ref sleep_timeout + /* + * Implementation of IBrightnessStore */ - virtual std::chrono::minutes get_sleep_timeout() const; + virtual int get_standby_brightness() const override; + virtual int get_active_brightness() const override; - /** - * Update sleep timeout Minutes after which DigitalRooster goes in standby - * @param timeout \ref sleep_timeout + /* + * Implementation of IWeatherConfigStore */ - void set_sleep_timeout(std::chrono::minutes timeout); + virtual const WeatherConfig& get_weather_config() const override; /** * Path to wpa_supplicant control socket @@ -200,45 +159,6 @@ class ConfigurationManager : public QObject { return get_cache_dir_name(); }; - /** - * Append the radio stream to list - duplicates will not be checked - * @param src the new stream source - we take ownership - */ - void add_radio_station(std::shared_ptr src); - - /** - * Append new PodcastSource to list - * @param podcast source - */ - void add_podcast_source(std::shared_ptr podcast); - - /** - * Append new alarm to list - * @param alarm - */ - void add_alarm(std::shared_ptr alarm); - - /** - * Delete an alarm identified by ID from the list of alarms - * @param id of alarm - * @throws std::out_of_range if not found - */ - void delete_alarm(const QUuid& id); - - /** - * Delete a internet radio station identified by id form the list - * @param id unique id of radio station - * @throws std::out_of_range if not found - */ - void delete_radio_station(const QUuid& id); - - /** - * Delete a podcast source identified by id form the list of sources - * @param id unique id of podcast source - * @throws std::out_of_range if not found - */ - void delete_podcast_source(const QUuid& id); - public slots: /** * Any Item (Alarm, PodcastSource...) changed @@ -256,13 +176,13 @@ public slots: * user changed standby brightness * @param brightness new volume settings (0..100) */ - void set_standby_brightness(int brightness); + void set_standby_brightness(int brightness) override; /** * user changed standby brightness * @param brightness new volume settings (0..100) */ - void set_active_brightness(int brightness); + void set_active_brightness(int brightness) override; /** * Write memory config to file - will overwrite changes in file @@ -289,10 +209,12 @@ public slots: * podcast list was changed (added/deleted items) */ void podcast_sources_changed(); + /** * alarm list was changed (added/deleted items) */ void alarms_changed(); + /** * radio list was changed (added/deleted items) */ @@ -350,7 +272,7 @@ public slots: /** * Weather configuration */ - std::unique_ptr weather_cfg; + WeatherConfig weather_cfg; /** * Configuration directory, writable, created if it doesn't exist @@ -433,45 +355,6 @@ public slots: */ void refresh_configuration(); - /** - * get all radio stream sources - */ - virtual QVector>& get_iradio_list() { - return stream_sources; - } - - /** - * get all podcast sources - */ - virtual QVector>& get_podcast_list() { - return podcast_sources; - } - - /** - * get all radio stream sources - */ - virtual QVector>& get_alarm_list() { - return alarms; - } - - /** - * Weather configuration object - */ - virtual const WeatherConfig* get_weather_cfg() { - return weather_cfg.get(); - } - - /** - * Private virtual interface for brightness settings - */ - virtual int do_get_brightness_sb() const { - return brightness_sb; - } - - virtual int do_get_brightness_act() const { - return brightness_act; - } - /** * actually set active brightness * @param brightness - new actual brightness diff --git a/include/mediaplayer.hpp b/include/mediaplayer.hpp index 4214e2d..073a9b5 100644 --- a/include/mediaplayer.hpp +++ b/include/mediaplayer.hpp @@ -56,6 +56,12 @@ class MediaPlayer : public QObject { QMediaPlayer::Error error() const; public slots: + /** + * Update Media - yes it takes shared ownership mediaplayer updates the + * media position while playing make sure it exists even if e.g. the alarm + * has been deleted + * @param media + */ void set_media(std::shared_ptr media); void set_playlist(QMediaPlaylist* playlist); /** diff --git a/include/podcast_serializer.hpp b/include/podcast_serializer.hpp index 836b215..eb1045c 100644 --- a/include/podcast_serializer.hpp +++ b/include/podcast_serializer.hpp @@ -24,12 +24,10 @@ #include #include -#include "PlayableItem.hpp" namespace DigitalRooster { class PodcastSource; - /** * Serialization/Deserialization of PodcastSources and PodcastEpisodes * from filesystem diff --git a/include/sleeptimer.hpp b/include/sleeptimer.hpp index e24f6fc..eae873c 100644 --- a/include/sleeptimer.hpp +++ b/include/sleeptimer.hpp @@ -15,13 +15,16 @@ #include #include +#include + #include #include -#include "alarm.hpp" -#include "configuration_manager.hpp" +#include "ITimeoutStore.hpp" namespace DigitalRooster { +// forward decl +class Alarm; /** * SleepTimer emits sleep_timer_elapsed() after \ref @@ -42,8 +45,8 @@ class SleepTimer : public QObject { * @param cm configuration manager * @param parent owning QObject */ - SleepTimer( - std::shared_ptr cm, QObject* parent = nullptr); + explicit SleepTimer( + ITimeOutStore& store, QObject* parent = nullptr); /** * Access the remaining time until standby is triggered @@ -139,7 +142,7 @@ public slots: /** * Central configuration and data handler */ - std::shared_ptr cm; + ITimeOutStore& cm; /** * Timer to trigger standby diff --git a/include/weather.hpp b/include/weather.hpp index ba4f03e..16389a3 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -22,74 +22,11 @@ #include #include +#include "IWeatherConfigStore.hpp" namespace DigitalRooster { class ConfigurationManager; -/** - * Configuration of weather source as found in application config file - * read by ConfigurationManager \ref ConfigurationManager::get_weather_cfg - */ -class WeatherConfig { -public: - /** - * Constructor with 'relevant' information - * @param token - * @param cityid - * @param interval 1hour - */ - WeatherConfig( - const QString& token =QString() , - const QString& cityid =QString() , - const std::chrono::seconds& interval = std::chrono::seconds(3600)); - - /** - * create Weatherconfig form Json configuration - * @param json jobject - * @return - */ - static std::unique_ptr from_json_object(const QJsonObject& json); - /** - * Serialize as JSON Object - only contains information that is not - * updated through RSS feed and cached. - * @return QJsonObject - */ - QJsonObject to_json_object() const; - - /** - * Openweather City id / location id - * @return \ref location_id - */ - const QString& get_location_id() const { - return location_id; - } - /** - * 'Secret' api-token for openweather api - * @return token string - */ - const QString& get_api_token() const{ - return api_token; - } - /** - * update interval to poll openweather api - * @return seconds - */ - const std::chrono::seconds get_update_interval() { - return update_interval; - } -private: - /** Openweathermap API Key */ - QString api_token; - /** - * location id - * from http://bulk.openweathermap.org/sample/city.list.json.gz - * e.g. 'Esslingen,de' = id 2928751, Porto Alegre=3452925 - */ - QString location_id; - /** Update Interval for wheather information */ - std::chrono::seconds update_interval; -}; - /** * Periodically downloads weather info from Openweathermaps @@ -105,10 +42,10 @@ class Weather : public QObject { public: /** * Constructor for Weather provider - * @param confman configuration + * @param store access to current weather configuration * @param parent */ - explicit Weather(std::shared_ptr confman, + explicit Weather(const IWeatherConfigStore& store, QObject* parent = nullptr); /** * Update Download interval @@ -188,7 +125,7 @@ public slots: /** * Central configuration and data handler */ - std::shared_ptr cm; + const IWeatherConfigStore& cm; /** * Interval in ms for auto refresh of content @@ -238,7 +175,7 @@ public slots: * @param cfg configuration with location, units etc. * @return uri e.g. api.openweathermap.org/data/2.5/weather?zip=94040,us */ -QUrl create_weather_uri(const WeatherConfig* cfg); +QUrl create_weather_uri(const WeatherConfig& cfg); } // namespace DigitalRooster diff --git a/libsrc/alarm.cpp b/libsrc/alarm.cpp index c0773ab..de26bb0 100644 --- a/libsrc/alarm.cpp +++ b/libsrc/alarm.cpp @@ -23,7 +23,16 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.Alarm"); /*****************************************************************************/ -// Alarm from time +Alarm::Alarm() + : id(QUuid::createUuid()) + , media(std::make_shared("Alarm", QUrl())) + , period(Alarm::Daily) + , enabled(true) + , timeout(DEFAULT_ALARM_TIMEOUT) { +} + +/*****************************************************************************/ +// Alarm from time with media etc. Alarm::Alarm(const QUrl& media, const QTime& timepoint, Alarm::Period period, bool enabled, const QUuid& uid, QObject* parent) : QObject(parent) @@ -48,6 +57,7 @@ void Alarm::set_time(const QTime& timeofday) { /*****************************************************************************/ QUrl Alarm::get_media_url() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; return media->get_url(); } @@ -84,10 +94,9 @@ const QTime& Alarm::get_time() const { /*****************************************************************************/ -static const std::vector> - period_to_string = {{Alarm::Daily, KEY_ALARM_DAILY}, - {Alarm::Workdays, KEY_ALARM_WORKDAYS}, - {Alarm::Weekend, KEY_ALARM_WEEKEND}, {Alarm::Once, KEY_ALARM_ONCE}}; +static const std::vector> period_to_string = { + {Alarm::Daily, KEY_ALARM_DAILY}, {Alarm::Workdays, KEY_ALARM_WORKDAYS}, + {Alarm::Weekend, KEY_ALARM_WEEKEND}, {Alarm::Once, KEY_ALARM_ONCE}}; /*****************************************************************************/ void Alarm::update_media_url(const QUrl& url) { @@ -97,6 +106,12 @@ void Alarm::update_media_url(const QUrl& url) { emit dataChanged(); } +/*****************************************************************************/ +void Alarm::set_media(std::shared_ptr new_media) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + media = new_media; +} + /*****************************************************************************/ Alarm::Period DigitalRooster::json_string_to_alarm_period( const QString& literal) { @@ -122,16 +137,15 @@ QString DigitalRooster::alarm_period_to_json_string( return item.first == period; }); if (res == period_to_string.end()) { - qCWarning(CLASS_LC) << "Invalid Period passed (" << period << ")" << - "This should never happen - aborting"; + qCWarning(CLASS_LC) << "Invalid Period passed (" << period << ")" + << "This should never happen - aborting"; abort(); } return res->second; } /*****************************************************************************/ -std::shared_ptr Alarm::from_json_object( - const QJsonObject& json_alarm) { +std::shared_ptr Alarm::from_json_object(const QJsonObject& json_alarm) { qCDebug(CLASS_LC) << Q_FUNC_INFO; if (json_alarm.isEmpty()) { throw std::invalid_argument("Empty Alarm JSON object!"); @@ -146,8 +160,9 @@ std::shared_ptr Alarm::from_json_object( json_alarm[KEY_ALARM_PERIOD].toString(KEY_ALARM_DAILY)); auto timepoint = QTime::fromString(json_alarm[KEY_TIME].toString(), "hh:mm"); - if(!timepoint.isValid()){ - qCWarning(CLASS_LC) << "Invalid Time " << json_alarm[KEY_TIME].toString(); + if (!timepoint.isValid()) { + qCWarning(CLASS_LC) + << "Invalid Time " << json_alarm[KEY_TIME].toString(); throw std::invalid_argument("Alarm Time invalid!"); } @@ -183,7 +198,7 @@ QJsonObject Alarm::to_json_object() const { alarm_period_to_json_string(this->get_period()); alarmcfg[KEY_TIME] = this->get_time().toString("hh:mm"); alarmcfg[KEY_VOLUME] = this->get_volume(); - alarmcfg[KEY_URI] = this->get_media()->get_url().toString(); + alarmcfg[KEY_URI] = this->get_media_url().toString(); alarmcfg[KEY_ENABLED] = this->is_enabled(); return alarmcfg; } diff --git a/libsrc/alarmdispatcher.cpp b/libsrc/alarmdispatcher.cpp index da545a5..1c92c93 100644 --- a/libsrc/alarmdispatcher.cpp +++ b/libsrc/alarmdispatcher.cpp @@ -16,9 +16,9 @@ #include #include +#include "IAlarmStore.hpp" #include "alarm.hpp" #include "alarmdispatcher.hpp" -#include "configuration_manager.hpp" #include "mediaplayerproxy.hpp" #include "timeprovider.hpp" @@ -30,9 +30,9 @@ static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.AlarmDispatcher"); /*****************************************************************************/ AlarmDispatcher::AlarmDispatcher( - std::shared_ptr confman, QObject* parent) + IAlarmStore& store, QObject* parent) : QObject(parent) - , cm(confman) + , cm(store) , interval(std::chrono::seconds(30)) { qCDebug(CLASS_LC) << Q_FUNC_INFO; interval_timer.setInterval(duration_cast(interval)); @@ -46,15 +46,15 @@ void AlarmDispatcher::check_alarms() { qCDebug(CLASS_LC) << Q_FUNC_INFO; auto now = wallclock->now(); auto dow = now.date().dayOfWeek(); - for (const auto& alarm : cm->get_alarms()) { + for (const auto& alarm : cm.get_alarms()) { /* skip if alarm is disabled */ if (!alarm->is_enabled()) { - continue; - } - /* - * skip if now is not near the alarm_time, i.e. less than interval - * delta is negative if alarm_time is in the past - */ + continue; + } + /* + * skip if now is not near the alarm_time, i.e. less than interval + * delta is negative if alarm_time is in the past + */ auto delta = now.time().secsTo(alarm->get_time()); if (delta < 0 || delta > interval.count()) { continue; diff --git a/libsrc/alarmmonitor.cpp b/libsrc/alarmmonitor.cpp index 4533147..35f65b1 100644 --- a/libsrc/alarmmonitor.cpp +++ b/libsrc/alarmmonitor.cpp @@ -22,7 +22,7 @@ using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.AlarmMonitor"); /*****************************************************************************/ -AlarmMonitor::AlarmMonitor(std::shared_ptr player, +AlarmMonitor::AlarmMonitor(MediaPlayer& player, std::chrono::milliseconds fallback_timeout, QObject* parent) : QObject(parent) , mpp(player) @@ -30,7 +30,7 @@ AlarmMonitor::AlarmMonitor(std::shared_ptr player, qCDebug(CLASS_LC) << Q_FUNC_INFO; /* Receive errors from player */ - QObject::connect(mpp.get(), + QObject::connect(&mpp, static_cast( &MediaPlayer::error), [&](QMediaPlayer::Error error) { @@ -43,14 +43,14 @@ AlarmMonitor::AlarmMonitor(std::shared_ptr player, }); /* listen to player state changes */ - QObject::connect(mpp.get(), &MediaPlayer::playback_state_changed, + QObject::connect(&mpp, &MediaPlayer::playback_state_changed, [&](QMediaPlayer::State player_state) { qCDebug(CLASS_LC) << "AlarmMonitor PlayerState:" << player_state - << "PlayerError:" << mpp->error(); + << "PlayerError:" << mpp.error(); /* check if alarm was stopped without error (user stopped) */ if (player_state == QMediaPlayer::StoppedState || player_state == QMediaPlayer::PausedState) { - if (mpp->error() == QMediaPlayer::NoError) { + if (mpp.error() == QMediaPlayer::NoError) { qCDebug(CLASS_LC) << " Stopped player without error"; set_state(Idle); } else { @@ -91,9 +91,9 @@ void AlarmMonitor::alarm_triggered( std::shared_ptr alarm) { qCDebug(CLASS_LC) << Q_FUNC_INFO; set_state(ExpectingAlarm); - mpp->set_media(alarm->get_media()); - mpp->set_volume(alarm->get_volume()); - mpp->play(); + mpp.set_media(alarm->get_media()); + mpp.set_volume(alarm->get_volume()); + mpp.play(); fallback_alarm_timer.start(timeout); } @@ -102,9 +102,9 @@ void AlarmMonitor::trigger_fallback_behavior() { qCDebug(CLASS_LC) << Q_FUNC_INFO; fallback_alarm.setCurrentIndex(0); set_state(FallBackMode); - mpp->set_volume(50); - mpp->set_playlist(&fallback_alarm); - mpp->play(); + mpp.set_volume(50); + mpp.set_playlist(&fallback_alarm); + mpp.play(); } /*****************************************************************************/ diff --git a/libsrc/brightnesscontrol.cpp b/libsrc/brightnesscontrol.cpp index 4c2fd81..5b723d6 100644 --- a/libsrc/brightnesscontrol.cpp +++ b/libsrc/brightnesscontrol.cpp @@ -12,9 +12,11 @@ #include #include #include +#include +#include "IBrightnessStore.hpp" #include "brightnesscontrol.hpp" -#include "configuration_manager.hpp" +#include "powercontrol.hpp" using namespace DigitalRooster; @@ -22,9 +24,8 @@ static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.BrightnessControl"); static const double LOG_100 = 4.6052; /*****************************************************************************/ -BrightnessControl::BrightnessControl( - std::shared_ptr confman) - : cm(confman) +BrightnessControl::BrightnessControl(IBrightnessStore& store) + : cm(store) , linear_brightness(0) { qCDebug(CLASS_LC) << Q_FUNC_INFO; } @@ -34,7 +35,7 @@ void BrightnessControl::set_brightness(int brightness) { qCDebug(CLASS_LC) << Q_FUNC_INFO << "Linear: " << brightness; linear_brightness = brightness; emit brightness_pwm_change(lin2log(linear_brightness)); - cm->set_active_brightness(brightness); + cm.set_active_brightness(brightness); } /*****************************************************************************/ @@ -57,14 +58,14 @@ int BrightnessControl::lin2log(int lb) { /*****************************************************************************/ void BrightnessControl::restore_active_brightness() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - linear_brightness = cm->get_active_brightness(); + linear_brightness = cm.get_active_brightness(); emit brightness_pwm_change(lin2log(linear_brightness)); } /*****************************************************************************/ void BrightnessControl::restore_standby_brightness() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - linear_brightness = cm->get_standby_brightness(); + linear_brightness = cm.get_standby_brightness(); emit brightness_pwm_change(lin2log(linear_brightness)); } diff --git a/libsrc/configuration_manager.cpp b/libsrc/configuration_manager.cpp index 73ef2e1..570c242 100644 --- a/libsrc/configuration_manager.cpp +++ b/libsrc/configuration_manager.cpp @@ -20,9 +20,10 @@ #include #include -#include "alarm.hpp" -#include "appconstants.hpp" +#include "PlayableItem.hpp" +#include "PodcastSource.hpp" #include "UpdateTask.hpp" +#include "alarm.hpp" #include "configuration_manager.hpp" using namespace DigitalRooster; @@ -82,7 +83,6 @@ ConfigurationManager::ConfigurationManager( , volume(DEFAULT_VOLUME) , brightness_sb(DEFAULT_BRIGHTNESS) , brightness_act(DEFAULT_BRIGHTNESS) - , weather_cfg(new WeatherConfig) , config_file(configpath) , application_cache_dir(cachedir) , wpa_socket_name(WPA_CONTROL_SOCKET_PATH) { @@ -197,8 +197,7 @@ void ConfigurationManager::parse_json(const QByteArray& json) { } /*****************************************************************************/ -void ConfigurationManager::read_radio_streams( - const QJsonObject& appconfig) { +void ConfigurationManager::read_radio_streams(const QJsonObject& appconfig) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray stations = appconfig[DigitalRooster::KEY_GROUP_IRADIO_SOURCES].toArray(); @@ -223,15 +222,14 @@ void ConfigurationManager::read_radio_streams( } /*****************************************************************************/ -void ConfigurationManager::read_podcasts( - const QJsonObject& appconfig) { +void ConfigurationManager::read_podcasts(const QJsonObject& appconfig) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonArray podcasts = appconfig[DigitalRooster::KEY_GROUP_PODCAST_SOURCES].toArray(); for (const auto pc : podcasts) { auto ps = PodcastSource::from_json_object(pc.toObject()); auto serializer = std::make_unique( - application_cache_dir, ps.get()); + application_cache_dir, ps.get()); // populate podcast source from cached info serializer->restore_info(); // Move ownership to Podcast Source and setup signal/slot connections @@ -272,8 +270,7 @@ void ConfigurationManager::read_alarms(const QJsonObject& appconfig) { } /*****************************************************************************/ -void ConfigurationManager::read_weather( - const QJsonObject& appconfig) { +void ConfigurationManager::read_weather(const QJsonObject& appconfig) { if (appconfig[KEY_WEATHER].isNull()) { qCWarning(CLASS_LC) << "no weather configuration found!"; return; @@ -286,55 +283,6 @@ void ConfigurationManager::read_weather( } } -/*****************************************************************************/ -void ConfigurationManager::add_radio_station( - std::shared_ptr src) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - this->stream_sources.push_back(src); - dataChanged(); - emit stations_changed(); -} - -/*****************************************************************************/ -const PlayableItem* ConfigurationManager::get_stream_source( - const QUuid& id) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - /* Find by id throws - just pass it on to the client */ - return find_by_id(stream_sources, id); -} - -/*****************************************************************************/ -void ConfigurationManager::add_podcast_source( - std::shared_ptr src) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - this->podcast_sources.push_back(src); - dataChanged(); - emit podcast_sources_changed(); -} - -/*****************************************************************************/ -const PodcastSource* ConfigurationManager::get_podcast_source( - const QUuid& id) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - /* Find by id throws - just pass it on to the client */ - return find_by_id(podcast_sources, id); -} - -/*****************************************************************************/ -void ConfigurationManager::add_alarm(std::shared_ptr alm) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - this->alarms.push_back(alm); - dataChanged(); - emit alarms_changed(); -} - -/*****************************************************************************/ -const Alarm* ConfigurationManager::get_alarm(const QUuid& id) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - /* Find by id throws - just pass it on to the client */ - return find_by_id(alarms, id); -} - /*****************************************************************************/ void ConfigurationManager::dataChanged() { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -413,7 +361,7 @@ void ConfigurationManager::store_current_config() { appconfig[KEY_GROUP_ALARMS] = alarms_json; /* Store Weather information*/ - appconfig[KEY_WEATHER] = weather_cfg->to_json_object(); + appconfig[KEY_WEATHER] = weather_cfg.to_json_object(); /* global application configuration */ appconfig[KEY_ALARM_TIMEOUT] = @@ -532,20 +480,17 @@ QString ConfigurationManager::check_and_create_config() { } /*****************************************************************************/ -PodcastSource* ConfigurationManager::get_podcast_source_by_index( - int index) const { +QString ConfigurationManager::get_cache_dir_name() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - return podcast_sources.at(index).get(); + return application_cache_dir.path(); } /*****************************************************************************/ -void ConfigurationManager::remove_podcast_source_by_index(int index) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - podcast_sources.remove(index); - writeTimer.start(); // start delayed write -} -/*****************************************************************************/ + +/***************************************************************************** + * Implementation of IAlarmStore + *****************************************************************************/ void ConfigurationManager::delete_alarm(const QUuid& id) { qCDebug(CLASS_LC) << Q_FUNC_INFO; /* delete may throw - just pass it on to the client */ @@ -553,15 +498,39 @@ void ConfigurationManager::delete_alarm(const QUuid& id) { dataChanged(); emit alarms_changed(); }; - /*****************************************************************************/ -void ConfigurationManager::delete_podcast_source(const QUuid& id) { +void ConfigurationManager::add_alarm(std::shared_ptr alm) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - /* delete may throw - just pass it on to the client */ - delete_by_id(podcast_sources, id); + this->alarms.push_back(alm); dataChanged(); emit alarms_changed(); -}; +} + +/*****************************************************************************/ +const Alarm* ConfigurationManager::get_alarm(const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* Find by id throws - just pass it on to the client */ + return find_by_id(alarms, id); +} + +/*****************************************************************************/ +const QVector>& +ConfigurationManager::get_alarms() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return alarms; +} + + +/***************************************************************************** + * Implementation of IStationStore + *****************************************************************************/ +void ConfigurationManager::add_radio_station( + std::shared_ptr src) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + this->stream_sources.push_back(src); + dataChanged(); + emit stations_changed(); +} /*****************************************************************************/ void ConfigurationManager::delete_radio_station(const QUuid& id) { @@ -569,10 +538,91 @@ void ConfigurationManager::delete_radio_station(const QUuid& id) { /* delete may throw - just pass it on to the client */ delete_by_id(stream_sources, id); dataChanged(); - emit alarms_changed(); + emit stations_changed(); +}; + +/*****************************************************************************/ +const PlayableItem* ConfigurationManager::get_station(const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* Find by id throws - just pass it on to the client */ + return find_by_id(stream_sources, id); +} + +/*****************************************************************************/ +const QVector>& +ConfigurationManager::get_stations() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return stream_sources; +} + + +/***************************************************************************** + * Implementation of IPodcastStore + *****************************************************************************/ +void ConfigurationManager::add_podcast_source( + std::shared_ptr src) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + this->podcast_sources.push_back(src); + dataChanged(); + emit podcast_sources_changed(); +} + +/*****************************************************************************/ +void ConfigurationManager::delete_podcast_source(const QUuid& id) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* delete may throw - just pass it on to the client */ + delete_by_id(podcast_sources, id); + dataChanged(); + emit podcast_sources_changed(); }; /*****************************************************************************/ +const PodcastSource* ConfigurationManager::get_podcast_source( + const QUuid& id) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + /* Find by id throws - just pass it on to the client */ + return find_by_id(podcast_sources, id); +} + +/*****************************************************************************/ +const QVector>& +ConfigurationManager::get_podcast_sources() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return podcast_sources; +} + +/*****************************************************************************/ +PodcastSource* ConfigurationManager::get_podcast_source_by_index( + int index) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return podcast_sources.at(index).get(); +} + +/*****************************************************************************/ +void ConfigurationManager::remove_podcast_source_by_index(int index) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + podcast_sources.remove(index); + emit podcast_sources_changed(); + emit dataChanged(); + writeTimer.start(); // start delayed write +} +/***************************************************************************** + * Implementation of IBrightnessStore + *****************************************************************************/ +int ConfigurationManager::get_active_brightness() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return brightness_act; +} + +/*****************************************************************************/ +int ConfigurationManager::get_standby_brightness() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return brightness_sb; +} + +/***************************************************************************** + * Implementation of ITimeoutStore + *****************************************************************************/ std::chrono::minutes ConfigurationManager::get_sleep_timeout() const { return sleep_timeout; } @@ -585,9 +635,17 @@ void ConfigurationManager::set_sleep_timeout(std::chrono::minutes timeout) { } /*****************************************************************************/ -QString ConfigurationManager::get_cache_dir_name() { +std::chrono::minutes ConfigurationManager::get_alarm_timeout() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - return application_cache_dir.path(); + return global_alarm_timeout; +} + +/***************************************************************************** + * Implementation of IWeatherConfigStore + *****************************************************************************/ +const WeatherConfig& ConfigurationManager::get_weather_config() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + return weather_cfg; } /*****************************************************************************/ diff --git a/libsrc/sleeptimer.cpp b/libsrc/sleeptimer.cpp index f8db929..b6ad63a 100644 --- a/libsrc/sleeptimer.cpp +++ b/libsrc/sleeptimer.cpp @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include "sleeptimer.hpp" +#include "alarm.hpp" using namespace DigitalRooster; using namespace std::chrono; @@ -25,15 +27,15 @@ static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.SleepTimer"); /*****************************************************************************/ SleepTimer::SleepTimer( - std::shared_ptr cm, QObject* parent) + ITimeOutStore& store, QObject* parent) : QObject(parent) - , cm(cm) + , cm(store) , evt_timer_id(0) - , remaining_time(cm->get_sleep_timeout()) + , remaining_time(cm.get_sleep_timeout()) , activity(SleepTimer::Idle) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - sleep_timer.setInterval(cm->get_sleep_timeout()); + sleep_timer.setInterval(cm.get_sleep_timeout()); sleep_timer.setSingleShot(true); connect( @@ -53,7 +55,7 @@ void SleepTimer::playback_state_changed(QMediaPlayer::State state) { /* Check if alarm was dispatched do not set normal sleep_timeout */ if (activity != SleepTimer::Alarm) { qCDebug(CLASS_LC) << " restarting timer"; - sleep_timer.setInterval(cm->get_sleep_timeout()); + sleep_timer.setInterval(cm.get_sleep_timeout()); sleep_timer.start(); emit remaining_time_changed(get_remaining_time()); } @@ -107,13 +109,13 @@ int SleepTimer::get_remaining_time() { /*****************************************************************************/ std::chrono::minutes SleepTimer::get_sleep_timeout() const { - return cm->get_sleep_timeout(); + return cm.get_sleep_timeout(); } /*****************************************************************************/ void SleepTimer::set_sleep_timeout(std::chrono::minutes timeout) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - cm->set_sleep_timeout(timeout); + cm.set_sleep_timeout(timeout); // also update running timeouts timeout_changed(timeout); emit sleep_timeout_changed(timeout); @@ -129,7 +131,7 @@ void SleepTimer::set_sleep_timeout(int timeout) { /*****************************************************************************/ int SleepTimer::get_sleep_timeout_minutes_count() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - return cm->get_sleep_timeout().count(); + return cm.get_sleep_timeout().count(); } /*****************************************************************************/ diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index d81f028..347abf6 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -17,7 +17,7 @@ #include #include // std::system_error -#include "configuration_manager.hpp" +#include "IWeatherConfigStore.hpp" #include "weather.hpp" using namespace DigitalRooster; @@ -27,8 +27,8 @@ using namespace std::chrono; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.Weather"); /*****************************************************************************/ -Weather::Weather(std::shared_ptr confman, QObject* parent) - : cm(confman) { +Weather::Weather(const IWeatherConfigStore& store, QObject* parent) + : cm(store) { qCDebug(CLASS_LC) << Q_FUNC_INFO; // timer starts refresh, refresh calls downloader connect(&timer, SIGNAL(timeout()), this, SLOT(refresh())); @@ -39,7 +39,7 @@ Weather::Weather(std::shared_ptr confman, QObject* parent) timer.setInterval(duration_cast(update_interval)); timer.setSingleShot(false); timer.start(); - downloader.doDownload(create_weather_uri(cm->get_weather_config())); + downloader.doDownload(create_weather_uri(cm.get_weather_config())); } /*****************************************************************************/ @@ -59,7 +59,7 @@ std::chrono::seconds Weather::get_update_interval() const { /*****************************************************************************/ void Weather::refresh() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - downloader.doDownload(create_weather_uri(cm->get_weather_config())); + downloader.doDownload(create_weather_uri(cm.get_weather_config())); } /*****************************************************************************/ @@ -79,15 +79,15 @@ void Weather::parse_response(QByteArray content) { } /*****************************************************************************/ -QUrl DigitalRooster::create_weather_uri(const WeatherConfig* cfg) { +QUrl DigitalRooster::create_weather_uri(const WeatherConfig& cfg) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QString request_str({"http://api.openweathermap.org/data/2.5/weather?"}); request_str.reserve(512); request_str += "id="; - request_str += cfg->get_location_id(); + request_str += cfg.get_location_id(); request_str += "&units=metric"; request_str += "&appid="; - request_str += cfg->get_api_token(); + request_str += cfg.get_api_token(); request_str += "&lang=en"; //default english return QUrl(request_str); } @@ -142,7 +142,7 @@ QJsonObject WeatherConfig::to_json_object() const{ } /*****************************************************************************/ -std::unique_ptr WeatherConfig::from_json_object(const QJsonObject& json) { +WeatherConfig WeatherConfig::from_json_object(const QJsonObject& json) { qCDebug(CLASS_LC) << Q_FUNC_INFO; if (json[KEY_WEATHER_LOCATION_ID].toString().isEmpty()) { throw std::invalid_argument("Json Weather has no location id"); @@ -153,7 +153,7 @@ std::unique_ptr WeatherConfig::from_json_object(const QJsonObject auto interval = std::chrono::seconds(json[KEY_UPDATE_INTERVAL].toInt(3600LL)); - return std::make_unique(json[KEY_WEATHER_API_KEY].toString(), + return WeatherConfig(json[KEY_WEATHER_API_KEY].toString(), json[KEY_WEATHER_LOCATION_ID].toString(), interval); } /*****************************************************************************/ diff --git a/qtgui/alarmlistmodel.cpp b/qtgui/alarmlistmodel.cpp index a37b882..2b8f1e4 100644 --- a/qtgui/alarmlistmodel.cpp +++ b/qtgui/alarmlistmodel.cpp @@ -11,36 +11,34 @@ * SPDX-License-Identifier: GPL-3.0-or-later} ******************************************************************************/ #include -#include #include +#include #include #include "alarm.hpp" #include "alarmlistmodel.hpp" -#include "configuration_manager.hpp" using namespace DigitalRooster; static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.AlarmListModel"); /******************************************************************************/ - -AlarmListModel::AlarmListModel( - std::shared_ptr confman, - QObject* parent) +AlarmListModel::AlarmListModel(IAlarmStore& store, QObject* parent) : QAbstractListModel(parent) - , cm(confman) { + , cm(store) { } /******************************************************************************/ - -AlarmListModel::AlarmListModel(QObject* parent) - : QAbstractListModel(parent) - , cm(nullptr) { +bool AlarmListModel::check_selection(int row) const { + auto sz = cm.get_alarms().size(); + if (row < 0 || row >= sz) { + qWarning() << "Invalid Selection"; + return false; + } + return true; } /******************************************************************************/ - QHash AlarmListModel::roleNames() const { QHash roles; @@ -51,24 +49,23 @@ QHash AlarmListModel::roleNames() const { roles[EnabledRole] = "alarmEnabled"; return roles; } -/******************************************************************************/ +/******************************************************************************/ int AlarmListModel::rowCount(const QModelIndex& /*parent */) const { - if (cm->get_alarms().size() <= 0) { - qWarning() << " alarms configured "; + auto sz = cm.get_alarms().size(); + if (sz <= 0) { + qWarning() << "no alarms configured "; } - return cm->get_alarms().size(); + return sz; } /******************************************************************************/ QVariant AlarmListModel::data(const QModelIndex& index, int role) const { - if (cm->get_alarms().size() <= 0) - return QVariant(); - - if (index.row() < 0 || index.row() >= cm->get_alarms().size()) + if (!check_selection(index.row())) { return QVariant(); + } - auto alarm = cm->get_alarms().at(index.row()); + auto alarm = cm.get_alarms().at(index.row()); switch (role) { case PeriodicityRole: @@ -87,22 +84,21 @@ QVariant AlarmListModel::data(const QModelIndex& index, int role) const { } /******************************************************************************/ void AlarmListModel::set_enabled(int row, bool enabled) { - if (row < 0 || row >= cm->get_alarms().size()) { - qWarning() << "Invalid Selection"; + if (!check_selection(row)) { return; } - cm->get_alarms().at(row)->enable(enabled); + cm.get_alarms().at(row)->enable(enabled); emit dataChanged(index(row, 0), index(row, 0), {EnabledRole}); } /******************************************************************************/ DigitalRooster::Alarm* AlarmListModel::get_alarm(int row) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - if (row < 0 || row >= cm->get_alarms().size()) { - qWarning() << "Invalid Selection"; + if (!check_selection(row)) { + qCCritical(CLASS_LC) << Q_FUNC_INFO << "invalid row!"; + return nullptr; } - auto ret = cm->get_alarms().at(row).get(); + auto ret = cm.get_alarms().at(row).get(); QQmlEngine::setObjectOwnership(ret, QQmlEngine::CppOwnership); return ret; @@ -110,11 +106,9 @@ DigitalRooster::Alarm* AlarmListModel::get_alarm(int row) { /******************************************************************************/ void AlarmListModel::update_row(int row) { - qCDebug(CLASS_LC) << Q_FUNC_INFO << row; - if (row < 0 || row >= cm->get_alarms().size()) { - qWarning() << "Invalid Selection"; + if (!check_selection(row)) { + return; } - emit dataChanged(index(row, 0), index(row, 0), {TimeRole, EnabledRole, PeriodicityRole, PeriodStringRole, UriRole}); } @@ -122,10 +116,11 @@ void AlarmListModel::update_row(int row) { /*****************************************************************************/ bool AlarmListModel::removeRows( - int row, int count, const QModelIndex& /*parent */){ - qCWarning(CLASS_LC) << Q_FUNC_INFO << "is not implemented!"; - //cm->delete_alarm(id); - return true; + int row, int count, const QModelIndex& /*parent */) { + qCWarning(CLASS_LC) << Q_FUNC_INFO << "is not implemented!"; + // TODO: + // cm->delete_alarm(id); + return true; } /*****************************************************************************/ @@ -133,9 +128,9 @@ int AlarmListModel::delete_alarm(qint64 row) { qCDebug(CLASS_LC) << Q_FUNC_INFO << row; beginRemoveRows(QModelIndex(), row, row); try { - auto alarm = cm->get_alarms().at(row); + auto alarm = cm.get_alarms().at(row); if (alarm) { - cm->delete_alarm(alarm->get_id()); + cm.delete_alarm(alarm->get_id()); } } catch (std::out_of_range& exc) { qCWarning(CLASS_LC) << Q_FUNC_INFO << " Alarm not found! "; @@ -148,11 +143,13 @@ int AlarmListModel::delete_alarm(qint64 row) { DigitalRooster::Alarm* AlarmListModel::create_alarm() { qCDebug(CLASS_LC) << Q_FUNC_INFO; auto new_alarm = std::make_shared(); - new_alarm->set_media(cm->get_stream_sources().at(0)); - new_alarm->set_time(QTime::fromString("06:30","hh:mm")); + // TODO: Can we do without access to streamsources? + // new_alarm->set_media(cm->get_stream_sources().at(0)); + + new_alarm->set_time(QTime::fromString("06:30", "hh:mm")); beginInsertRows(QModelIndex(), rowCount(), rowCount()); QQmlEngine::setObjectOwnership(new_alarm.get(), QQmlEngine::CppOwnership); - cm->add_alarm(new_alarm); + cm.add_alarm(new_alarm); endInsertRows(); return new_alarm.get(); } diff --git a/qtgui/alarmlistmodel.hpp b/qtgui/alarmlistmodel.hpp index 1228bea..fc8594b 100644 --- a/qtgui/alarmlistmodel.hpp +++ b/qtgui/alarmlistmodel.hpp @@ -16,9 +16,10 @@ #include #include +#include "IAlarmStore.hpp" + namespace DigitalRooster { class Alarm; -class ConfigurationManager; /** * ListModel to show alarms in QML Gui @@ -26,11 +27,13 @@ class ConfigurationManager; class AlarmListModel : public QAbstractListModel { Q_OBJECT public: - explicit AlarmListModel(QObject* parent = nullptr); - + /** + * Constructor for AlarmListModel + * @param store Interface to get/update/delete alarms + * @param parent QObject hierarchy manage lifetime + */ explicit AlarmListModel( - std::shared_ptr confman, - QObject* parent = nullptr); + IAlarmStore& store, QObject* parent = nullptr); enum AlarmRoles { PeriodicityRole = Qt::UserRole + 1, @@ -71,7 +74,14 @@ class AlarmListModel : public QAbstractListModel { QHash roleNames() const; private: - std::shared_ptr cm; + IAlarmStore& cm; + + /** + * Check if valid row was selected + * @param row current row + * @return true if row corresponds to alarm in store + */ + bool check_selection(int row) const; }; } // namespace DigitalRooster diff --git a/qtgui/iradiolistmodel.cpp b/qtgui/iradiolistmodel.cpp index 7fa873b..0f35a22 100644 --- a/qtgui/iradiolistmodel.cpp +++ b/qtgui/iradiolistmodel.cpp @@ -17,26 +17,17 @@ #include #include "PlayableItem.hpp" -#include "configuration_manager.hpp" #include "iradiolistmodel.hpp" #include "mediaplayerproxy.hpp" using namespace DigitalRooster; /*****************************************************************************/ -IRadioListModel::IRadioListModel( - std::shared_ptr confman, - std::shared_ptr pp, QObject* parent) +IRadioListModel::IRadioListModel(IStationStore& store, + MediaPlayer& mp, QObject* parent) : QAbstractListModel(parent) - , cm(confman) - , mpp(pp) { -} - - -/*****************************************************************************/ -IRadioListModel::IRadioListModel(QObject* parent) - : QAbstractListModel(parent) - , cm(nullptr) { + , cm(store) + , mpp(mp) { } /*****************************************************************************/ @@ -50,35 +41,37 @@ QHash IRadioListModel::roleNames() const { /*****************************************************************************/ int IRadioListModel::rowCount(const QModelIndex& /*parent */) const { // qDebug() << __FUNCTION__; - if (cm->get_stream_sources().size() <= 0) { + auto sz = cm.get_stations().size(); + if (sz <= 0) { qWarning() << " no stations "; } - return cm->get_stream_sources().size(); + return sz; } /*****************************************************************************/ QUrl IRadioListModel::get_station_url(int index) { - auto pi = cm->get_stream_sources().at(index); + auto pi = cm.get_stations().at(index); return pi->get_url(); } /*****************************************************************************/ void IRadioListModel::send_to_player(int index) { - auto station = cm->get_stream_sources().at(index); - mpp->set_media(station); - mpp->play(); + auto station = cm.get_stations().at(index); + mpp.set_media(station); + mpp.play(); } /*****************************************************************************/ QVariant IRadioListModel::data(const QModelIndex& index, int role) const { // qDebug() << __FUNCTION__ << "(" << index.row() << ")"; - if (cm->get_stream_sources().size() <= 0) + auto sz = cm.get_stations().size(); + if (sz <= 0) return QVariant(); - if (index.row() < 0 || index.row() >= cm->get_stream_sources().size()) + if (index.row() < 0 || index.row() >= sz) return QVariant(); - auto station = cm->get_stream_sources().at(index.row()); + auto station = cm.get_stations().at(index.row()); switch (role) { case StationNameRole: diff --git a/qtgui/iradiolistmodel.hpp b/qtgui/iradiolistmodel.hpp index 6d3f42e..451f0ee 100644 --- a/qtgui/iradiolistmodel.hpp +++ b/qtgui/iradiolistmodel.hpp @@ -17,10 +17,11 @@ #include #include +#include "IStationStore.hpp" + namespace DigitalRooster { class PlayableItem; -class MediaPlayerProxy; -class ConfigurationManager; +class MediaPlayer; /** * Simple Model for displaying Internet Radio stations in QML List @@ -28,11 +29,15 @@ class ConfigurationManager; class IRadioListModel : public QAbstractListModel { Q_OBJECT public: - explicit IRadioListModel(QObject* parent = nullptr); - - IRadioListModel( - std::shared_ptr confman, - std::shared_ptr pp, + /** + * Constructor + * @param store cannot be a refernce because Object must be default + * constructable in QML/MOC Code + * @param pp Player + * @param parent + */ + IRadioListModel(IStationStore& store, + DigitalRooster::MediaPlayer& mp, QObject* parent = nullptr); enum IRadioStationRoles { StationNameRole = Qt::UserRole + 1, UriRole }; @@ -48,8 +53,8 @@ class IRadioListModel : public QAbstractListModel { QHash roleNames() const; private: - std::shared_ptr cm; - std::shared_ptr mpp; + IStationStore& cm; + MediaPlayer& mpp; }; } // namespace DigitalRooster diff --git a/qtgui/main.cpp b/qtgui/main.cpp index 4a7d3f3..e45578d 100644 --- a/qtgui/main.cpp +++ b/qtgui/main.cpp @@ -31,6 +31,7 @@ #include "hwif/hardware_control.hpp" // Local classes +#include "PlayableItem.hpp" // to register type #include "alarm.hpp" #include "alarmdispatcher.hpp" #include "alarmlistmodel.hpp" @@ -148,13 +149,12 @@ int main(int argc, char* argv[]) { /* * Read configuration */ - auto cm = std::make_shared( - cmdline.value(confpath), cmdline.value(cachedir)); - cm->update_configuration(); + ConfigurationManager cm(cmdline.value(confpath), cmdline.value(cachedir)); + cm.update_configuration(); // Initialize Player - auto playerproxy = std::make_shared(); - playerproxy->set_volume(cm->get_volume()); + MediaPlayerProxy playerproxy; + playerproxy.set_volume(cm.get_volume()); AlarmDispatcher alarmdispatcher(cm); AlarmMonitor alarmmonitor(playerproxy, std::chrono::seconds(20)); @@ -164,8 +164,8 @@ int main(int argc, char* argv[]) { SLOT(alarm_triggered(std::shared_ptr))); PodcastSourceModel psmodel(cm, playerproxy); - IRadioListModel iradiolistmodel(cm, playerproxy); AlarmListModel alarmlistmodel(cm); + IRadioListModel iradiolistmodel(cm, playerproxy); WifiListModel wifilistmodel; Weather weather(cm); @@ -183,8 +183,8 @@ int main(int argc, char* argv[]) { QObject::connect(&power, SIGNAL(becoming_active()), &brightness, SLOT(restore_active_brightness())); /* Powercontrol standby stops player */ - QObject::connect( - &power, SIGNAL(going_in_standby()), playerproxy.get(), SLOT(stop())); + QObject::connect(&power, &PowerControl::going_in_standby, &playerproxy, + &MediaPlayer::stop); /* Wire shutdown and reboot requests to hardware */ QObject::connect(&power, &PowerControl::reboot_request, &hwctrl, &Hal::HardwareControl::system_reboot); @@ -199,7 +199,7 @@ int main(int argc, char* argv[]) { QObject::connect(&sleeptimer, &SleepTimer::sleep_timer_elapsed, &power, &PowerControl::standby); /* Sleeptimer resets when player changes state to play */ - QObject::connect(playerproxy.get(), &MediaPlayer::playback_state_changed, + QObject::connect(&playerproxy, &MediaPlayer::playback_state_changed, &sleeptimer, &SleepTimer::playback_state_changed); /* Sleeptimer also monitors alarms */ QObject::connect(&alarmdispatcher, @@ -216,50 +216,54 @@ int main(int argc, char* argv[]) { &VolumeButton::process_rotary_event); /* wire volume button to consumers */ - QObject::connect( - &volbtn, SIGNAL(button_released()), &power, SLOT(toggle_power_state())); - QObject::connect(&volbtn, SIGNAL(volume_incremented(int)), - playerproxy.get(), SLOT(increment_volume(int))); + QObject::connect(&volbtn, &VolumeButton::button_released, &power, + &PowerControl::toggle_power_state); + QObject::connect(&volbtn, &VolumeButton::volume_incremented, &playerproxy, + &MediaPlayer::increment_volume); /* Standby deactivates Volume button events */ QObject::connect(&power, SIGNAL(active(bool)), &volbtn, SLOT(monitor_rotary_button(bool))); - QObject::connect(playerproxy.get(), SIGNAL(volume_changed(int)), cm.get(), - SLOT(set_volume(int))); + QObject::connect(&playerproxy, &MediaPlayer::volume_changed, &cm, + &ConfigurationManager::set_volume); /* we start in standby */ power.standby(); /* - * QML Setup + * QML Setup Dynamically createable Types + * All Elements/Lists are created in C++ */ - qmlRegisterType( - "ruschi.PodcastEpisodeModel", 1, 0, "PodcastEpisodeModel"); - qmlRegisterType( - "ruschi.PodcastEpisode", 1, 0, "PodcastEpisode"); - qmlRegisterType("ruschi.Alarm", 1, 0, "Alarm"); - qmlRegisterType( - "ruschi.IRadioListModel", 1, 0, "IRadioListModel"); - qmlRegisterType( - "ruschi.PlayableItem", 1, 0, "PlayableItem"); - qmlRegisterType( - "ruschi.WifiListModel", 1, 0, "WifiListModel"); + qmlRegisterUncreatableType( + "ruschi.PodcastEpisodeModel", 1, 0, "PodcastEpisodeModel", + "QML must not instantiate PodcastEpisodeModel!"); + qmlRegisterUncreatableType( + "ruschi.PodcastEpisode", 1, 0, "PodcastEpisode", + "QML must not instantiate PodcastEpisode!"); + qmlRegisterUncreatableType( + "ruschi.Alarm", 1, 0, "Alarm", "QML must not instatiate Alarm!"); + qmlRegisterUncreatableType( + "ruschi.PlayableItem", 1, 0, "PlayableItem", + "QML must not instantiate PlayableItem!"); + qmlRegisterUncreatableType( + "ruschi.WifiListModel", 1, 0, "WifiListModel", + "QML must not instantiate WifiListModel!"); QQmlApplicationEngine view; QQmlContext* ctxt = view.rootContext(); - WifiControl* wifictrl = WifiControl::get_instance(cm.get()); + WifiControl* wifictrl = WifiControl::get_instance(&cm); ctxt->setContextProperty("wifictrl", wifictrl); ctxt->setContextProperty("wifilistmodel", &wifilistmodel); QObject::connect(wifictrl, &WifiControl::networks_found, &wifilistmodel, &WifiListModel::update_scan_results); ctxt->setContextProperty("podcastmodel", &psmodel); - ctxt->setContextProperty("playerProxy", playerproxy.get()); + ctxt->setContextProperty("playerProxy", &playerproxy); ctxt->setContextProperty("alarmlistmodel", &alarmlistmodel); ctxt->setContextProperty("iradiolistmodel", &iradiolistmodel); ctxt->setContextProperty("weather", &weather); - ctxt->setContextProperty("config", cm.get()); + ctxt->setContextProperty("config", &cm); ctxt->setContextProperty("powerControl", &power); ctxt->setContextProperty("brightnessControl", &brightness); ctxt->setContextProperty("volumeButton", &volbtn); diff --git a/qtgui/podcastepisodemodel.cpp b/qtgui/podcastepisodemodel.cpp index 9347111..1ca975b 100644 --- a/qtgui/podcastepisodemodel.cpp +++ b/qtgui/podcastepisodemodel.cpp @@ -25,16 +25,10 @@ using namespace DigitalRooster; /*****************************************************************************/ PodcastEpisodeModel::PodcastEpisodeModel( const QVector>* ep, - std::shared_ptr pp, QObject* parent) + MediaPlayer& mp, QObject* parent) : QAbstractListModel(parent) , episodes(ep) - , mpp(pp) { -} - -/*****************************************************************************/ -PodcastEpisodeModel::PodcastEpisodeModel(QObject* parent) - : QAbstractListModel(parent) - , episodes(nullptr) { + , mpp(mp) { } /*****************************************************************************/ @@ -79,8 +73,8 @@ PodcastEpisode* PodcastEpisodeModel::get_episode(int index) { /*****************************************************************************/ void PodcastEpisodeModel::send_to_player(int index) { auto ep = episodes->at(index); - mpp->set_media(ep); - mpp->play(); + mpp.set_media(ep); + mpp.play(); } /*****************************************************************************/ diff --git a/qtgui/podcastepisodemodel.hpp b/qtgui/podcastepisodemodel.hpp index ec0e326..128740c 100644 --- a/qtgui/podcastepisodemodel.hpp +++ b/qtgui/podcastepisodemodel.hpp @@ -22,21 +22,19 @@ namespace DigitalRooster { class ConfigurationManager; class PodcastEpisode; -class MediaPlayerProxy; +class MediaPlayer; -} // namespace DigitalRooster +/** + * Model for list of Episodes of one podcast source + */ class PodcastEpisodeModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int currentIndex READ get_current_index WRITE set_current_index NOTIFY current_index_changed) public: - explicit PodcastEpisodeModel(QObject* parent = nullptr); - PodcastEpisodeModel( - const QVector>* - episodes, - std::shared_ptr pp, - QObject* parent = nullptr); + const QVector>* episodes, + MediaPlayer& mp, QObject* parent = nullptr); enum PodcastEpisodeRoles { DisplayNameRole = Qt::UserRole + 1, @@ -53,9 +51,7 @@ class PodcastEpisodeModel : public QAbstractListModel { QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - void set_episodes( - const QVector>* - episodes); + void set_episodes(const QVector>* episodes); const QString& getName() { return name; @@ -84,12 +80,12 @@ class PodcastEpisodeModel : public QAbstractListModel { QHash roleNames() const; private: - const QVector>* episodes; - std::shared_ptr mpp; - + const QVector>* episodes; + MediaPlayer& mpp; int currentIndex = -1; QString name; }; +} // namespace DigitalRooster #endif diff --git a/qtgui/podcastsourcemodel.cpp b/qtgui/podcastsourcemodel.cpp index 6eb0b53..62c4c18 100644 --- a/qtgui/podcastsourcemodel.cpp +++ b/qtgui/podcastsourcemodel.cpp @@ -8,7 +8,7 @@ * \copyright 2018 Thomas Ruschival * This file is licensed under GNU PUBLIC LICENSE Version 3 or later * SPDX-License-Identifier: GPL-3.0-or-later -******************************************************************************/ + ******************************************************************************/ #include #include #include @@ -16,7 +16,6 @@ #include "PodcastSource.hpp" -#include "configuration_manager.hpp" #include "mediaplayerproxy.hpp" #include "podcastepisodemodel.hpp" #include "podcastsourcemodel.hpp" @@ -27,13 +26,13 @@ static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.PodcastSourceModel"); /*****************************************************************************/ PodcastSourceModel::PodcastSourceModel( - std::shared_ptr confman, - std::shared_ptr pp, QObject* parent) + IPodcastStore& store, + MediaPlayer& mp, QObject* parent) : QAbstractListModel(parent) - , cm(confman) - , mpp(pp) { + , cm(store) + , mpp(mp) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto v = cm->get_podcast_sources(); + auto v = cm.get_podcast_sources(); for (auto ps : v) { connect( ps.get(), SIGNAL(titleChanged()), this, SLOT(newDataAvailable())); @@ -41,7 +40,6 @@ PodcastSourceModel::PodcastSourceModel( } /*****************************************************************************/ - QHash PodcastSourceModel::roleNames() const { QHash roles; roles[DisplayNameRole] = "display_name"; @@ -51,11 +49,12 @@ QHash PodcastSourceModel::roleNames() const { roles[ImageRole] = "logo_image"; return roles; } + /*****************************************************************************/ int PodcastSourceModel::rowCount(const QModelIndex& /*parent */) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - return cm->get_podcast_sources().size(); + return cm.get_podcast_sources().size(); } /*****************************************************************************/ @@ -68,17 +67,18 @@ void PodcastSourceModel::newDataAvailable() { PodcastEpisodeModel* PodcastSourceModel::get_episodes(int index) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto v = cm->get_podcast_sources(); + auto v = cm.get_podcast_sources(); if (index < 0 || index >= v.size()) return nullptr; + /* Lifetime will be managed in QML! */ return new PodcastEpisodeModel(&(v[index]->get_episodes()), mpp, this); } /*****************************************************************************/ QVariant PodcastSourceModel::data(const QModelIndex& index, int role) const { qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto v = cm->get_podcast_sources(); + auto v = cm.get_podcast_sources(); if (index.row() < 0 || index.row() >= v.size()) return QVariant(); QString desc; @@ -92,8 +92,8 @@ QVariant PodcastSourceModel::data(const QModelIndex& index, int role) const { case DisplayCountRole: return ps->get_episodes().size(); case DescriptionRole: - desc = ps->get_description(); - desc.remove(QRegExp("<[^>]*>")); //Strip HTML tags + desc = ps->get_description(); + desc.remove(QRegExp("<[^>]*>")); // Strip HTML tags return QVariant(desc); case ImageRole: return ps->get_icon(); @@ -105,7 +105,7 @@ QVariant PodcastSourceModel::data(const QModelIndex& index, int role) const { void PodcastSourceModel::refresh(int index) { qCDebug(CLASS_LC) << Q_FUNC_INFO; try { - cm->get_podcast_source_by_index(index)->refresh(); + cm.get_podcast_source_by_index(index)->refresh(); } catch (std::out_of_range&) { qCCritical(CLASS_LC) << "index out of range " << index; } @@ -115,7 +115,7 @@ void PodcastSourceModel::refresh(int index) { void PodcastSourceModel::purge(int index) { qCDebug(CLASS_LC) << Q_FUNC_INFO; try { - cm->get_podcast_source_by_index(index)->purge(); + cm.get_podcast_source_by_index(index)->purge(); } catch (std::out_of_range&) { qCCritical(CLASS_LC) << "index out of range " << index; } @@ -126,7 +126,7 @@ void PodcastSourceModel::remove(int index) { qCDebug(CLASS_LC) << Q_FUNC_INFO; beginRemoveRows(QModelIndex(), index, index); try { - cm->remove_podcast_source_by_index(index); + cm.remove_podcast_source_by_index(index); } catch (std::out_of_range&) { qCCritical(CLASS_LC) << "index out of range " << index; } diff --git a/qtgui/podcastsourcemodel.hpp b/qtgui/podcastsourcemodel.hpp index c208d57..d502d8b 100644 --- a/qtgui/podcastsourcemodel.hpp +++ b/qtgui/podcastsourcemodel.hpp @@ -16,20 +16,26 @@ #include #include -namespace DigitalRooster { -class ConfigurationManager; -class MediaPlayerProxy; -} // namespace DigitalRooster +#include "IPodcastStore.hpp" +namespace DigitalRooster { +class MediaPlayer; class PodcastEpisodeModel; +/** + * ListModel for Podcast Stations + */ class PodcastSourceModel : public QAbstractListModel { Q_OBJECT public: + /** + * Create a PodcastSource model + * @param store info on PodcastSources + * @param mp Mediaplayer will be passed on to EpisodesModel + * @param parent + */ PodcastSourceModel( - std::shared_ptr confman, - std::shared_ptr pp, - QObject* parent = nullptr); + IPodcastStore& store, MediaPlayer& mp, QObject* parent = nullptr); enum PodcastSourceRoles { DisplayNameRole = Qt::UserRole + 1, @@ -43,7 +49,7 @@ class PodcastSourceModel : public QAbstractListModel { QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - Q_INVOKABLE PodcastEpisodeModel* get_episodes(int index); + Q_INVOKABLE DigitalRooster::PodcastEpisodeModel* get_episodes(int index); Q_INVOKABLE void refresh(int index); Q_INVOKABLE void purge(int index); @@ -56,8 +62,9 @@ public slots: QHash roleNames() const; private: - std::shared_ptr cm; - std::shared_ptr mpp; + IPodcastStore& cm; + MediaPlayer& mpp; }; +} // namespace DigitalRooster #endif /* QTGUI_PODCASTSOURCEMODEL_HPP_ */ diff --git a/qtgui/qml/AlarmEditDialog.qml b/qtgui/qml/AlarmEditDialog.qml index ab169ca..046e379 100644 --- a/qtgui/qml/AlarmEditDialog.qml +++ b/qtgui/qml/AlarmEditDialog.qml @@ -83,7 +83,7 @@ Popup { currentIndex: currentAlarm.period_id; onActivated: { - console.log("new index" + currentIndex); + console.log("new period:" + currentIndex); currentAlarm.period_id = currentIndex; } } diff --git a/test/cm_mock.hpp b/test/cm_mock.hpp index 34c3daf..a12a23a 100644 --- a/test/cm_mock.hpp +++ b/test/cm_mock.hpp @@ -32,15 +32,16 @@ class CmMock : public DigitalRooster::ConfigurationManager { }; - MOCK_METHOD0( - get_alarm_list, QVector>&()); + MOCK_CONST_METHOD0( + get_alarms, QVector>&()); - MOCK_METHOD0(get_weather_cfg, const DigitalRooster::WeatherConfig*()); + MOCK_CONST_METHOD0( + get_weather_config, const DigitalRooster::WeatherConfig&()); - MOCK_CONST_METHOD0(do_get_brightness_sb, int()); - MOCK_CONST_METHOD0(do_get_brightness_act, int()); + MOCK_CONST_METHOD0(get_active_brightness, int()); + MOCK_CONST_METHOD0(get_standby_brightness, int()); MOCK_CONST_METHOD0(do_get_volume, int()); - MOCK_METHOD1(do_set_brightness_act, void(int ab)); + MOCK_METHOD1(set_active_brightness, void(int ab)); MOCK_CONST_METHOD0(get_wpa_socket_name, QString()); MOCK_CONST_METHOD0(get_alarm_timeout, std::chrono::minutes()); MOCK_CONST_METHOD0(get_sleep_timeout, std::chrono::minutes()); diff --git a/test/test_alarmdispatcher.cpp b/test/test_alarmdispatcher.cpp index e3971ff..d7a5ab3 100644 --- a/test/test_alarmdispatcher.cpp +++ b/test/test_alarmdispatcher.cpp @@ -33,10 +33,12 @@ using ::testing::AtLeast; // Fixture to inject fake clock as the global clock class AlarmDispatcherFixture : public ::testing::Test { public: + AlarmDispatcherFixture() + : mc(new MockClock) + , dut(cm){}; + // Make our own clock to be the wallclock void SetUp() { - cm = std::make_shared(); - mc = std::make_shared(); DigitalRooster::wallclock = std::static_pointer_cast(mc); }; @@ -48,23 +50,23 @@ class AlarmDispatcherFixture : public ::testing::Test { protected: std::shared_ptr mc; - std::shared_ptr cm; + CmMock cm; + AlarmDispatcher dut; }; /*****************************************************************************/ TEST_F(AlarmDispatcherFixture, callsGetAlarms) { - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // Friday 8:29:45, just before and just before and after the alarm time EXPECT_CALL(*(mc.get()), get_time()) .Times(AtLeast(1)) .WillRepeatedly( Return(QDateTime::fromString("2018-09-26T08:29:45", Qt::ISODate))); - AlarmDispatcher a(cm); - a.check_alarms(); + dut.check_alarms(); } /*****************************************************************************/ @@ -72,11 +74,11 @@ TEST_F(AlarmDispatcherFixture, dispatchSingleAlarm) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:30:00", "hh:mm:ss"), Alarm::Daily); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // Friday 8:29:45, just before and just before and after the alarm time EXPECT_CALL(*(mc.get()), get_time()) @@ -84,15 +86,14 @@ TEST_F(AlarmDispatcherFixture, dispatchSingleAlarm) { .WillRepeatedly( Return(QDateTime::fromString("2018-09-26T08:29:45", Qt::ISODate))); - AlarmDispatcher a(cm); QSignalSpy spy1( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy1.isValid()); - QSignalSpy spy2(&a, SIGNAL(alarm_triggered())); + QSignalSpy spy2(&dut, SIGNAL(alarm_triggered())); ASSERT_TRUE(spy2.isValid()); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy1.count(), 1); ASSERT_EQ(spy2.count(), 1); } @@ -103,23 +104,22 @@ TEST_F(AlarmDispatcherFixture, disableSingleAlarmAfterDispatch) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:30:00", "hh:mm:ss"), Alarm::Once); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // Friday 8:29:45, just before and just before and after the alarm time EXPECT_CALL(*(mc.get()), get_time()) .Times(AtLeast(1)) .WillRepeatedly( Return(QDateTime::fromString("2018-09-26T08:29:45", Qt::ISODate))); - AlarmDispatcher a(cm); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); - a.check_alarms(); + dut.check_alarms(); ASSERT_FALSE(alm->is_enabled()); ASSERT_EQ(spy.count(), 1); } @@ -130,11 +130,11 @@ TEST_F(AlarmDispatcherFixture, DontDispatchDisabledAlarm) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:30:00", "hh:mm:ss"), Alarm::Daily, false); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // just before and just after the alarm time EXPECT_CALL(*(mc.get()), get_time()) @@ -142,13 +142,11 @@ TEST_F(AlarmDispatcherFixture, DontDispatchDisabledAlarm) { .WillRepeatedly( Return(QDateTime::fromString("2018-09-26T08:30:02", Qt::ISODate))); - AlarmDispatcher a(cm); - QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 0); } /*****************************************************************************/ @@ -156,23 +154,22 @@ TEST_F(AlarmDispatcherFixture, DontDispatchPastAlarms) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:25:00", "hh:mm:ss"), Alarm::Daily, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // 1 Minute after Alarm time EXPECT_CALL(*(mc.get()), get_time()) .Times(AtLeast(1)) .WillRepeatedly( Return(QDateTime::fromString("2018-09-26T08:26:00", Qt::ISODate))); - AlarmDispatcher a(cm); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 0); } /*****************************************************************************/ @@ -180,7 +177,7 @@ TEST_F(AlarmDispatcherFixture, DispatchAlarmsWithSlightOffset) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:30:00", "hh:mm:ss"), Alarm::Workdays, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Friday 8:29:45, just before and just before and after the alarm time EXPECT_CALL(*(mc.get()), get_time()) @@ -190,16 +187,15 @@ TEST_F(AlarmDispatcherFixture, DispatchAlarmsWithSlightOffset) { .WillRepeatedly( Return(QDateTime::fromString("2018-09-28T08:30:20", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(2) - .WillRepeatedly(ReturnRef(cm->alarms)); - AlarmDispatcher a(cm); + .WillRepeatedly(ReturnRef(cm.alarms)); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); - a.check_alarms(); - a.check_alarms(); + dut.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 1); // past alarm should not be played } /*****************************************************************************/ @@ -207,7 +203,7 @@ TEST_F(AlarmDispatcherFixture, Workdays_Friday) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:30:00", "hh:mm:ss"), Alarm::Workdays, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Friday 8:29:45, just before the alarm time EXPECT_CALL(*(mc.get()), get_time()) @@ -215,14 +211,13 @@ TEST_F(AlarmDispatcherFixture, Workdays_Friday) { .WillOnce( Return(QDateTime::fromString("2018-09-26T08:29:45", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); - AlarmDispatcher a(cm); + .WillRepeatedly(ReturnRef(cm.alarms)); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); - a.check_alarms(); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); + dut.check_alarms(); ASSERT_EQ(spy.count(), 1); } /*****************************************************************************/ @@ -232,7 +227,7 @@ TEST_F(AlarmDispatcherFixture, Workdays_Monday) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), timepoint, Alarm::Workdays, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Monday 8:29 EXPECT_CALL(*(mc.get()), get_time()) @@ -240,15 +235,14 @@ TEST_F(AlarmDispatcherFixture, Workdays_Monday) { .WillOnce( Return(QDateTime::fromString("2018-09-14T08:29:45", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); - AlarmDispatcher a(cm); + .WillRepeatedly(ReturnRef(cm.alarms)); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 1); } /*****************************************************************************/ @@ -258,7 +252,7 @@ TEST_F(AlarmDispatcherFixture, Weekends_Monday) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), timepoint, Alarm::Weekend, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Monday 8:29 EXPECT_CALL(*(mc.get()), get_time()) @@ -267,15 +261,14 @@ TEST_F(AlarmDispatcherFixture, Weekends_Monday) { Return(QDateTime::fromString("2018-09-14T08:29:45", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); - AlarmDispatcher a(cm); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 0); } /*****************************************************************************/ @@ -284,7 +277,7 @@ TEST_F(AlarmDispatcherFixture, Weekends_Saturday) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), timepoint, Alarm::Weekend, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Saturday 8:29 EXPECT_CALL(*(mc.get()), get_time()) @@ -292,15 +285,14 @@ TEST_F(AlarmDispatcherFixture, Weekends_Saturday) { .WillOnce( Return(QDateTime::fromString("2018-09-29T08:29:45", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); - AlarmDispatcher a(cm); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 1); } /*****************************************************************************/ @@ -310,7 +302,7 @@ TEST_F(AlarmDispatcherFixture, Workdays_Sunday) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), timepoint, Alarm::Weekend, true); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); // Friday 8:29:45, just before the alarm time EXPECT_CALL(*(mc.get()), get_time()) @@ -318,15 +310,14 @@ TEST_F(AlarmDispatcherFixture, Workdays_Sunday) { .WillOnce( Return(QDateTime::fromString("2018-09-28T08:29:45", Qt::ISODate))); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); - AlarmDispatcher a(cm); QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); - a.check_alarms(); + dut.check_alarms(); ASSERT_EQ(spy.count(), 0); } @@ -337,17 +328,17 @@ TEST_F(AlarmDispatcherFixture, dispatch2DueAlarms) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:29:48", "hh:mm:ss"), Alarm::Once); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); auto alm2 = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:29:50", "hh:mm:ss"), Alarm::Daily); - cm->alarms.push_back(alm2); + cm.alarms.push_back(alm2); - ASSERT_EQ(cm->alarms.size(), 2); + ASSERT_EQ(cm.alarms.size(), 2); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(1) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); // Friday 8:29:45, just before the alarm time EXPECT_CALL(*(mc.get()), get_time()) .Times(1) @@ -365,15 +356,14 @@ TEST_F(AlarmDispatcherFixture, dispatch2DueAlarms) { /*****************************************************************************/ TEST_F(AlarmDispatcherFixture, LoopTimerTriggers) { - auto cm = std::make_shared(); auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::fromString("08:29:40", "hh:mm:ss"), Alarm::Daily); - cm->alarms.push_back(alm); + cm.alarms.push_back(alm); - EXPECT_CALL(*(cm.get()), get_alarm_list()) + EXPECT_CALL(cm, get_alarms()) .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(cm->alarms)); + .WillRepeatedly(ReturnRef(cm.alarms)); EXPECT_CALL(*(mc.get()), get_time()) .Times(AtLeast(1)) @@ -381,13 +371,12 @@ TEST_F(AlarmDispatcherFixture, LoopTimerTriggers) { Return(QDateTime::fromString("2018-09-28T08:29:40", Qt::ISODate))); /* Interval 1s */ - AlarmDispatcher a(cm); - a.set_interval(std::chrono::seconds(1)); - ASSERT_EQ(a.get_interval(), std::chrono::seconds(1)); + dut.set_interval(std::chrono::seconds(1)); + ASSERT_EQ(dut.get_interval(), std::chrono::seconds(1)); // Only observable signal, actually we want to test if it loops QSignalSpy spy( - &a, SIGNAL(alarm_triggered(std::shared_ptr))); + &dut, SIGNAL(alarm_triggered(std::shared_ptr))); ASSERT_TRUE(spy.isValid()); spy.wait(1100); diff --git a/test/test_alarmmonitor.cpp b/test/test_alarmmonitor.cpp index e405f54..e5b5fa0 100644 --- a/test/test_alarmmonitor.cpp +++ b/test/test_alarmmonitor.cpp @@ -36,7 +36,7 @@ using namespace std::chrono_literals; /*****************************************************************************/ TEST(AlarmMonitor, playsAlarmFuture) { - auto player = std::make_shared(); + PlayerMock player; AlarmMonitor mon(player); ASSERT_EQ(mon.get_state(),AlarmMonitor::Idle); @@ -44,39 +44,38 @@ TEST(AlarmMonitor, playsAlarmFuture) { QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::currentTime().addSecs(3), Alarm::Daily); - EXPECT_CALL(*(player.get()), do_play()).Times(1); - EXPECT_CALL(*(player.get()), do_stop()).Times(1); + EXPECT_CALL(player, do_play()).Times(1); + EXPECT_CALL(player, do_stop()).Times(1); - EXPECT_CALL(*(player.get()), do_set_volume(_)).Times(1); - EXPECT_CALL(*(player.get()), do_set_media(_)).Times(1); + EXPECT_CALL(player, do_set_volume(_)).Times(1); + EXPECT_CALL(player, do_set_media(_)).Times(1); mon.alarm_triggered(alm); ASSERT_EQ(mon.get_state(),AlarmMonitor::ExpectingAlarm); - - player->stop(); + player.stop(); } /*****************************************************************************/ TEST(AlarmMonitor, triggersFallbackForError) { - auto player = std::make_shared(); + PlayerMock player; AlarmMonitor mon(player); auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::currentTime().addSecs(1), Alarm::Daily); - EXPECT_CALL(*(player.get()), do_play()).Times(2); - EXPECT_CALL(*(player.get()), do_set_media(_)).Times(1); - EXPECT_CALL(*(player.get()), do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); + EXPECT_CALL(player, do_play()).Times(2); + EXPECT_CALL(player, do_set_media(_)).Times(1); + EXPECT_CALL(player, do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); // Fallback behavior - EXPECT_CALL(*(player.get()), do_set_volume(50)).Times(1); - EXPECT_CALL(*(player.get()), do_set_playlist(_)).Times(1); + EXPECT_CALL(player, do_set_volume(50)).Times(1); + EXPECT_CALL(player, do_set_playlist(_)).Times(1); QSignalSpy spy(&mon, SIGNAL(state_changed(AlarmMonitor::MonitorState))); ASSERT_TRUE(spy.isValid()); mon.alarm_triggered(alm); - player->emitError(QMediaPlayer::NetworkError); + player.emitError(QMediaPlayer::NetworkError); ASSERT_EQ(spy.count(),2); // ExpectingAlarm, FallBackMode ASSERT_EQ(mon.get_state(),AlarmMonitor::FallBackMode); @@ -85,7 +84,7 @@ TEST(AlarmMonitor, triggersFallbackForError) { /*****************************************************************************/ TEST(AlarmMonitor, triggersFallbackForTimeout) { // Nice mock - we don't care about calls to player - auto player = std::make_shared>(); + NiceMock player; AlarmMonitor mon(player,20ms); auto alm = std::make_shared( @@ -104,23 +103,23 @@ TEST(AlarmMonitor, triggersFallbackForTimeout) { /*****************************************************************************/ TEST(AlarmMonitor, noFallBackIfStoppedNormally) { - auto player = std::make_shared(); + PlayerMock player; AlarmMonitor mon(player); auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::currentTime().addSecs(1), Alarm::Daily); - EXPECT_CALL(*(player.get()), do_play()).Times(1); - EXPECT_CALL(*(player.get()), do_set_media(_)).Times(1); - EXPECT_CALL(*(player.get()), do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); - EXPECT_CALL(*(player.get()), do_error()).Times(AtLeast(1)).WillRepeatedly(Return(QMediaPlayer::NoError)); + EXPECT_CALL(player, do_play()).Times(1); + EXPECT_CALL(player, do_set_media(_)).Times(1); + EXPECT_CALL(player, do_set_volume(DEFAULT_ALARM_VOLUME)).Times(1); + EXPECT_CALL(player, do_error()).Times(AtLeast(1)).WillRepeatedly(Return(QMediaPlayer::NoError)); mon.alarm_triggered(alm); ASSERT_EQ(mon.get_state(),AlarmMonitor::ExpectingAlarm); - player->playback_state_changed(QMediaPlayer::PlayingState); + player.playback_state_changed(QMediaPlayer::PlayingState); ASSERT_EQ(mon.get_state(),AlarmMonitor::AlarmActive); - player->playback_state_changed(QMediaPlayer::StoppedState); + player.playback_state_changed(QMediaPlayer::StoppedState); ASSERT_EQ(mon.get_state(),AlarmMonitor::Idle); } diff --git a/test/test_brightness.cpp b/test/test_brightness.cpp index 851bf53..e2dae57 100644 --- a/test/test_brightness.cpp +++ b/test/test_brightness.cpp @@ -23,10 +23,17 @@ using ::testing::AtLeast; /* log_100 values for 0:5:100 */ const int log_ref[] = {0, 1, 2, 4, 5, 6, 8, 9, 11, 13, 15, 17, 20, 23, 26, 30, 35, 41, 50, 65, 100}; +/*****************************************************************************/ +class BrightnessFixture : public ::testing::Test{ +public: + BrightnessFixture():dut(cm){}; +protected: + CmMock cm; + BrightnessControl dut; +}; /*****************************************************************************/ -TEST(Brightness, lin2log) { - auto cm = std::make_shared(); +TEST_F(BrightnessFixture, lin2log) { BrightnessControl dut(cm); for (int i = 0; i <= 100;) { // cout << dut.lin2log(i) << ", " << endl; @@ -36,36 +43,33 @@ TEST(Brightness, lin2log) { } /*****************************************************************************/ -TEST(Brightness, RestoreActive) { - auto cm = std::make_shared(); +TEST_F(BrightnessFixture,RestoreActive) { // active brightness is read in constructor - EXPECT_CALL(*cm.get(), do_get_brightness_act()) + EXPECT_CALL(cm, get_active_brightness()) .Times(1) .WillOnce(Return(42)); - BrightnessControl dut(cm); + dut.restore_active_brightness(); ASSERT_EQ(dut.get_brightness(), 42); } /*****************************************************************************/ -TEST(Brightness, setBrightness) { - auto cm = std::make_shared(); +TEST_F(BrightnessFixture, setBrightness) { // active brightness is read in constructor - EXPECT_CALL(*cm.get(), do_set_brightness_act(25)) + EXPECT_CALL(cm, set_active_brightness(25)) .Times(1); - BrightnessControl dut(cm); + dut.set_brightness(25); ASSERT_EQ(dut.get_brightness(), 25); } /*****************************************************************************/ -TEST(Brightness, RestoreStandby) { - auto cm = std::make_shared(); +TEST_F(BrightnessFixture, RestoreStandby) { // active brightness is read in constructor - EXPECT_CALL(*cm.get(), do_get_brightness_sb()) + EXPECT_CALL(cm, get_standby_brightness()) .Times(1) .WillOnce(Return(10)); - BrightnessControl dut(cm); + dut.restore_standby_brightness(); ASSERT_EQ(dut.get_brightness(), 10); } diff --git a/test/test_settings.cpp b/test/test_settings.cpp index eea8e19..807cb0b 100644 --- a/test/test_settings.cpp +++ b/test/test_settings.cpp @@ -9,6 +9,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later} * *****************************************************************************/ +#include #include #include #include @@ -19,6 +20,9 @@ #include #include +#include "PlayableItem.hpp" +#include "PodcastSource.hpp" +#include "alarm.hpp" #include "appconstants.hpp" #include "config.h" #include "configuration_manager.hpp" @@ -171,7 +175,7 @@ class SettingsFixture : public virtual ::testing::Test { /*****************************************************************************/ TEST_F(SettingsFixture, read_radio_streams_two_streams) { - auto& v = cm->get_stream_sources(); + auto& v = cm->get_stations(); ASSERT_EQ(2, v.size()); } /*****************************************************************************/ @@ -181,7 +185,7 @@ TEST_F(SettingsFixture, addRadioStation_no_write) { std::make_shared("foo", QUrl("http://bar.baz"))); cm->add_radio_station( std::make_shared("ref", QUrl("http://gmx.net"))); - auto& v = cm->get_stream_sources(); + auto& v = cm->get_stations(); ASSERT_EQ(4, v.size()); } /*****************************************************************************/ @@ -197,7 +201,7 @@ TEST_F(SettingsFixture, addRadioStation_write) { } ConfigurationManager control(filename, TEST_FILE_PATH); control.update_configuration(); - auto& v = control.get_stream_sources(); + auto& v = control.get_stations(); ASSERT_EQ(4, v.size()); auto stream = v[2]; @@ -213,7 +217,7 @@ TEST_F(SettingsFixture, add_podcast_source) { auto size_before = cm->get_podcast_sources().size(); cm->add_podcast_source(ps); ASSERT_EQ(spy.count(), 1); - ASSERT_EQ(cm->get_podcast_sources().size(), size_before+1); + ASSERT_EQ(cm->get_podcast_sources().size(), size_before + 1); } /*****************************************************************************/ @@ -224,26 +228,25 @@ TEST_F(SettingsFixture, get_podcast_source_throws) { /*****************************************************************************/ TEST_F(SettingsFixture, get_podcast_source_ok) { - auto& v = cm->get_podcast_sources(); - auto uid = v[0]->get_id(); - auto item = cm->get_podcast_source(uid); - ASSERT_EQ(item,v[0].get()); + auto& v = cm->get_podcast_sources(); + auto uid = v[0]->get_id(); + auto item = cm->get_podcast_source(uid); + ASSERT_EQ(item, v[0].get()); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_podcast_source) { - auto& v = cm->get_podcast_sources(); - auto size_before=v.size(); - auto uid = v[0]->get_id(); - cm->delete_podcast_source(uid); - ASSERT_EQ(cm->get_podcast_sources().size(), size_before-1); - EXPECT_THROW( - cm->get_podcast_source(uid), std::out_of_range); + auto& v = cm->get_podcast_sources(); + auto size_before = v.size(); + auto uid = v[0]->get_id(); + cm->delete_podcast_source(uid); + ASSERT_EQ(cm->get_podcast_sources().size(), size_before - 1); + EXPECT_THROW(cm->get_podcast_source(uid), std::out_of_range); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_podcast_throws) { - EXPECT_THROW( - cm->delete_podcast_source(QUuid::createUuid()), std::out_of_range); + EXPECT_THROW( + cm->delete_podcast_source(QUuid::createUuid()), std::out_of_range); } /*****************************************************************************/ @@ -251,40 +254,38 @@ TEST_F(SettingsFixture, add_radio_station) { QSignalSpy spy(cm.get(), SIGNAL(stations_changed())); ASSERT_TRUE(spy.isValid()); auto radio = std::make_shared(); - auto size_before = cm->get_stream_sources().size(); + auto size_before = cm->get_stations().size(); cm->add_radio_station(radio); ASSERT_EQ(spy.count(), 1); - ASSERT_EQ(cm->get_stream_sources().size(), size_before+1); + ASSERT_EQ(cm->get_stations().size(), size_before + 1); } /*****************************************************************************/ TEST_F(SettingsFixture, get_radio_station_throws) { - EXPECT_THROW( - cm->get_stream_source(QUuid::createUuid()), std::out_of_range); + EXPECT_THROW(cm->get_station(QUuid::createUuid()), std::out_of_range); } /*****************************************************************************/ TEST_F(SettingsFixture, get_radio_station_ok) { - auto& v = cm->get_stream_sources(); - auto uid = v[0]->get_id(); - auto item = cm->get_stream_source(uid); - ASSERT_EQ(item,v[0].get()); + auto& v = cm->get_stations(); + auto uid = v[0]->get_id(); + auto item = cm->get_station(uid); + ASSERT_EQ(item, v[0].get()); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_radio_station) { - auto& v = cm->get_stream_sources(); - auto size_before=v.size(); - auto uid = v[0]->get_id(); - cm->delete_radio_station(uid); - ASSERT_EQ(cm->get_stream_sources().size(), size_before-1); - EXPECT_THROW( - cm->get_stream_source(uid), std::out_of_range); + auto& v = cm->get_stations(); + auto size_before = v.size(); + auto uid = v[0]->get_id(); + cm->delete_radio_station(uid); + ASSERT_EQ(cm->get_stations().size(), size_before - 1); + EXPECT_THROW(cm->get_station(uid), std::out_of_range); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_radio_throws) { - EXPECT_THROW( - cm->delete_radio_station(QUuid::createUuid()), std::out_of_range); + EXPECT_THROW( + cm->delete_radio_station(QUuid::createUuid()), std::out_of_range); } @@ -296,38 +297,35 @@ TEST_F(SettingsFixture, add_alarm) { auto size_before = cm->get_alarms().size(); cm->add_alarm(alm); ASSERT_EQ(spy.count(), 1); - ASSERT_EQ(cm->get_alarms().size(), size_before+1); + ASSERT_EQ(cm->get_alarms().size(), size_before + 1); } /*****************************************************************************/ TEST_F(SettingsFixture, get_alarm_throws) { - EXPECT_THROW( - cm->get_alarm(QUuid::createUuid()), std::out_of_range); + EXPECT_THROW(cm->get_alarm(QUuid::createUuid()), std::out_of_range); } /*****************************************************************************/ TEST_F(SettingsFixture, get_alarm_ok) { - auto& v = cm->get_alarms(); - auto uid = v[0]->get_id(); - auto item = cm->get_alarm(uid); - ASSERT_EQ(item,v[0].get()); + auto& v = cm->get_alarms(); + auto uid = v[0]->get_id(); + auto item = cm->get_alarm(uid); + ASSERT_EQ(item, v[0].get()); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_alarm) { - auto& v = cm->get_alarms(); - auto size_before=v.size(); - auto uid = v[0]->get_id(); - cm->delete_alarm(uid); - ASSERT_EQ(cm->get_alarms().size(), size_before-1); - EXPECT_THROW( - cm->get_alarm(uid), std::out_of_range); + auto& v = cm->get_alarms(); + auto size_before = v.size(); + auto uid = v[0]->get_id(); + cm->delete_alarm(uid); + ASSERT_EQ(cm->get_alarms().size(), size_before - 1); + EXPECT_THROW(cm->get_alarm(uid), std::out_of_range); } /*****************************************************************************/ TEST_F(SettingsFixture, delete_alarm_throws) { - EXPECT_THROW( - cm->delete_alarm(QUuid::createUuid()), std::out_of_range); + EXPECT_THROW(cm->delete_alarm(QUuid::createUuid()), std::out_of_range); } /*****************************************************************************/ @@ -405,7 +403,7 @@ TEST_F(SettingsFixture, podcastid) { /*****************************************************************************/ TEST_F(SettingsFixture, streamsourceid) { - auto& v = cm->get_stream_sources(); + auto& v = cm->get_stations(); auto res = std::find_if( v.begin(), v.end(), [&](const std::shared_ptr& item) { return item->get_id() == @@ -511,13 +509,13 @@ TEST(ConfigManager, DefaultForNotWritableConfig) { /*****************************************************************************/ TEST_F(SettingsFixture, GetweatherConfigApiToken) { auto cfg = cm->get_weather_config(); - ASSERT_EQ(cfg->get_api_token(), QString("d77bd1ca2fd77ce4e1cdcdd5f8b7206c")); + ASSERT_EQ(cfg.get_api_token(), QString("d77bd1ca2fd77ce4e1cdcdd5f8b7206c")); } /*****************************************************************************/ TEST_F(SettingsFixture, GetweatherConfigCityId) { auto cfg = cm->get_weather_config(); - ASSERT_EQ(cfg->get_location_id(), QString("3452925")); + ASSERT_EQ(cfg.get_location_id(), QString("3452925")); } /*****************************************************************************/ diff --git a/test/test_sleeptimer.cpp b/test/test_sleeptimer.cpp index f647649..a7010c8 100644 --- a/test/test_sleeptimer.cpp +++ b/test/test_sleeptimer.cpp @@ -36,14 +36,19 @@ using namespace std::chrono_literals; using ::testing::AtLeast; /*****************************************************************************/ -TEST(SleepTimer, stopsRunningAlarm) { - auto cm = std::make_shared(); +class SleepTimerFixture : public ::testing::Test { +protected: + CmMock cm; +}; + +/*****************************************************************************/ +TEST_F(SleepTimerFixture, stopsRunningAlarm) { auto alm = std::make_shared( QUrl("http://st01.dlf.de/dlf/01/104/ogg/stream.ogg"), QTime::currentTime().addSecs(3), Alarm::Daily); /* sleep timeout 500 ms */ - EXPECT_CALL(*(cm.get()), get_sleep_timeout()) + EXPECT_CALL(cm, get_sleep_timeout()) .Times(AtLeast(1)) .WillRepeatedly(Return(duration_cast(500ms))); @@ -59,10 +64,9 @@ TEST(SleepTimer, stopsRunningAlarm) { } /*****************************************************************************/ -TEST(SleepTimer, stopsPlayer) { - auto cm = std::make_shared(); +TEST_F(SleepTimerFixture,stopsPlayer) { /* sleep timeout 500 ms */ - EXPECT_CALL(*(cm.get()), get_sleep_timeout()) + EXPECT_CALL(cm, get_sleep_timeout()) .Times(AtLeast(1)) .WillRepeatedly(Return(duration_cast(500ms))); @@ -77,10 +81,9 @@ TEST(SleepTimer, stopsPlayer) { } /*****************************************************************************/ -TEST(SleepTimer, PlayerStoppedStillEmitsSignal) { - auto cm = std::make_shared(); +TEST_F(SleepTimerFixture, PlayerStoppedStillEmitsSignal) { /* sleep timeout 500 ms */ - EXPECT_CALL(*(cm.get()), get_sleep_timeout()) + EXPECT_CALL(cm, get_sleep_timeout()) .Times(AtLeast(1)) .WillRepeatedly(Return(duration_cast(500ms))); @@ -95,11 +98,9 @@ TEST(SleepTimer, PlayerStoppedStillEmitsSignal) { } /*****************************************************************************/ - -TEST(SleepTimer, TimeoutChangeEmitsSignal) { - auto cm = std::make_shared(); +TEST_F(SleepTimerFixture, TimeoutChangeEmitsSignal) { /* sleep timeout 500 ms */ - EXPECT_CALL(*(cm.get()), get_sleep_timeout()) + EXPECT_CALL(cm, get_sleep_timeout()) .Times(AtLeast(1)) .WillRepeatedly(Return(duration_cast(500ms))); @@ -107,13 +108,13 @@ TEST(SleepTimer, TimeoutChangeEmitsSignal) { QSignalSpy spy(&dut, SIGNAL(sleep_timeout_changed(int))); ASSERT_TRUE(spy.isValid()); // dispatch alarm - dut.set_sleep_timeout(5); // use int signature + dut.set_sleep_timeout(5); // use int signature ASSERT_EQ(spy.count(), 1); // expect 1 timeout change auto args = spy.first(); bool valid = true; auto to = args.at(0).toInt(&valid); ASSERT_TRUE(valid); - ASSERT_EQ(to,5); + ASSERT_EQ(to, 5); } /*****************************************************************************/ diff --git a/test/test_weather.cpp b/test/test_weather.cpp index fb601be..d1752ab 100644 --- a/test/test_weather.cpp +++ b/test/test_weather.cpp @@ -29,7 +29,7 @@ using ::testing::AtLeast; class WeatherFile : public virtual ::testing::Test { public: WeatherFile() - : weatherFile(TEST_FILE_PATH + "/sample_weather.json") { + : weatherFile(TEST_FILE_PATH + "/sample_weather.json"){ if (!weatherFile.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << weatherFile.errorString(); throw std::system_error( @@ -40,16 +40,16 @@ class WeatherFile : public virtual ::testing::Test { protected: QFile weatherFile; + CmMock cm; }; /*****************************************************************************/ -TEST(Weather, RefreshEmitsSignal) { - auto cm = std::make_shared(); +TEST_F(WeatherFile, RefreshEmitsSignal) { auto weather_cfg = DigitalRooster::WeatherConfig( QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(AtLeast(1)) - .WillRepeatedly(Return(&weather_cfg)); + .WillRepeatedly(ReturnRef(weather_cfg)); Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); @@ -61,13 +61,12 @@ TEST(Weather, RefreshEmitsSignal) { } /*****************************************************************************/ -TEST(Weather, GetConfigForDownloadAfterTimerExpired) { - auto cm = std::make_shared(); +TEST_F(WeatherFile, GetConfigForDownloadAfterTimerExpired) { auto weather_cfg = DigitalRooster::WeatherConfig( QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(AtLeast(1)) - .WillRepeatedly(Return(&weather_cfg)); + .WillRepeatedly(ReturnRef(weather_cfg)); Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); @@ -80,15 +79,13 @@ TEST(Weather, GetConfigForDownloadAfterTimerExpired) { /*****************************************************************************/ TEST_F(WeatherFile, ParseTemperatureFromFile) { - auto cm = std::make_shared(); auto weather_cfg = DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(1) - .WillRepeatedly(Return(&weather_cfg)); - Weather dut(cm); + .WillRepeatedly(ReturnRef(weather_cfg)); + Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); dut.parse_response(weatherFile.readAll()); spy.wait(10); @@ -97,16 +94,13 @@ TEST_F(WeatherFile, ParseTemperatureFromFile) { } /*****************************************************************************/ TEST_F(WeatherFile, GetCityFromFile) { - auto cm = std::make_shared(); auto weather_cfg = DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(1) - .WillRepeatedly(Return(&weather_cfg)); + .WillRepeatedly(ReturnRef(weather_cfg)); Weather dut(cm); - QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); dut.parse_response(weatherFile.readAll()); spy.wait(10); @@ -116,15 +110,13 @@ TEST_F(WeatherFile, GetCityFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, ParseConditionFromFile) { - auto cm = std::make_shared(); auto weather_cfg = DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(1) - .WillRepeatedly(Return(&weather_cfg)); - Weather dut(cm); + .WillRepeatedly(ReturnRef(weather_cfg)); + Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(condition_changed(const QString&))); dut.parse_response(weatherFile.readAll()); spy.wait(10); @@ -134,15 +126,13 @@ TEST_F(WeatherFile, ParseConditionFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, IconURI) { - auto cm = std::make_shared(); auto weather_cfg = DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - - EXPECT_CALL(*(cm.get()), get_weather_cfg()) + EXPECT_CALL(cm, get_weather_config()) .Times(1) - .WillRepeatedly(Return(&weather_cfg)); - Weather dut(cm); + .WillRepeatedly(ReturnRef(weather_cfg)); + Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); dut.parse_response(weatherFile.readAll()); spy.wait(10); @@ -150,6 +140,7 @@ TEST_F(WeatherFile, IconURI) { ASSERT_EQ(dut.get_weather_icon_url(), QUrl("http://openweathermap.org/img/w/02d.png")); } + /*****************************************************************************/ TEST(WeatherCfg, fromJsonGood) { auto json_string = QString(R"( @@ -160,12 +151,12 @@ TEST(WeatherCfg, fromJsonGood) { } )"); auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); - auto dut = WeatherConfig::from_json_object(jdoc.object()); - ASSERT_EQ(dut->get_api_token(), QString("Secret")); - ASSERT_EQ(dut->get_location_id(), QString("ABCD")); - ASSERT_EQ(dut->get_update_interval(), std::chrono::seconds(123)); + ASSERT_EQ(dut.get_api_token(), QString("Secret")); + ASSERT_EQ(dut.get_location_id(), QString("ABCD")); + ASSERT_EQ(dut.get_update_interval(), std::chrono::seconds(123)); } + /*****************************************************************************/ TEST(WeatherCfg, throwEmptyLocation) { auto json_string = QString(R"( @@ -180,6 +171,7 @@ TEST(WeatherCfg, throwEmptyLocation) { ASSERT_THROW( WeatherConfig::from_json_object(jdoc.object()), std::invalid_argument); } + /*****************************************************************************/ TEST(WeatherCfg, throwNoApiToken) { auto json_string = QString(R"( From 0704350c80234805e7efb24f6d74fe0f54617456 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Fri, 6 Mar 2020 19:40:55 +0100 Subject: [PATCH 16/26] Auto close drawer Start a timer to close drawer Signed-off-by: Thomas Ruschival --- qtgui/qml/main.qml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/qtgui/qml/main.qml b/qtgui/qml/main.qml index 817f999..0bb2588 100644 --- a/qtgui/qml/main.qml +++ b/qtgui/qml/main.qml @@ -14,10 +14,10 @@ ApplicationWindow { visible: true width: Style.canvasWidth; height: Style.canvasHeight; - + Material.theme: Material.Dark Material.accent: Material.Red - + property alias playerControlWidget: playerControlWidget property string functionMode: "Clock" @@ -62,7 +62,7 @@ ApplicationWindow { elide: Label.ElideRight Layout.fillWidth: true } - + Label{ id: countdown_to_sleep; text: "\uf51a @@ -118,9 +118,22 @@ ApplicationWindow { width: Style.drawer.w; height: applicationWindow.height margins: Style.itemMargins.slim; + closePolicy : Popup.CloseOnPressOutside; edge: Qt.LeftEdge; interactive: true; - + + onOpened :{ + autocloseTimer.start() + } + + Timer { + id: autocloseTimer + interval: 4000 + running: true + repeat: false + onTriggered: drawer.close(); + } + ListView { id: listView anchors.fill: parent @@ -226,7 +239,7 @@ ApplicationWindow { stackView.pop(null); } } - } + } /**** global connections ****/ Component.onCompleted: { From e8f67b3b105374b1c168271b9a45eeb576ca565b Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sat, 21 Mar 2020 18:03:58 +0100 Subject: [PATCH 17/26] Feature/forecast (#20) Clock page shows weather forecast for 6h and 12h instead of location - I assume you know where you are. Signed-off-by: Thomas Ruschival --- config/digitalrooster.json | 154 +++++++++++++++++++--------- include/alarm.hpp | 1 - include/appconstants.hpp | 17 ++++ include/weather.hpp | 143 +++++++++++++++++++++++--- libsrc/alarm.cpp | 6 -- libsrc/playableitem.cpp | 2 +- libsrc/weather.cpp | 148 ++++++++++++++++++++++++--- qtgui/main.cpp | 11 +- qtgui/qml/ClockPage.qml | 155 +++++++++++++++++++++++------ qtgui/qml/main.qml | 1 + test/CMakeLists.txt | 52 +++++----- test/sample_forecast.json | 199 +++++++++++++++++++++++++++++++++++++ test/test_weather.cpp | 134 +++++++++++++++++-------- 13 files changed, 841 insertions(+), 182 deletions(-) create mode 100644 test/sample_forecast.json diff --git a/config/digitalrooster.json b/config/digitalrooster.json index 3eaa091..1e16f97 100644 --- a/config/digitalrooster.json +++ b/config/digitalrooster.json @@ -1,70 +1,126 @@ { - "version": "0.5.2", - "AlarmTimeout": 15, - "SleepTimeout": 45, - + "Alarms": [ + { + "enabled": true, + "id": "{8364287c-3036-4cd0-b243-0a4f5dd863ae}", + "period": "workdays", + "time": "06:30", + "url": "http://st01.dlf.de/dlf/01/104/ogg/stream.ogg", + "volume": 25 + }, + { + "enabled": false, + "id": "{30df096e-f776-404c-8619-3c4c2c4da212}", + "period": "once", + "time": "07:00", + "url": "http://st01.dlf.de/dlf/01/104/ogg/stream.ogg", + "volume": 40 + } + ], "InternetRadio": [ { - "name": "SWR2", - "uri": "http://swr-swr2-live.cast.addradio.de/swr/swr2/live/mp3/256/stream.mp3" + "id": "{0a2152ef-da4f-4f26-8a77-078193da536e}", + "name": "BBC Radio 4", + "url": "http://bbcwssc.ic.llnwd.net/stream/bbcwssc_mp1_ws-eieuk" + }, + { + "id": "{09be8e85-a9d3-4db8-b2c5-02e3eb3ff66d}", + "name": "Deutschlandfunk", + "url": "http://st01.dlf.de/dlf/01/104/ogg/stream.ogg" }, { + "id": "{51ec1a1b-bd15-4123-bab9-aa2ffaf9ff5c}", "name": "Deutschlandfunk Nova", - "uri": "http://st03.dlf.de/dlf/03/104/ogg/stream.ogg" + "url": "http://st03.dlf.de/dlf/03/104/ogg/stream.ogg" }, { - "name": "Deutschlandfunk", - "uri": "http://st01.dlf.de/dlf/01/104/ogg/stream.ogg" + "id": "{de2c79da-c250-4c78-a2db-5db398c0cbd9}", + "name": "Radio FM4", + "url": "https://fm4shoutcast.sf.apa.at" + }, + { + "id": "{d742d526-e26f-4f56-89ad-57a188bc747c}", + "name": "SWR Aktuell", + "url": "http://swr-swraktuell-live.cast.addradio.de/swr/swraktuell/live/mp3/128/stream.mp3" + }, + { + "id": "{344c156c-5294-4742-94bd-7ece7f165043}", + "name": "SWR2", + "url": "http://swr-swr2-live.cast.addradio.de/swr/swr2/live/mp3/256/stream.mp3" } ], "Podcasts": [ { - "name": "DRadio Essay&Diskurs", - "uri": "http://www.deutschlandfunk.de/podcast-essay-und-diskurs.1185.de.podcast.xml", - "UpdateInterval": 7200 + "id": "{c3bf652a-d270-4e8c-9b57-6e6391625956}", + "title": "Ab 21 - Deutschlandfunk Nova", + "updateInterval": 3600, + "url": "https://www.deutschlandfunknova.de/podcast/ab-21" + }, + { + "id": "{61f6bcda-0334-49a7-bfe9-b206b9f0bef7}", + "title": "Alternativlos", + "updateInterval": 3600, + "url": "https://alternativlos.org/alternativlos.rss" + }, + { + "id": "{6d2ebae6-d961-411d-aecc-7820d1be1650}", + "title": "Arms Control Wonk", + "updateInterval": 3600, + "url": "http://armscontrolwonk.libsyn.com/rss" + }, + { + "id": "{b36899ea-5ca3-4929-afbe-005198f13e88}", + "title": "Deutschlandfunk - Der Tag - Deutschlandfunk", + "updateInterval": 3600, + "url": "https://www.deutschlandfunk.de/podcast-deutschlandfunk-der-tag.3417.de.podcast.xml" + }, + { + "id": "{8bbc9a39-81df-4e99-9a19-a56cfb7d0b19}", + "title": "Eine Stunde Liebe - Deutschlandfunk Nova", + "updateInterval": 3600, + "url": "https://www.deutschlandfunknova.de/podcast/eine-stunde-liebe" + }, + { + "id": "{285cf124-c4b9-4813-b6bb-e17517e4dd48}", + "title": "Eine Stunde Was mit Medien - Deutschlandfunk Nova", + "updateInterval": 3600, + "url": "https://www.deutschlandfunknova.de/podcast/eine-stunde-medien" + }, + { + "id": "{0a86a6ab-a174-4ccd-b8a5-4f375b5dbac8}", + "title": "Hielscher oder Haase - Deutschlandfunk Nova", + "updateInterval": 3600, + "url": "https://www.deutschlandfunknova.de/podcast/hielscher-oder-haase" }, { - "name": "BBC Everyday Ethics", - "uri": "http://podcasts.files.bbci.co.uk/p02nrsmh.rss", - "UpdateInterval": 7200 + "id": "{cdb47dd0-fea1-47d5-896a-d766b8b4a111}", + "title": "More or Less: Behind the Stats", + "updateInterval": 3600, + "url": "https://podcasts.files.bbci.co.uk/p02nrss1.rss" }, { - "name": "Alternativlos", - "uri": "https://alternativlos.org/alternativlos.rss", - "UpdateInterval": 7200 + "id": "{532f29b8-8f8e-4790-96aa-53d8755ac091}", + "title": "My Dad Wrote A Porno", + "updateInterval": 3600, + "url": "https://rss.acast.com/mydadwroteaporno" }, { - "name": "Arms Control Wonk", - "uri": "http://armscontrolwonk.libsyn.com/rss", - "UpdateInterval": 7200 + "id": "{e5383b65-7e82-49e4-bda0-02e41e29af6b}", + "title": "Witness History", + "updateInterval": 3600, + "url": "https://podcasts.files.bbci.co.uk/p004t1hd.rss" } ], - "Alarms":[ - { - "time" : "7:00", - "enabled" : true, - "period" : "workdays", - "AlarmTimeout": 10, - "uri" : "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3" - }, - { - "time" : "9:00", - "enabled" : true, - "period" : "weekend", - "uri" : "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3" - }, - { - "time" : "10:00", - "enabled" : true, - "period" : "daily", - "uri" : "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3" - }, - { - "time" : "6:00", - "enabled" : false, - "period" : "once", - "uri" : "http://st01.dlf.de/dlf/01/128/mp3/stream.mp3" - } - ] + "Weather": { + "API-Key": "a904431b4e0eae431bcc1e075c761abb", + "locationID": "2928751", + "updateInterval": 3600 + }, + "alarmTimeout": 30, + "brightnessActive": 19, + "brightnessStandby": 4, + "sleepTimeout": 40, + "version": "0.8.0", + "volume": 25, + "wpa_ctrl": "/var/run/wpa_supplicant/wlan0" } - diff --git a/include/alarm.hpp b/include/alarm.hpp index b4b9333..91031f0 100644 --- a/include/alarm.hpp +++ b/include/alarm.hpp @@ -150,7 +150,6 @@ class Alarm : public QObject { std::shared_ptr get_media() const { return media; } - void set_media(std::shared_ptr new_media); /** * is this alarm set diff --git a/include/appconstants.hpp b/include/appconstants.hpp index 14e03e1..2f17cef 100644 --- a/include/appconstants.hpp +++ b/include/appconstants.hpp @@ -240,6 +240,23 @@ const int DEFAULT_BRIGHTNESS = 25; */ const int DEFAULT_ICON_WIDTH = 88; +/** + * Where to find icons for weather condition + */ +const QString WEATHER_ICON_BASE_URL("https://openweathermap.org/img/w/"); + +/** + * API Base URL for weather + */ +const QString WEATHER_API_BASE_URL( + "https://api.openweathermap.org/data/2.5/weather?"); + +/** + * API Base URL for Forecasts + */ +const QString WEATHER_FORECASTS_API_BASE_URL( + "https://api.openweathermap.org/data/2.5/forecast?"); + /***************************************************************************** CMake build configurations from config.h *****************************************************************************/ diff --git a/include/weather.hpp b/include/weather.hpp index 16389a3..613ceaa 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -22,12 +22,63 @@ #include #include +#include + #include "IWeatherConfigStore.hpp" namespace DigitalRooster { class ConfigurationManager; +/** + * Object for Forecast data + * Still a QObject, would be nice if I figure out how to pass a list + * of Forecasts to QML (and keep the ressources managed and thread-safe) + */ +class Forecast : public QObject { + Q_OBJECT + Q_PROPERTY(QDateTime timestamp READ get_timestamp) + Q_PROPERTY(double temperature READ get_temperature) + Q_PROPERTY(QUrl weatherIcon READ get_weather_icon_url) +public: + Forecast() = default; + /** + * Create a new forecast from JSON object + * @param json + * @param parent + */ + explicit Forecast(const QJsonObject& json); + + QDateTime get_timestamp() const { + return timestamp; + }; + + QUrl get_weather_icon_url() const { + return icon_url; + }; + + double get_temperature() const { + return temperature; + }; + +private: + /** + * expected temperature at timepoint + */ + double temperature = 0.0; + + /** + * Weather Icon URL + */ + QUrl icon_url; + + /** + * Timepoint for this forecast + */ + QDateTime timestamp; +}; + + /** * Periodically downloads weather info from Openweathermaps */ @@ -38,15 +89,14 @@ class Weather : public QObject { Q_PROPERTY( float temperature READ get_temperature NOTIFY temperature_changed) Q_PROPERTY(QUrl weatherIcon READ get_weather_icon_url NOTIFY icon_changed) - public: /** * Constructor for Weather provider * @param store access to current weather configuration * @param parent */ - explicit Weather(const IWeatherConfigStore& store, - QObject* parent = nullptr); + explicit Weather( + const IWeatherConfigStore& store, QObject* parent = nullptr); /** * Update Download interval * @param iv interval in seconds @@ -79,21 +129,67 @@ class Weather : public QObject { * Construct a URL from icon_id */ QUrl get_weather_icon_url() const { - const QString base_uri("http://openweathermap.org/img/w/"); - return QUrl(base_uri + icon_id + ".png"); + return icon_url; } + /** + * Ugly Interface to pass the information to QML + * I would like to keep a list of Forecasts in C++ and pass it to QML + * however I only can pass a list of raw pointers to QML (QObject is not + * copyable) and I would like to avoid raw pointers in the list so I can + * manage resources. + * The initial idea was a method like: + * Q_INVOKABLE QList get_forecasts() const; + * A QList (raw pointers) would work for this case, however there + * is no way to assert that QML does not hold one of these Forecast pointers + * while the forecast is updated, the Object may be deleted and we have a + * dangling pointer. + * TODO: I have not figured out how to return a + * QList> + */ + /** + * Get a copy of Forecast list + * @return + */ + const QList> get_forecasts() const; + + /** + * reach into list of forecasts and get temperature + * @param fc_idx index in list + * @return tempertature + */ + Q_INVOKABLE double get_forecast_temperature(int fc_idx) const; + /** + * reach into list of forecasts and get timestamp + * @param fc_idx index in list + * @return tempertature + */ + Q_INVOKABLE QDateTime get_forecast_timestamp(int fc_idx) const; + /** + * reach into list of forecasts and get icon url + * @param fc_idx index in list + * @return tempertature + */ + Q_INVOKABLE QUrl get_forecast_icon_url(int fc_idx) const; + public slots: /** * Slot triggers refresh of weather data and starts a new download process */ void refresh(); + /** - * Read Json from content bytearray and update internal fields + * Read Current weather status as JSON and update member fields * @param content JSON as bytearray */ - void parse_response(QByteArray content); + void parse_weather(const QByteArray& content); + + /** + * Read Current weather status as JSON and update member fields + * @param content weather forecast JSON as bytearray + */ + void parse_forecast(const QByteArray& content); signals: /** @@ -121,6 +217,11 @@ public slots: */ void icon_changed(const QUrl& img_uri); + /** + * Forecast available, emitted after successfully parsing new forecasts + */ + void forecast_available(); + private: /** * Central configuration and data handler @@ -144,14 +245,14 @@ public slots: QString city_name; /** - * Localized weathercondition string + * Localized weather condition string */ QString condition; /** - * Icon matching the current weather condtion returned by weather-api + * Icon matching the current weather condition returned by weather-api */ - QString icon_id; + QUrl icon_url; /** * QTimer for periodic updates @@ -161,8 +262,17 @@ public slots: /** * HTTP handle to download JSONs */ - HttpClient downloader; + HttpClient weather_downloader; + HttpClient forecast_downloader; + /** + * Mutex to lock list of forecasts + */ + mutable std::mutex forecast_mtx; + /** + * list of forecasts + */ + QList> forecasts; void parse_city(const QJsonObject& o); void parse_temperature(const QJsonObject& o); @@ -171,11 +281,18 @@ public slots: /** * create a valid URL to download weather-json from openweathermaps - * based on infromation in WeatherConfig + * based on information in WeatherConfig + * @param cfg configuration with location, units etc. + * @return uri e.g. api.openweathermap.org/data/2.5/weather?zip=94040,us + */ +QUrl create_weather_url(const WeatherConfig& cfg); +/** + * create a valid URL to download weather-json from openweathermaps + * based on information in WeatherConfig * @param cfg configuration with location, units etc. * @return uri e.g. api.openweathermap.org/data/2.5/weather?zip=94040,us */ -QUrl create_weather_uri(const WeatherConfig& cfg); +QUrl create_forecast_url(const WeatherConfig& cfg); } // namespace DigitalRooster diff --git a/libsrc/alarm.cpp b/libsrc/alarm.cpp index de26bb0..373634b 100644 --- a/libsrc/alarm.cpp +++ b/libsrc/alarm.cpp @@ -106,12 +106,6 @@ void Alarm::update_media_url(const QUrl& url) { emit dataChanged(); } -/*****************************************************************************/ -void Alarm::set_media(std::shared_ptr new_media) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - media = new_media; -} - /*****************************************************************************/ Alarm::Period DigitalRooster::json_string_to_alarm_period( const QString& literal) { diff --git a/libsrc/playableitem.cpp b/libsrc/playableitem.cpp index 2d15878..e2d3c55 100644 --- a/libsrc/playableitem.cpp +++ b/libsrc/playableitem.cpp @@ -155,7 +155,7 @@ void PodcastEpisode::set_description(const QString& desc) { /***********************************************************************/ QString PodcastEpisode::get_guid() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; +// qCDebug(CLASS_LC) << Q_FUNC_INFO; if (guid.isEmpty()) { return get_url().toString(); } diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index 347abf6..995cd99 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -30,16 +30,21 @@ static Q_LOGGING_CATEGORY(CLASS_LC, "DigitalRooster.Weather"); Weather::Weather(const IWeatherConfigStore& store, QObject* parent) : cm(store) { qCDebug(CLASS_LC) << Q_FUNC_INFO; + // timer starts refresh, refresh calls downloader - connect(&timer, SIGNAL(timeout()), this, SLOT(refresh())); + connect(&timer, &QTimer::timeout, this, &Weather::refresh); // downloader finished -> parse result - connect(&downloader, SIGNAL(dataAvailable(QByteArray)), this, - SLOT(parse_response(QByteArray))); + connect(&weather_downloader, &HttpClient::dataAvailable, this, + &Weather::parse_weather); + connect(&forecast_downloader, &HttpClient::dataAvailable, this, + &Weather::parse_forecast); timer.setInterval(duration_cast(update_interval)); timer.setSingleShot(false); timer.start(); - downloader.doDownload(create_weather_uri(cm.get_weather_config())); + weather_downloader.doDownload(create_weather_url(cm.get_weather_config())); + forecast_downloader.doDownload( + create_forecast_url(cm.get_weather_config())); } /*****************************************************************************/ @@ -59,16 +64,19 @@ std::chrono::seconds Weather::get_update_interval() const { /*****************************************************************************/ void Weather::refresh() { qCDebug(CLASS_LC) << Q_FUNC_INFO; - downloader.doDownload(create_weather_uri(cm.get_weather_config())); + /* restart downloads */ + weather_downloader.doDownload(create_weather_url(cm.get_weather_config())); + forecast_downloader.doDownload( + create_forecast_url(cm.get_weather_config())); } /*****************************************************************************/ -void Weather::parse_response(QByteArray content) { +void Weather::parse_weather(const QByteArray& content) { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonParseError perr; QJsonDocument doc = QJsonDocument::fromJson(content, &perr); if (perr.error != QJsonParseError::NoError) { - qCWarning(CLASS_LC) << "Parsing Failed"; + qCWarning(CLASS_LC) << "Parsing weather failed"; return; } QJsonObject o = doc.object(); @@ -77,26 +85,75 @@ void Weather::parse_response(QByteArray content) { parse_condition(o); // also extracts icon emit weather_info_updated(); } + /*****************************************************************************/ +void Weather::parse_forecast(const QByteArray& content) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QJsonParseError perr; + QJsonDocument doc = QJsonDocument::fromJson(content, &perr); + if (perr.error != QJsonParseError::NoError) { + qCWarning(CLASS_LC) << "Parsing forecast failed"; + return; + } + auto fc_array = doc["list"].toArray(); + { // scope for lock + const std::lock_guard lock(forecast_mtx); + forecasts.clear(); + for (const auto fc_val : fc_array) { + forecasts.append( + std::make_shared(fc_val.toObject())); + } + } // emit outside of lock, code is executed in the same thread + emit forecast_available(); +} + +/*****************************************************************************/ +QUrl DigitalRooster::create_weather_url(const WeatherConfig& cfg) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + QString request_str(WEATHER_API_BASE_URL); + request_str.reserve(512); + request_str += "id="; + request_str += cfg.get_location_id(); + request_str += "&units=metric"; + request_str += "&appid="; + request_str += cfg.get_api_token(); + request_str += "&lang=en"; // default english + qCDebug(CLASS_LC) << request_str; + return QUrl(request_str); +} -QUrl DigitalRooster::create_weather_uri(const WeatherConfig& cfg) { +/*****************************************************************************/ +QUrl DigitalRooster::create_forecast_url(const WeatherConfig& cfg) { qCDebug(CLASS_LC) << Q_FUNC_INFO; - QString request_str({"http://api.openweathermap.org/data/2.5/weather?"}); + QString request_str(WEATHER_FORECASTS_API_BASE_URL); request_str.reserve(512); request_str += "id="; request_str += cfg.get_location_id(); request_str += "&units=metric"; request_str += "&appid="; request_str += cfg.get_api_token(); - request_str += "&lang=en"; //default english + request_str += "&lang=en"; // default english + request_str += "&cnt=5"; // ~now, +3h, +6h, +9h, +12h + qCDebug(CLASS_LC) << request_str; return QUrl(request_str); } + +/*****************************************************************************/ +const QList> Weather::get_forecasts() const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + const std::lock_guard lock(forecast_mtx); + auto ret = forecasts; // copy + return ret; +} + /*****************************************************************************/ void Weather::parse_city(const QJsonObject& o) { qCDebug(CLASS_LC) << Q_FUNC_INFO; city_name = o["name"].toString(); + qCInfo(CLASS_LC) << "Weather location:" << city_name; emit city_updated(city_name); } + /*****************************************************************************/ void Weather::parse_temperature(const QJsonObject& o) { qCDebug(CLASS_LC) << Q_FUNC_INFO; @@ -114,13 +171,50 @@ void Weather::parse_condition(const QJsonObject& o) { auto weather_arr = o["weather"].toArray(); auto weather = weather_arr.at(0); if (weather.isUndefined()) { - qCWarning(CLASS_LC) << " couldn't read weather JSON object"; + qCWarning(CLASS_LC) << "couldn't read weather JSON object"; return; } condition = weather["description"].toString(); - icon_id = weather["icon"].toString(); + icon_url = + QUrl(WEATHER_ICON_BASE_URL + weather["icon"].toString() + ".png"); emit condition_changed(condition); - emit icon_changed(icon_id); + emit icon_changed(icon_url); +} + +/*****************************************************************************/ +double Weather::get_forecast_temperature(int fc_idx) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + const std::lock_guard lock(forecast_mtx); + if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { + qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; + return std::nan(""); + } + return forecasts[fc_idx]->get_temperature(); +} + +/*****************************************************************************/ +QDateTime Weather::get_forecast_timestamp(int fc_idx) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + const std::lock_guard lock(forecast_mtx); + if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { + qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; + return QDateTime(); + } + return forecasts[fc_idx]->get_timestamp(); +} + +/*****************************************************************************/ +QUrl Weather::get_forecast_icon_url(int fc_idx) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + const std::lock_guard lock(forecast_mtx); + if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { + qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; + return QUrl(); + } + return forecasts[fc_idx]->get_weather_icon_url(); } /*****************************************************************************/ @@ -129,10 +223,11 @@ WeatherConfig::WeatherConfig(const QString& token, const QString& location, : api_token(token) , location_id(location) , update_interval(interval) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; } /*****************************************************************************/ -QJsonObject WeatherConfig::to_json_object() const{ +QJsonObject WeatherConfig::to_json_object() const { qCDebug(CLASS_LC) << Q_FUNC_INFO; QJsonObject j; j[KEY_UPDATE_INTERVAL] = static_cast(update_interval.count()); @@ -156,4 +251,29 @@ WeatherConfig WeatherConfig::from_json_object(const QJsonObject& json) { return WeatherConfig(json[KEY_WEATHER_API_KEY].toString(), json[KEY_WEATHER_LOCATION_ID].toString(), interval); } + +/*****************************************************************************/ +Forecast::Forecast(const QJsonObject& json) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + + auto main = json["main"].toObject(); + // for some reasons the weather is an array with 1 element + auto weather_array = json["weather"].toArray(); + auto weather = weather_array.at(0).toObject(); + + timestamp = + QDateTime::fromSecsSinceEpoch(json["dt"].toInt(), QTimeZone::utc()); + + if (!main.isEmpty()) { + temperature = main["temp"].toDouble(); + } + if (!weather.isEmpty()) { + icon_url = + QUrl(WEATHER_ICON_BASE_URL + weather["icon"].toString() + ".png"); + } + + qCDebug(CLASS_LC) << "forecast for" << timestamp << ":" << temperature + << "°C icon:" << icon_url; +} + /*****************************************************************************/ diff --git a/qtgui/main.cpp b/qtgui/main.cpp index e45578d..b087517 100644 --- a/qtgui/main.cpp +++ b/qtgui/main.cpp @@ -73,14 +73,14 @@ const QString DigitalRooster::DEFAULT_LOG_PATH( */ const QString DigitalRooster::DEFAULT_CONFIG_FILE_PATH( QDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)) - .filePath(CONFIG_JSON_FILE_NAME)); + .filePath(CONFIG_JSON_FILE_NAME.toLower())); /** * Cache directory */ const QString DigitalRooster::DEFAULT_CACHE_DIR_PATH( QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .filePath(APPLICATION_NAME)); + .filePath(APPLICATION_NAME.toLower())); /** * Global wall clock @@ -231,7 +231,7 @@ int main(int argc, char* argv[]) { power.standby(); /* - * QML Setup Dynamically createable Types + * QML Setup dynamically createable types * All Elements/Lists are created in C++ */ qmlRegisterUncreatableType( @@ -241,13 +241,16 @@ int main(int argc, char* argv[]) { "ruschi.PodcastEpisode", 1, 0, "PodcastEpisode", "QML must not instantiate PodcastEpisode!"); qmlRegisterUncreatableType( - "ruschi.Alarm", 1, 0, "Alarm", "QML must not instatiate Alarm!"); + "ruschi.Alarm", 1, 0, "Alarm", "QML must not instantiate Alarm!"); qmlRegisterUncreatableType( "ruschi.PlayableItem", 1, 0, "PlayableItem", "QML must not instantiate PlayableItem!"); qmlRegisterUncreatableType( "ruschi.WifiListModel", 1, 0, "WifiListModel", "QML must not instantiate WifiListModel!"); + qmlRegisterUncreatableType( + "ruschi.Forecast", 1, 0, "Forecast", + "QML must not instantiate Forecast!"); QQmlApplicationEngine view; QQmlContext* ctxt = view.rootContext(); diff --git a/qtgui/qml/ClockPage.qml b/qtgui/qml/ClockPage.qml index 83e5b5a..4692299 100644 --- a/qtgui/qml/ClockPage.qml +++ b/qtgui/qml/ClockPage.qml @@ -6,55 +6,154 @@ Page { id: clockPage property string objectName : "ClockPage" - ColumnLayout{ + GridLayout{ + columns: 2; + rows: 3; anchors.fill: parent - anchors.leftMargin: Style.itemMargins.extrawide; - anchors.rightMargin:Style.itemMargins.extrawide; - spacing: Style.itemSpacings.sparse; - + anchors.leftMargin: Style.itemMargins.sparse; + anchors.rightMargin: Style.itemMargins.sparse; + anchors.bottomMargin: Style.itemMargins.sparse; Text{ text: currentTime.timestring_lz_hh_mm font: Style.font.clock; color: "white" - Layout.alignment: Qt.AlignHCenter + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter ; + anchors.bottomMargin: Style.itemMargins.extrawide; } + RowLayout{ + Layout.columnSpan: 2 + Layout.margins: Style.itemMargins.slim; + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Text{ + text: Math.round(weather.temperature) + "\u00B0C"; + font: Style.font.subtitle; + color: "white" + Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; + } + Image { + Layout.maximumWidth: 72 + Layout.maximumHeight: 72 + Layout.preferredWidth: 72 + Layout.preferredHeight: 72 + Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; + Layout.fillHeight: true; + fillMode: Image.PreserveAspectFit + source: weather.weatherIcon; + } + }// Rowlayout Temp+Icon current + /* Text{ */ - /* text: currentTime.datestring_lz */ - /* font: Style.font.subtitle; */ - /* color: "white" */ - /* Layout.alignment: Qt.AlignHCenter */ + /* text: weather.city; */ + /* font: Style.font.label; */ + /* color: "white" */ + /* Layout.columnSpan: 2 */ + /* Layout.margins: 0; */ + /* Layout.alignment: Qt.AlignHCenter */ /* } */ GridLayout{ - id: weatherLayout - columns: 2 - rows: 2 - rowSpacing: 0; - columnSpacing: Style.itemSpacings.dense; + columns: 2; + rows: 2; + rowSpacing:0; + columnSpacing: Style.itemMargins.slim; + + Layout.margins: Style.itemMargins.slim; + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Layout.alignment: Qt.AlignHCenter Text{ - text: weather.city + " " + Math.round(weather.temperature*10)/10 + "\u00B0C"; - font: Style.font.subtitle; + id: temperature_6h; + text: Math.round(weather.temperature) + "\u00B0C"; + font: Style.font.label; color: "white" - Layout.columnSpan: 1 - Layout.alignment: Qt.AlignHCenter + Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; } - Image { - id: cloudicon - Layout.maximumWidth: 72 - Layout.maximumHeight: 72 - Layout.preferredWidth: 64 - Layout.preferredHeight: 64 - Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter + id: condition_icon_6h; + Layout.leftMargin:-2; + Layout.maximumWidth: 64 + Layout.maximumHeight: 64 + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; Layout.fillHeight: true; + fillMode: Image.PreserveAspectFit + source: weather.weatherIcon; + } + Text{ + id: timestamp_6h; + text: "6:00"; + font: Style.font.label; + Layout.columnSpan: 2 + color: "white" + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; + Layout.topMargin: -Style.itemMargins.slim; + Layout.bottomMargin: Style.itemMargins.slim; + } + }// Gridlayout Temp+Icon +6h + + GridLayout{ + columns: 2; + rows: 2; + rowSpacing:0; + columnSpacing: Style.itemMargins.slim; + + Layout.margins: Style.itemMargins.slim; + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Text{ + id: temperature_12h; + text: Math.round(weather.temperature) + "\u00B0C"; + font: Style.font.label; + color: "white" + Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; + } + Image { + id: condition_icon_12h; + Layout.leftMargin:-2; + Layout.maximumWidth: 64 + Layout.maximumHeight: 64 + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; + Layout.fillHeight: true; fillMode: Image.PreserveAspectFit source: weather.weatherIcon; } + Text{ + id: timestamp_12h; + text: "12:00"; + font: Style.font.label; + Layout.columnSpan: 2 + color: "white" + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; + Layout.topMargin: -Style.itemMargins.slim; + Layout.bottomMargin: Style.itemMargins.slim; + } + }// Gridlayout Temp+Icon +12h + + }// Gridlayout + + function update_forecast(){ + console.log("update_forecast") + var idx =0; + for(idx =0 ; idx <5 ; idx++){ + console.log("Time"+ weather.get_forecast_timestamp(idx)) + console.log("\tTemp:" + weather.get_forecast_temperature(idx)+ "°C") + console.log("\tURL:" +weather.get_forecast_icon_url(idx) ); } + timestamp_6h.text = Qt.formatTime(weather.get_forecast_timestamp(2),"hh:mm"); + temperature_6h.text = Math.round(weather.get_forecast_temperature(2)) + "\u00B0C"; + condition_icon_6h.source = weather.get_forecast_icon_url(2); + + timestamp_12h.text = Qt.formatTime(weather.get_forecast_timestamp(4),"hh:mm"); + temperature_12h.text = Math.round(weather.get_forecast_temperature(4)) + "\u00B0C"; + condition_icon_12h.source = weather.get_forecast_icon_url(4); + } + /* one signal->update all forecasts*/ + Component.onCompleted: { + weather.forecast_available.connect(update_forecast) } -} +}//Page diff --git a/qtgui/qml/main.qml b/qtgui/qml/main.qml index 0bb2588..3d17b95 100644 --- a/qtgui/qml/main.qml +++ b/qtgui/qml/main.qml @@ -239,6 +239,7 @@ ApplicationWindow { stackView.pop(null); } } + } /**** global connections ****/ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e3ac102..fa26461 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -57,11 +57,11 @@ ELSE(NOT BUILD_GTEST_FROM_SRC) # https://github.com/google/googletest/tree/master/googletest#incorporating-into-an-existing-cmake-project #-------------------------------------------------------------------------- set(GTEST_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/GTestExternal/") - + # Download and unpack googletest at configure time configure_file(CMakeLists.txt.in ${GTEST_PREFIX}/CMakeLists.txt) - message(STATUS "Executing cmake on generated Download CMakeLists.txt") + message(STATUS "Executing cmake on generated Download CMakeLists.txt") execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . RESULT_VARIABLE result WORKING_DIRECTORY ${GTEST_PREFIX} ) @@ -69,7 +69,7 @@ ELSE(NOT BUILD_GTEST_FROM_SRC) message(FATAL_ERROR "CMake step for googletest failed: ${result}") endif() - message(STATUS "Building google test" ) + message(STATUS "Building google test" ) execute_process(COMMAND ${CMAKE_COMMAND} --build ${GTEST_PREFIX} RESULT_VARIABLE result WORKING_DIRECTORY ${GTEST_PREFIX}/googletest-build ) @@ -93,13 +93,13 @@ ELSE(NOT BUILD_GTEST_FROM_SRC) set(GTEST_INCLUDES "${GTEST_PREFIX}/googletest-src/googletest/include") set(GMOCK_INCLUDES "${GTEST_PREFIX}/googletest-src/googlemock/include") - + set(GTEST_LIBRARY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBPREFIX}gtest") set(GTEST_MAINLIB "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBPREFIX}gtest_main") set(GMOCK_LIBRARY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBPREFIX}gmock") set(GMOCK_MAINLIB "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBPREFIX}gmock_main") - + add_library(GTest IMPORTED STATIC GLOBAL) set_target_properties(GTest PROPERTIES IMPORTED_LOCATION "${GTEST_LIBRARY}${LIBSUFFIX}" @@ -113,7 +113,7 @@ ELSE(NOT BUILD_GTEST_FROM_SRC) IMPORTED_LOCATION_DEBUG "${GTEST_MAINLIB}${CMAKE_DEBUG_POSTFIX}${LIBSUFFIX}" INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDES}" IMPORTED_LINK_INTERFACE_LIBRARIES GTest) - + add_library(GMock IMPORTED STATIC GLOBAL) set_target_properties(GMock PROPERTIES IMPORTED_LOCATION "${GMOCK_LIBRARY}${LIBSUFFIX}" @@ -127,10 +127,10 @@ ELSE(NOT BUILD_GTEST_FROM_SRC) IMPORTED_LOCATION_DEBUG "${GMOCK_MAINLIB}${CMAKE_DEBUG_POSTFIX}${LIBSUFFIX}" INTERFACE_INCLUDE_DIRECTORIES "${GTEST_INCLUDES};${GMOCK_INCLUDES}" IMPORTED_LINK_INTERFACE_LIBRARIES GMock ) - + add_dependencies(GTest gtest_main gtest) add_dependencies(GMock gmock_main gmock) - + endif(NOT BUILD_GTEST_FROM_SRC) #------------------------------ @@ -155,16 +155,22 @@ CONFIGURE_FILE( SET(AUDIO_TEST_FILE "testaudio.mp3") CONFIGURE_FILE( - "${CMAKE_CURRENT_SOURCE_DIR}/${AUDIO_TEST_FILE}" + "${CMAKE_CURRENT_SOURCE_DIR}/${AUDIO_TEST_FILE}" "${CMAKE_BINARY_DIR}/${AUDIO_TEST_FILE}" COPYONLY) - + SET(WEATHER_TEST_FILE "sample_weather.json") CONFIGURE_FILE( - "${CMAKE_CURRENT_SOURCE_DIR}/${WEATHER_TEST_FILE}" + "${CMAKE_CURRENT_SOURCE_DIR}/${WEATHER_TEST_FILE}" "${CMAKE_BINARY_DIR}/${WEATHER_TEST_FILE}" COPYONLY) - + +SET(FORECAST_TEST_FILE "sample_forecast.json") +CONFIGURE_FILE( + "${CMAKE_CURRENT_SOURCE_DIR}/${FORECAST_TEST_FILE}" + "${CMAKE_BINARY_DIR}/${FORECAST_TEST_FILE}" + COPYONLY) + #------------------------------------------------------------------------------- # Unit test sources, TestDoubles, Mocks etc. #------------------------------------------------------------------------------- @@ -186,15 +192,15 @@ SET(TEST_HARNESS_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_sleeptimer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_powercontrol.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_hardware_config.cpp - ) - + ) + IF(HAS_WPA_SUPPLICANT) LIST(APPEND TEST_HARNESS_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_wpa_ctrl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_wifi_control.cpp ) -ENDIF() - +ENDIF() + #------------------------------ # Binary #------------------------------ @@ -211,10 +217,10 @@ SET_TARGET_PROPERTIES(${COMPONENT_NAME} ) TARGET_COMPILE_DEFINITIONS(${COMPONENT_NAME} - PUBLIC ${CPP_DEFS} + PUBLIC ${CPP_DEFS} ) -TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} +TARGET_COMPILE_OPTIONS(${COMPONENT_NAME} PRIVATE $<$:${CUSTOM_CXX_FLAGS}> $<$:${CUSTOM_C_FLAGS}>) @@ -234,10 +240,10 @@ TARGET_LINK_LIBRARIES( IF(TEST_COVERAGE) IF(NOT MSVC) #Remove googletest, autogenerated QT code and headers for testcoverage - SET(COVERAGE_EXCLUDES - '*GTestExternal/*' - '*PistacheExternal/*' - "*_autogen*" + SET(COVERAGE_EXCLUDES + '*GTestExternal/*' + '*PistacheExternal/*' + "*_autogen*" '/usr/*' ) SETUP_TARGET_FOR_COVERAGE( @@ -249,7 +255,7 @@ IF(TEST_COVERAGE) SET_PROPERTY(TARGET ${COMPONENT_NAME} APPEND PROPERTY LINK_FLAGS /PROFILE) ENDIF(NOT MSVC) -ENDIF(TEST_COVERAGE) +ENDIF(TEST_COVERAGE) # Call the testBinary with junit-xml output ADD_TEST(junitout diff --git a/test/sample_forecast.json b/test/sample_forecast.json new file mode 100644 index 0000000..7fcd4f2 --- /dev/null +++ b/test/sample_forecast.json @@ -0,0 +1,199 @@ +{ + "cod": "200", + "message": 0, + "cnt": 5, + "list": [ + { + "dt": 1584792000, + "main": { + "temp": 24.78, + "feels_like": 23.39, + "temp_min": 24.78, + "temp_max": 26.25, + "pressure": 1015, + "sea_level": 1015, + "grnd_level": 1012, + "humidity": 79, + "temp_kf": -1.47 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "clouds": { + "all": 38 + }, + "wind": { + "speed": 7.88, + "deg": 150 + }, + "rain": { + "3h": 1.44 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2020-03-21 12:00:00" + }, + { + "dt": 1584802800, + "main": { + "temp": 25.06, + "feels_like": 23.46, + "temp_min": 25.06, + "temp_max": 26.16, + "pressure": 1013, + "sea_level": 1013, + "grnd_level": 1011, + "humidity": 80, + "temp_kf": -1.1 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "clouds": { + "all": 77 + }, + "wind": { + "speed": 8.52, + "deg": 144 + }, + "rain": { + "3h": 2.06 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2020-03-21 15:00:00" + }, + { + "dt": 1584813600, + "main": { + "temp": 25.05, + "feels_like": 25.02, + "temp_min": 25.05, + "temp_max": 25.79, + "pressure": 1013, + "sea_level": 1013, + "grnd_level": 1010, + "humidity": 82, + "temp_kf": -0.74 + }, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "clouds": { + "all": 84 + }, + "wind": { + "speed": 6.57, + "deg": 145 + }, + "rain": { + "3h": 4.38 + }, + "sys": { + "pod": "n" + }, + "dt_txt": "2020-03-21 18:00:00" + }, + { + "dt": 1584824400, + "main": { + "temp": 25.7, + "feels_like": 24.62, + "temp_min": 25.7, + "temp_max": 26.07, + "pressure": 1013, + "sea_level": 1013, + "grnd_level": 1011, + "humidity": 83, + "temp_kf": -0.37 + }, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": { + "all": 97 + }, + "wind": { + "speed": 8.71, + "deg": 144 + }, + "rain": { + "3h": 5.06 + }, + "sys": { + "pod": "d" + }, + "dt_txt": "2020-03-21 21:00:00" + }, + { + "dt": 1584835200, + "main": { + "temp": 26.51, + "feels_like": 26.26, + "temp_min": 26.51, + "temp_max": 26.51, + "pressure": 1015, + "sea_level": 1015, + "grnd_level": 1013, + "humidity": 80, + "temp_kf": 0 + }, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": { + "all": 98 + }, + "wind": { + "speed": 7.67, + "deg": 136 + }, + "rain": { + "3h": 4.13 + }, + "sys": { + "pod": "d" + }, + "dt_txt": "2020-03-22 00:00:00" + } + ], + "city": { + "id": 2172797, + "name": "Cairns", + "coord": { + "lat": -16.9167, + "lon": 145.7667 + }, + "country": "AU", + "timezone": 36000, + "sunrise": 1584735656, + "sunset": 1584779235 + } +} \ No newline at end of file diff --git a/test/test_weather.cpp b/test/test_weather.cpp index d1752ab..fc919c5 100644 --- a/test/test_weather.cpp +++ b/test/test_weather.cpp @@ -29,28 +29,39 @@ using ::testing::AtLeast; class WeatherFile : public virtual ::testing::Test { public: WeatherFile() - : weatherFile(TEST_FILE_PATH + "/sample_weather.json"){ + : weatherFile(TEST_FILE_PATH + "/sample_weather.json") + , forecastFile(TEST_FILE_PATH + "/sample_forecast.json") + , weather_cfg( + QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")) { + if (!weatherFile.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << weatherFile.errorString(); throw std::system_error( make_error_code(std::errc::no_such_file_or_directory), weatherFile.errorString().toStdString()); } + + if (!forecastFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << forecastFile.errorString(); + throw std::system_error( + make_error_code(std::errc::no_such_file_or_directory), + forecastFile.errorString().toStdString()); + } + + EXPECT_CALL(cm, get_weather_config()) + .Times(AtLeast(1)) + .WillRepeatedly(ReturnRef(weather_cfg)); }; protected: QFile weatherFile; + QFile forecastFile; + WeatherConfig weather_cfg; CmMock cm; }; /*****************************************************************************/ TEST_F(WeatherFile, RefreshEmitsSignal) { - auto weather_cfg = DigitalRooster::WeatherConfig( - QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); ASSERT_TRUE(spy.isValid()); @@ -62,12 +73,6 @@ TEST_F(WeatherFile, RefreshEmitsSignal) { /*****************************************************************************/ TEST_F(WeatherFile, GetConfigForDownloadAfterTimerExpired) { - auto weather_cfg = DigitalRooster::WeatherConfig( - QString("a904431b4e0eae431bcc1e075c761abb"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(AtLeast(1)) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); ASSERT_TRUE(spy.isValid()); @@ -79,30 +84,18 @@ TEST_F(WeatherFile, GetConfigForDownloadAfterTimerExpired) { /*****************************************************************************/ TEST_F(WeatherFile, ParseTemperatureFromFile) { - auto weather_cfg = - DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(1) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); - dut.parse_response(weatherFile.readAll()); + dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); ASSERT_FLOAT_EQ(dut.get_temperature(), 16); } /*****************************************************************************/ TEST_F(WeatherFile, GetCityFromFile) { - auto weather_cfg = - DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(1) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); - dut.parse_response(weatherFile.readAll()); + dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); ASSERT_EQ(dut.get_city(), QString("Porto Alegre")); @@ -110,15 +103,9 @@ TEST_F(WeatherFile, GetCityFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, ParseConditionFromFile) { - auto weather_cfg = - DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(1) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(condition_changed(const QString&))); - dut.parse_response(weatherFile.readAll()); + dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); ASSERT_EQ(dut.get_condition(), QString("few clouds")); @@ -126,19 +113,80 @@ TEST_F(WeatherFile, ParseConditionFromFile) { /*****************************************************************************/ TEST_F(WeatherFile, IconURI) { - auto weather_cfg = - DigitalRooster::WeatherConfig(QString("ABC"), QString("2172797")); - EXPECT_CALL(cm, get_weather_config()) - .Times(1) - .WillRepeatedly(ReturnRef(weather_cfg)); - Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); - dut.parse_response(weatherFile.readAll()); + dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_weather_icon_url(), - QUrl("http://openweathermap.org/img/w/02d.png")); + ASSERT_EQ( + dut.get_weather_icon_url(), QUrl(WEATHER_ICON_BASE_URL + "02d.png")); +} + + +/*****************************************************************************/ +TEST_F(WeatherFile, parseForecasts5) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(forecast_available())); + ASSERT_TRUE(spy.isValid()); + dut.parse_forecast(forecastFile.readAll()); + spy.wait(10); + EXPECT_EQ(spy.count(), 1); + ASSERT_EQ(dut.get_forecasts().size(), 5); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, ForecastOutOfRange) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(forecast_available())); + ASSERT_TRUE(spy.isValid()); + dut.parse_forecast(forecastFile.readAll()); + spy.wait(10); + EXPECT_EQ(spy.count(), 1); + ASSERT_TRUE(std::isnan(dut.get_forecast_temperature(-1))); + ASSERT_TRUE(dut.get_forecast_icon_url(-1).isEmpty()); + ASSERT_FALSE(dut.get_forecast_timestamp(-1).isValid()); + + ASSERT_TRUE(std::isnan(dut.get_forecast_temperature(6))); + ASSERT_TRUE(dut.get_forecast_icon_url(6).isEmpty()); + ASSERT_FALSE(dut.get_forecast_timestamp(6).isValid()); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, ForeCastOk) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(forecast_available())); + ASSERT_TRUE(spy.isValid()); + dut.parse_forecast(forecastFile.readAll()); + spy.wait(10); + EXPECT_EQ(spy.count(), 1); + ASSERT_EQ(dut.get_forecasts().size(), 5); + ASSERT_EQ( + dut.get_forecast_icon_url(2), QUrl(WEATHER_ICON_BASE_URL + "10n.png")); + ASSERT_DOUBLE_EQ(dut.get_forecast_temperature(2), 25.05); + ASSERT_EQ(dut.get_forecast_timestamp(2).toSecsSinceEpoch(), 1584813600); + + ASSERT_EQ( + dut.get_forecast_icon_url(4), QUrl(WEATHER_ICON_BASE_URL + "10d.png")); + ASSERT_DOUBLE_EQ(dut.get_forecast_temperature(4), 26.51); + ASSERT_EQ(dut.get_forecast_timestamp(4).toSecsSinceEpoch(), 1584835200); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, ForcastMalformattedJSON) { + Weather dut(cm); + auto crap = QByteArray::fromStdString( + R"({This:"looks_like", "JSON":[], but:"is not!"})"); + dut.parse_forecast(crap); + ASSERT_EQ(dut.get_forecasts().size(), 0); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, ForcastNoInfo) { + Weather dut(cm); + auto crap = QByteArray::fromStdString( + R"({"This":"is valid", "weather":[], "main":{"but":false}})"); + dut.parse_forecast(crap); + ASSERT_EQ(dut.get_forecasts().size(), 0); } /*****************************************************************************/ From b1d3a6d53171edb36bc3bd0b4a23a52f49e8257a Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Wed, 25 Mar 2020 20:23:25 +0100 Subject: [PATCH 18/26] Bugfix/clockpage layout (#21) * ForecastWidget * Font scaling issues (still not happy with it) * refresh weather on press * unify current weather and forecasts * only download 24h forecasts Signed-off-by: Thomas Ruschival --- include/appconstants.hpp | 6 + include/weather.hpp | 171 ++++++++++-------- libsrc/weather.cpp | 210 +++++++++++----------- qtgui/externalRes/IconButton.qml | 2 +- qtgui/main.cpp | 20 ++- qtgui/qml/ClockPage.qml | 158 +++------------- qtgui/qml/ForecastWidget.qml | 75 ++++++++ qtgui/qml/Style.qml | 238 ++++++++++++------------ qtgui/qml/qml.qrc | 1 + test/sample_weather.json | 6 +- test/test_weather.cpp | 299 ++++++++++++++++++++++++------- 11 files changed, 691 insertions(+), 495 deletions(-) create mode 100644 qtgui/qml/ForecastWidget.qml diff --git a/include/appconstants.hpp b/include/appconstants.hpp index 2f17cef..af22039 100644 --- a/include/appconstants.hpp +++ b/include/appconstants.hpp @@ -240,6 +240,12 @@ const int DEFAULT_BRIGHTNESS = 25; */ const int DEFAULT_ICON_WIDTH = 88; +/** + * number of forecasts to fetch + * 8*3h = 24h + */ +const int WEATHER_FORECAST_COUNT = 8; + /** * Where to find icons for weather condition */ diff --git a/include/weather.hpp b/include/weather.hpp index 613ceaa..72fcb17 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -28,6 +28,7 @@ namespace DigitalRooster { +class Weather; class ConfigurationManager; /** @@ -35,19 +36,48 @@ class ConfigurationManager; * Still a QObject, would be nice if I figure out how to pass a list * of Forecasts to QML (and keep the ressources managed and thread-safe) */ -class Forecast : public QObject { +class WeatherStatus : public QObject { Q_OBJECT Q_PROPERTY(QDateTime timestamp READ get_timestamp) - Q_PROPERTY(double temperature READ get_temperature) - Q_PROPERTY(QUrl weatherIcon READ get_weather_icon_url) + Q_PROPERTY(double temp READ get_temperature) + Q_PROPERTY(double temp_min READ get_max_temperature) + Q_PROPERTY(double temp_max READ get_min_temperature) + Q_PROPERTY(QUrl icon_url READ get_weather_icon_url) public: - Forecast() = default; + WeatherStatus() = default; + /** - * Create a new forecast from JSON object - * @param json - * @param parent + * Cloning constructor - Q_OBJECT does not allow copy construction + * @param other where to take the fields from */ - explicit Forecast(const QJsonObject& json); + explicit WeatherStatus(const WeatherStatus* other) { + timestamp = other->timestamp; + icon_url = other->icon_url; + temp_max = other->temp_max; + temp_min = other->temp_min; + temperature = other->temperature; + } + + /** + * Update status with data from JSON object + * @return true if update worked, false otherwise + * @param json a 'weather' object: + * { + * "dt": 1584792000, + * "main": { + * "temp": 24.78, + * "temp_min": 24.78, + * "temp_max": 26.25, + * }, + * "weather": [ + * { + * "id": 500, + * "icon": "10n" + * } + * ] + * } + */ + bool update(const QJsonObject& json); QDateTime get_timestamp() const { return timestamp; @@ -61,11 +91,33 @@ class Forecast : public QObject { return temperature; }; + double get_min_temperature() const { + return temp_min; + }; + + double get_max_temperature() const { + return temp_max; + } + /** + * Basic validation of json from API will throw if a elementary object e.g. + * 'main' or 'weather' is missing + * @param json + */ + static void validate_json(const QJsonObject& json); + private: /** - * expected temperature at timepoint + * temperature at timepoint */ double temperature = 0.0; + /** + * measured/expected temperature at timepoint + */ + double temp_min = 0.0; + /** + * maximum measured/expected temperature at timepoint + */ + double temp_max = 0.0; /** * Weather Icon URL @@ -76,16 +128,27 @@ class Forecast : public QObject { * Timepoint for this forecast */ QDateTime timestamp; + + /** + * update min/max current temperature + * @param json object that contains a "main" object of forecast or current + * weather: "main": { "temp": 16, "pressure": 1018, "humidity": 93, + * "temp_min": 16, + * "temp_max": 16 + * } + */ + void parse_temperatures(const QJsonObject& json); }; /** * Periodically downloads weather info from Openweathermaps + * weather information is parsed into a WeatherStatus objects and + * can be accessed with get_weather() */ class Weather : public QObject { Q_OBJECT Q_PROPERTY(QString city READ get_city NOTIFY city_updated) - Q_PROPERTY(QString condition READ get_condition NOTIFY condition_changed) Q_PROPERTY( float temperature READ get_temperature NOTIFY temperature_changed) Q_PROPERTY(QUrl weatherIcon READ get_weather_icon_url NOTIFY icon_changed) @@ -105,19 +168,16 @@ class Weather : public QObject { std::chrono::seconds get_update_interval() const; /** - * Access to temperature + * Access to current temperature + * @deprecated but available for compatibility */ - double get_temperature() const { - return temperature; - } - + double get_temperature() const; /** - * Current wheather condition description (localized string) - * e.g. "light clouds", "light intensity drizzle" + * Icon url for current condition + * @deprecated but available for compatibility */ - QString get_condition() const { - return condition; - } + QUrl get_weather_icon_url() const; + /** * Access to city name */ @@ -125,13 +185,6 @@ class Weather : public QObject { return city_name; } - /** - * Construct a URL from icon_id - */ - QUrl get_weather_icon_url() const { - return icon_url; - } - /** * Ugly Interface to pass the information to QML * I would like to keep a list of Forecasts in C++ and pass it to QML @@ -146,38 +199,25 @@ class Weather : public QObject { * dangling pointer. * TODO: I have not figured out how to return a * QList> + * + * Instead I opted for a static array \ref weather where 0 is the current + * weather */ - /** - * Get a copy of Forecast list - * @return - */ - const QList> get_forecasts() const; /** - * reach into list of forecasts and get temperature - * @param fc_idx index in list - * @return tempertature + * Access the weather status at given time in the future or now (idx=0 + * + * @param idx 0=current condition forecast for now+idx*3h (more or less) + * @return Wheater status object */ - Q_INVOKABLE double get_forecast_temperature(int fc_idx) const; - /** - * reach into list of forecasts and get timestamp - * @param fc_idx index in list - * @return tempertature - */ - Q_INVOKABLE QDateTime get_forecast_timestamp(int fc_idx) const; - /** - * reach into list of forecasts and get icon url - * @param fc_idx index in list - * @return tempertature - */ - Q_INVOKABLE QUrl get_forecast_icon_url(int fc_idx) const; + Q_INVOKABLE DigitalRooster::WeatherStatus* get_weather(int idx) const; public slots: /** * Slot triggers refresh of weather data and starts a new download process */ - void refresh(); + Q_INVOKABLE void refresh(); /** * Read Current weather status as JSON and update member fields @@ -197,11 +237,6 @@ public slots: */ void weather_info_updated(); - /** - * Current Condition description - */ - void condition_changed(const QString& temp); - /** * City name available */ @@ -210,7 +245,7 @@ public slots: /** * Current temperature */ - void temperature_changed(const double temperature); + void temperature_changed(double temperature); /** * Weather icon corresponding to current condition @@ -234,26 +269,11 @@ public slots: */ std::chrono::seconds update_interval{3600LL}; - /** - * Current Temperature - */ - double temperature = 0.0; - /** * Name of location matching the ID, retured by weather-api */ QString city_name; - /** - * Localized weather condition string - */ - QString condition; - - /** - * Icon matching the current weather condition returned by weather-api - */ - QUrl icon_url; - /** * QTimer for periodic updates */ @@ -269,14 +289,13 @@ public slots: * Mutex to lock list of forecasts */ mutable std::mutex forecast_mtx; + /** - * list of forecasts + * Static array of Weatherstatus + * no + 24*3h forecasts should be enough */ - QList> forecasts; - - void parse_city(const QJsonObject& o); - void parse_temperature(const QJsonObject& o); - void parse_condition(const QJsonObject& o); + std::array + weather; }; /** diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index 995cd99..fa0dc23 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -68,6 +68,7 @@ void Weather::refresh() { weather_downloader.doDownload(create_weather_url(cm.get_weather_config())); forecast_downloader.doDownload( create_forecast_url(cm.get_weather_config())); + timer.start(); } /*****************************************************************************/ @@ -76,14 +77,48 @@ void Weather::parse_weather(const QByteArray& content) { QJsonParseError perr; QJsonDocument doc = QJsonDocument::fromJson(content, &perr); if (perr.error != QJsonParseError::NoError) { - qCWarning(CLASS_LC) << "Parsing weather failed"; + qCWarning(CLASS_LC) << "Parsing weather report failed"; return; } - QJsonObject o = doc.object(); - parse_city(o); - parse_temperature(o); - parse_condition(o); // also extracts icon - emit weather_info_updated(); + // input checking in WeatherStatus::validate() called by update(); + auto json = doc.object(); + // Weather 0 is current status + if (weather[0].update(json)) { + emit weather_info_updated(); + emit temperature_changed(weather[0].get_temperature()); + emit icon_changed(weather[0].get_weather_icon_url()); + } + + // City is in main.name - in forecast in city.name! + if (json["name"].isString()) { + city_name = json["name"].toString(); + qCInfo(CLASS_LC) << "Weather location:" << city_name; + emit city_updated(city_name); + } else { + qCWarning(CLASS_LC) << "Could not read City name for location"; + } +} + +/*****************************************************************************/ +double Weather::get_temperature() const { + return weather[0].get_temperature(); +} + +/*****************************************************************************/ +QUrl Weather::get_weather_icon_url() const { + return weather[0].get_weather_icon_url(); +} + +/*****************************************************************************/ +WeatherStatus* Weather::get_weather(int idx) const { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + try { + auto ret = new WeatherStatus(&weather.at(idx)); + return ret; + } catch (std::out_of_range& exc) { + qCCritical(CLASS_LC) << "Forecast index out of range!"; + return nullptr; // would be nice to throw exception to QML instead + } } /*****************************************************************************/ @@ -95,15 +130,19 @@ void Weather::parse_forecast(const QByteArray& content) { qCWarning(CLASS_LC) << "Parsing forecast failed"; return; } + + // QJson is very forgiving if "list" does not exist or is not an array + // default will be created auto fc_array = doc["list"].toArray(); - { // scope for lock - const std::lock_guard lock(forecast_mtx); - forecasts.clear(); - for (const auto fc_val : fc_array) { - forecasts.append( - std::make_shared(fc_val.toObject())); - } - } // emit outside of lock, code is executed in the same thread + if (fc_array.size() <= 0) { + qCWarning(CLASS_LC) << "no forecast 'list' found!"; + return; + } + + auto max_idx = std::min(fc_array.size(), static_cast(weather.size())); + for (int i = 1; i < max_idx; i++) { + weather[i].update(fc_array[i].toObject()); + } emit forecast_available(); } @@ -132,91 +171,12 @@ QUrl DigitalRooster::create_forecast_url(const WeatherConfig& cfg) { request_str += "&units=metric"; request_str += "&appid="; request_str += cfg.get_api_token(); - request_str += "&lang=en"; // default english - request_str += "&cnt=5"; // ~now, +3h, +6h, +9h, +12h + request_str += "&lang=en"; // default english + request_str += "&cnt=" + WEATHER_FORECAST_COUNT; // qCDebug(CLASS_LC) << request_str; return QUrl(request_str); } -/*****************************************************************************/ -const QList> Weather::get_forecasts() const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - const std::lock_guard lock(forecast_mtx); - auto ret = forecasts; // copy - return ret; -} - -/*****************************************************************************/ -void Weather::parse_city(const QJsonObject& o) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - city_name = o["name"].toString(); - qCInfo(CLASS_LC) << "Weather location:" << city_name; - emit city_updated(city_name); -} - -/*****************************************************************************/ -void Weather::parse_temperature(const QJsonObject& o) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - auto main = o["main"].toObject(); - if (!main.isEmpty()) { - temperature = main["temp"].toDouble(); - emit temperature_changed(temperature); - } -} - -/*****************************************************************************/ -void Weather::parse_condition(const QJsonObject& o) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - // Weather is an array - auto weather_arr = o["weather"].toArray(); - auto weather = weather_arr.at(0); - if (weather.isUndefined()) { - qCWarning(CLASS_LC) << "couldn't read weather JSON object"; - return; - } - condition = weather["description"].toString(); - icon_url = - QUrl(WEATHER_ICON_BASE_URL + weather["icon"].toString() + ".png"); - emit condition_changed(condition); - emit icon_changed(icon_url); -} - -/*****************************************************************************/ -double Weather::get_forecast_temperature(int fc_idx) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - - const std::lock_guard lock(forecast_mtx); - if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { - qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; - return std::nan(""); - } - return forecasts[fc_idx]->get_temperature(); -} - -/*****************************************************************************/ -QDateTime Weather::get_forecast_timestamp(int fc_idx) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - - const std::lock_guard lock(forecast_mtx); - if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { - qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; - return QDateTime(); - } - return forecasts[fc_idx]->get_timestamp(); -} - -/*****************************************************************************/ -QUrl Weather::get_forecast_icon_url(int fc_idx) const { - qCDebug(CLASS_LC) << Q_FUNC_INFO; - - const std::lock_guard lock(forecast_mtx); - if (!(fc_idx >= 0 && fc_idx < forecasts.length())) { - qCCritical(CLASS_LC) << Q_FUNC_INFO << fc_idx << "index out of range"; - return QUrl(); - } - return forecasts[fc_idx]->get_weather_icon_url(); -} - /*****************************************************************************/ WeatherConfig::WeatherConfig(const QString& token, const QString& location, const std::chrono::seconds& interval) @@ -253,27 +213,65 @@ WeatherConfig WeatherConfig::from_json_object(const QJsonObject& json) { } /*****************************************************************************/ -Forecast::Forecast(const QJsonObject& json) { - qCDebug(CLASS_LC) << Q_FUNC_INFO; +void WeatherStatus::validate_json(const QJsonObject& json) { + if (json["dt"].isUndefined()) { + throw std::runtime_error("no 'dt' in json object"); + } + if (json["weather"].isUndefined() || json["weather"].toArray().isEmpty()) { + throw std::runtime_error("no 'weather' in json object"); + } + if (json["main"].isUndefined()) { + throw std::runtime_error("no 'main' in json object"); + } auto main = json["main"].toObject(); + if (!main["temp"].isDouble()) { + throw std::runtime_error("temperature in 'main' is NaN"); + } + if (!main["temp_min"].isDouble()) { + throw std::runtime_error("temp_min in 'main' is NaN"); + } + if (!main["temp_max"].isDouble()) { + throw std::runtime_error("temp_max in 'main' is NaN"); + } +} + +/*****************************************************************************/ +bool WeatherStatus::update(const QJsonObject& json) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + // try-catch here because we are called in signal-slot context + try { + validate_json(json); + } catch (std::runtime_error& exc) { + qCCritical(CLASS_LC) << Q_FUNC_INFO << exc.what(); + return false; + } + // extract temperatures + parse_temperatures(json); + // for some reasons the weather is an array with 1 element auto weather_array = json["weather"].toArray(); auto weather = weather_array.at(0).toObject(); + // get Icon + icon_url = + QUrl(WEATHER_ICON_BASE_URL + weather["icon"].toString() + ".png"); + // get timestamp timestamp = QDateTime::fromSecsSinceEpoch(json["dt"].toInt(), QTimeZone::utc()); - if (!main.isEmpty()) { - temperature = main["temp"].toDouble(); - } - if (!weather.isEmpty()) { - icon_url = - QUrl(WEATHER_ICON_BASE_URL + weather["icon"].toString() + ".png"); - } - - qCDebug(CLASS_LC) << "forecast for" << timestamp << ":" << temperature + qCDebug(CLASS_LC) << "forecast for" << timestamp << " temp:" << temperature << "°C icon:" << icon_url; + return true; +} + +/*****************************************************************************/ +void WeatherStatus::parse_temperatures(const QJsonObject& json) { + qCDebug(CLASS_LC) << Q_FUNC_INFO; + auto main = json["main"].toObject(); + temperature = main["temp"].toDouble(); + temp_min = main["temp_min"].toDouble(); + temp_max = main["temp_max"].toDouble(); } /*****************************************************************************/ diff --git a/qtgui/externalRes/IconButton.qml b/qtgui/externalRes/IconButton.qml index efedf85..2ac1602 100644 --- a/qtgui/externalRes/IconButton.qml +++ b/qtgui/externalRes/IconButton.qml @@ -4,7 +4,7 @@ // https://github.com/kevincarlson/QmlBridgeForMaterialDesignIcons import QtQuick 2.9 -import QtQuick.Controls 2.0 +import QtQuick.Controls 2.2 Button { font: Style.font.button; diff --git a/qtgui/main.cpp b/qtgui/main.cpp index b087517..f8c8aaf 100644 --- a/qtgui/main.cpp +++ b/qtgui/main.cpp @@ -90,7 +90,9 @@ std::shared_ptr DigitalRooster::wallclock = /*****************************************************************************/ int main(int argc, char* argv[]) { - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + //QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + /* Force 96 DPI independent of display DPI by OS */ + // QCoreApplication::setAttribute(Qt::AA_Use96Dpi); QCoreApplication::setApplicationName(APPLICATION_NAME); QCoreApplication::setApplicationVersion(PROJECT_VERSION); QGuiApplication app(argc, argv); @@ -134,6 +136,13 @@ int main(int argc, char* argv[]) { } else { // Default behavour as before setup_logger_file(DEFAULT_LOG_PATH); } + + double dpi = QGuiApplication::primaryScreen()->logicalDotsPerInch(); + qCInfo(MAIN) << "DPI (physical):" << QGuiApplication::primaryScreen()->physicalDotsPerInch(); + qCInfo(MAIN) << "DPI (logical):" << dpi; + qCInfo(MAIN) << "Screen Size:" << QGuiApplication::primaryScreen()->size(); + qCInfo(MAIN) << "DevicePixelRatio:" << QGuiApplication::primaryScreen()->devicePixelRatio(); + qCInfo(MAIN) << "confpath: " << cmdline.value(confpath); qCInfo(MAIN) << "cachedir: " << cmdline.value(cachedir); qCInfo(MAIN) << APPLICATION_NAME << " - " << GIT_REVISION; @@ -248,9 +257,9 @@ int main(int argc, char* argv[]) { qmlRegisterUncreatableType( "ruschi.WifiListModel", 1, 0, "WifiListModel", "QML must not instantiate WifiListModel!"); - qmlRegisterUncreatableType( - "ruschi.Forecast", 1, 0, "Forecast", - "QML must not instantiate Forecast!"); + qmlRegisterUncreatableType( + "ruschi.WeatherStatus", 1, 0, "WeatherStatus", + "QML must not instantiate WeatherStatus!"); QQmlApplicationEngine view; QQmlContext* ctxt = view.rootContext(); @@ -271,8 +280,11 @@ int main(int argc, char* argv[]) { ctxt->setContextProperty("brightnessControl", &brightness); ctxt->setContextProperty("volumeButton", &volbtn); ctxt->setContextProperty("sleeptimer", &sleeptimer); + ctxt->setContextProperty( "DEFAULT_ICON_WIDTH", QVariant::fromValue(DEFAULT_ICON_WIDTH)); + ctxt->setContextProperty( + "FONT_SCALING", QVariant::fromValue(dpi)); view.load(QUrl("qrc:/main.qml")); diff --git a/qtgui/qml/ClockPage.qml b/qtgui/qml/ClockPage.qml index 4692299..293c3bd 100644 --- a/qtgui/qml/ClockPage.qml +++ b/qtgui/qml/ClockPage.qml @@ -7,149 +7,49 @@ Page { property string objectName : "ClockPage" GridLayout{ - columns: 2; - rows: 3; - anchors.fill: parent - anchors.leftMargin: Style.itemMargins.sparse; - anchors.rightMargin: Style.itemMargins.sparse; - anchors.bottomMargin: Style.itemMargins.sparse; + columns: 3; + rows: 2; + anchors.fill: parent; + anchors.margins: Style.itemMargins.slim; + anchors.bottomMargin: Style.itemMargins.slim; + rowSpacing: 0; + columnSpacing: 3; + Text{ text: currentTime.timestring_lz_hh_mm font: Style.font.clock; color: "white" - Layout.columnSpan: 2 + Layout.columnSpan: 3; + Layout.topMargin: Style.itemMargins.medium; + Layout.bottomMargin:0; Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter ; - anchors.bottomMargin: Style.itemMargins.extrawide; } - RowLayout{ - Layout.columnSpan: 2 - Layout.margins: Style.itemMargins.slim; - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - Text{ - text: Math.round(weather.temperature) + "\u00B0C"; - font: Style.font.subtitle; - color: "white" - Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; - } - Image { - Layout.maximumWidth: 72 - Layout.maximumHeight: 72 - Layout.preferredWidth: 72 - Layout.preferredHeight: 72 - Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; - Layout.fillHeight: true; - fillMode: Image.PreserveAspectFit - source: weather.weatherIcon; - } - }// Rowlayout Temp+Icon current - - /* Text{ */ - /* text: weather.city; */ - /* font: Style.font.label; */ - /* color: "white" */ - /* Layout.columnSpan: 2 */ - /* Layout.margins: 0; */ - /* Layout.alignment: Qt.AlignHCenter */ - /* } */ - - GridLayout{ - columns: 2; - rows: 2; - rowSpacing:0; - columnSpacing: Style.itemMargins.slim; - - Layout.margins: Style.itemMargins.slim; - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter - - Text{ - id: temperature_6h; - text: Math.round(weather.temperature) + "\u00B0C"; - font: Style.font.label; - color: "white" - Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; - } - Image { - id: condition_icon_6h; - Layout.leftMargin:-2; - Layout.maximumWidth: 64 - Layout.maximumHeight: 64 - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; - Layout.fillHeight: true; - fillMode: Image.PreserveAspectFit - source: weather.weatherIcon; - } - Text{ - id: timestamp_6h; - text: "6:00"; - font: Style.font.label; - Layout.columnSpan: 2 - color: "white" - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; - Layout.topMargin: -Style.itemMargins.slim; - Layout.bottomMargin: Style.itemMargins.slim; - } - }// Gridlayout Temp+Icon +6h - - GridLayout{ - columns: 2; - rows: 2; - rowSpacing:0; - columnSpacing: Style.itemMargins.slim; + ForecastWidget{ + id:fc0h; + timestamp: "+00 h"; + Layout.alignment: Qt.AlignHCenter| Qt.AlignVCenter; + } - Layout.margins: Style.itemMargins.slim; - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + ForecastWidget{ + id:fc6h; + timestamp: "+06 h"; + Layout.alignment: Qt.AlignHCenter| Qt.AlignVCenter; + } - Text{ - id: temperature_12h; - text: Math.round(weather.temperature) + "\u00B0C"; - font: Style.font.label; - color: "white" - Layout.alignment: Qt.AlignRight| Qt.AlignVCenter; - } - Image { - id: condition_icon_12h; - Layout.leftMargin:-2; - Layout.maximumWidth: 64 - Layout.maximumHeight: 64 - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - Layout.alignment: Qt.AlignLeft| Qt.AlignVCenter; - Layout.fillHeight: true; - fillMode: Image.PreserveAspectFit - source: weather.weatherIcon; - } - Text{ - id: timestamp_12h; - text: "12:00"; - font: Style.font.label; - Layout.columnSpan: 2 - color: "white" - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; - Layout.topMargin: -Style.itemMargins.slim; - Layout.bottomMargin: Style.itemMargins.slim; - } - }// Gridlayout Temp+Icon +12h + ForecastWidget{ + id:fc12h; + timestamp: "+12 h"; + Layout.alignment: Qt.AlignHCenter| Qt.AlignVCenter; + } }// Gridlayout function update_forecast(){ console.log("update_forecast") - var idx =0; - for(idx =0 ; idx <5 ; idx++){ - console.log("Time"+ weather.get_forecast_timestamp(idx)) - console.log("\tTemp:" + weather.get_forecast_temperature(idx)+ "°C") - console.log("\tURL:" +weather.get_forecast_icon_url(idx) ); - } - timestamp_6h.text = Qt.formatTime(weather.get_forecast_timestamp(2),"hh:mm"); - temperature_6h.text = Math.round(weather.get_forecast_temperature(2)) + "\u00B0C"; - condition_icon_6h.source = weather.get_forecast_icon_url(2); - - timestamp_12h.text = Qt.formatTime(weather.get_forecast_timestamp(4),"hh:mm"); - temperature_12h.text = Math.round(weather.get_forecast_temperature(4)) + "\u00B0C"; - condition_icon_12h.source = weather.get_forecast_icon_url(4); + fc0h.update(weather.get_weather(0)); + fc6h.update(weather.get_weather(2)); + fc12h.update(weather.get_weather(4)); } /* one signal->update all forecasts*/ diff --git a/qtgui/qml/ForecastWidget.qml b/qtgui/qml/ForecastWidget.qml new file mode 100644 index 0000000..105c2c2 --- /dev/null +++ b/qtgui/qml/ForecastWidget.qml @@ -0,0 +1,75 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + + +Rectangle { + property double temperature; + property string timestamp; + property url condition_icon; + + height: 80; + width: 100; + color: "transparent"; + // border.width: 1; + // border.color: "green"; + + GridLayout{ + rows: 2; + columns: 2; + rowSpacing: 0; + columnSpacing:0; + anchors.fill: parent; + anchors.margins: Style.itemMargins.slim; + anchors.topMargin: 0; + + Text{ + id: timestamp; + text: "16:00"; + font: Style.font.label; + color: "white"; + Layout.leftMargin: 3; + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; + } + + Image { + id: condition_icon; + fillMode: Image.PreserveAspectFit + + Layout.margins: -10; + Layout.minimumHeight: 64; + Layout.preferredHeight: 72; + Layout.maximumHeight: 72; + Layout.fillWidth: true; + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; + } + + Text{ + id: temps; + text: "-20 / 12\u00B0C"; + font: Style.font.weatherInfo; + color: "white" + Layout.topMargin: -5; + Layout.columnSpan: 2; + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; + } + + } + + function update(weather){ + console.log("update: " + weather.timestamp); + timestamp.text = Qt.formatTime(weather.timestamp,"hh:mm"); + temps.text = Math.round(weather.temp_min)+" | "+ + Math.round(weather.temp_max)+"\u00B0C"; + condition_icon.source = weather.icon_url; + } + + MouseArea{ + anchors.fill: parent; + pressAndHoldInterval: 500; //ms press to refresh + onPressAndHold: { + console.log("Refresh weather") + weather.refresh(); + } + } +} diff --git a/qtgui/qml/Style.qml b/qtgui/qml/Style.qml index 1136cd4..266f429 100644 --- a/qtgui/qml/Style.qml +++ b/qtgui/qml/Style.qml @@ -5,128 +5,144 @@ import QtQuick.Controls.Material 2.1 pragma Singleton QtObject { - property int canvasWidth: 320; - property int canvasHeight: 240; + property int canvasWidth: 320; + property int canvasHeight: 240; - property int toolbarWidth: 320; + property int toolbarWidth: 320; property int toolbarHeight: 42; - property int contentWidth: 320; - property int contentHeight: canvasHeight-toolbarHeight; + property int contentWidth: 320; + property int contentHeight: canvasHeight-toolbarHeight; - property QtObject colors: QtObject{ - property color selected : Material.primary; // "#2196F3"; - property color unselected: Material.color(Material.Grey) ;// "gainsboro"; - property color disabled: Material.color(Material.Grey); - property color enabled: Material.primary; //"#2196F3"; - } + property QtObject colors: QtObject{ + property color selected : Material.primary; // "#2196F3"; + property color unselected: Material.color(Material.Grey) ;// "gainsboro"; + property color disabled: Material.color(Material.Grey); + property color enabled: Material.primary; //"#2196F3"; + } - property QtObject font: QtObject{ + function scaleFont(px){ + return Math.ceil(px*1.5); + } - property font title: Qt.font({ + property QtObject font: QtObject{ + + property font title: Qt.font({ + weight: Font.Normal, + pixelSize: scaleFont(18) + }); + + property font titleBold: Qt.font({ + weight: Font.Bold, + pixelSize: scaleFont(18) + }); + + property font subtitle: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.DemiBold, + pixelSize: scaleFont(14) + }); + + property font weatherInfo: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.DemiBold, + pixelSize: scaleFont(12) + }); + + property font weatherTime: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.Normal, + pixelSize: scaleFont(12) + }); + + property font listItemHead: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.DemiBold, + letterSpacing:-1, + pixelSize: scaleFont(12) + }); + + property font listItemHeadListened: Qt.font({ + family: "DejaVu Sans Condensed", weight: Font.Normal, - pointSize: 16 - }); - - property font titleBold: Qt.font({ + letterSpacing:-1, + pixelSize: scaleFont(12) + }); + + property font label: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.DemiBold, + letterSpacing:-1, + pixelSize: scaleFont(10) + }); + + property font boldLabel: Qt.font({ + family: "DejaVu Sans Condensed", weight: Font.Bold, - pointSize: 16 + letterSpacing:-1, + pixelSize: scaleFont(10) }); - property font subtitle: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.DemiBold, - pointSize: 12 - }); + property font valueLabel: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.Normal, + letterSpacing:-1, + pixelSize: scaleFont(10) + }); + + property font flowText: Qt.font({ + family: "DejaVu Sans Condensed", + weight: Font.Normal, + letterSpacing:-1, + pixelSize: scaleFont(10) + }); - property font listItemHead: Qt.font({ - family: "DejaVu Sans Condensed", + // -- elements + property font clock: Qt.font({ + weight: Font.Bold, + pixelSize: scaleFont(64) + }) + + property font tumbler: Qt.font({ + weight: Font.Bold, + pixelSize: scaleFont(16) + }); + + property font button: Qt.font({ + family: "Material Design Icons", weight: Font.DemiBold, - letterSpacing:-1, - pointSize: 12 - }); - - property font listItemHeadListened: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.Normal, - letterSpacing:-1, - pointSize: 12 - }); - - property font label: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.DemiBold, - letterSpacing:-1, - pointSize: 10 - }); - - property font boldLabel: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.Bold, - letterSpacing:-1, - pointSize: 10 - }); - - property font valueLabel: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.Normal, - letterSpacing:-1, - pointSize: 10 - }); - - property font flowText: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.Normal, - letterSpacing:-1, - pointSize: 10, - }); - - // -- elements - property font clock: Qt.font({ - weight: Font.Bold, - pointSize: 42 - }) - - property font tumbler: Qt.font({ - weight: Font.Bold, - pointSize: 16 - }); - - property font button: Qt.font({ - family: "Material Design Icons", - weight: Font.DemiBold, - pointSize: 18 - }); - - property font sliderValue: Qt.font({ - weight: Font.DemiBold, - pointSize: 16 - }); - }// font - - property QtObject buttons: QtObject{ - property int minW: 42; - property int minH: 42; - property int normalW: 44; - property int normalH: 44; - } - - property QtObject drawer: QtObject{ - property int w: 48; - property int h: 48; - } - - property QtObject itemSpacings: QtObject{ - property int dense: 2; - property int medium: 4; - property int sparse: 6; - } - - property QtObject itemMargins: QtObject{ - property int slim: 2; - property int medium: 4; - property int wide: 6; - property int extrawide: 10; - } + pixelSize: scaleFont(18) + }); + + property font sliderValue: Qt.font({ + weight: Font.DemiBold, + pixelSize: scaleFont(16) + }); + }// font + + property QtObject buttons: QtObject{ + property int minW: 42; + property int minH: 42; + property int normalW: 44; + property int normalH: 44; + } + + property QtObject drawer: QtObject{ + property int w: 48; + property int h: 48; + } + + property QtObject itemSpacings: QtObject{ + property int dense: 2; + property int medium: 4; + property int sparse: 6; + } + + property QtObject itemMargins: QtObject{ + property int slim: 2; + property int medium: 4; + property int wide: 6; + property int extrawide: 10; + } } diff --git a/qtgui/qml/qml.qrc b/qtgui/qml/qml.qrc index 288a7fc..3aabae0 100644 --- a/qtgui/qml/qml.qrc +++ b/qtgui/qml/qml.qrc @@ -12,6 +12,7 @@ AlarmList.qml Clock.qml ClockPage.qml + ForecastWidget.qml IRadioDelegate.qml IRadioList.qml Jsutil.js diff --git a/test/sample_weather.json b/test/sample_weather.json index 9495fb2..7d3ea94 100644 --- a/test/sample_weather.json +++ b/test/sample_weather.json @@ -13,11 +13,11 @@ ], "base": "stations", "main": { - "temp": 16, + "temp": 16.7, "pressure": 1018, "humidity": 93, - "temp_min": 16, - "temp_max": 16 + "temp_min": 15.1, + "temp_max": 18.4 }, "visibility": 8000, "wind": { diff --git a/test/test_weather.cpp b/test/test_weather.cpp index fc919c5..334f510 100644 --- a/test/test_weather.cpp +++ b/test/test_weather.cpp @@ -6,7 +6,7 @@ * * \copyright (c) 2018 Thomas Ruschival * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later - * SPDX-License-Identifier: GPL-3.0-or-later} + * SPDX-License-Identifier: GPL-3.0-or-later} * *****************************************************************************/ #include @@ -61,21 +61,24 @@ class WeatherFile : public virtual ::testing::Test { }; /*****************************************************************************/ -TEST_F(WeatherFile, RefreshEmitsSignal) { +TEST_F(WeatherFile, CheckSignals) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); ASSERT_TRUE(spy.isValid()); - spy.wait(500); - dut.refresh(); - spy.wait(1500); // make sure we have enough time to download info - ASSERT_EQ(spy.count(), 2); + QSignalSpy spy2(&dut, SIGNAL(forecast_available())); + ASSERT_TRUE(spy2.isValid()); + QSignalSpy spy3(&dut, SIGNAL(city_updated(const QString&))); + ASSERT_TRUE(spy3.isValid()); + QSignalSpy spy4(&dut, SIGNAL(temperature_changed(double))); + ASSERT_TRUE(spy4.isValid()); + QSignalSpy spy5(&dut, SIGNAL(icon_changed(const QUrl&))); + ASSERT_TRUE(spy5.isValid()); } /*****************************************************************************/ TEST_F(WeatherFile, GetConfigForDownloadAfterTimerExpired) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); - ASSERT_TRUE(spy.isValid()); dut.set_update_interval(seconds(1)); ASSERT_EQ(dut.get_update_interval(), std::chrono::seconds(1)); spy.wait(500); // first download @@ -83,121 +86,287 @@ TEST_F(WeatherFile, GetConfigForDownloadAfterTimerExpired) { } /*****************************************************************************/ -TEST_F(WeatherFile, ParseTemperatureFromFile) { +TEST_F(WeatherFile, parseWeatherTemperatures) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_FLOAT_EQ(dut.get_temperature(), 16); + auto arguments = spy.takeFirst(); + ASSERT_DOUBLE_EQ(arguments.at(0).toDouble(), 16.7); + + ASSERT_FLOAT_EQ(dut.get_temperature(), 16.7); + ASSERT_DOUBLE_EQ(dut.get_weather(0)->get_temperature(), 16.7); + ASSERT_DOUBLE_EQ(dut.get_weather(0)->get_min_temperature(), 15.1); + ASSERT_DOUBLE_EQ(dut.get_weather(0)->get_max_temperature(), 18.4); } + /*****************************************************************************/ -TEST_F(WeatherFile, GetCityFromFile) { +TEST_F(WeatherFile, parseWeatherCity) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_city(), QString("Porto Alegre")); + auto arguments = spy.takeFirst(); + QString expected_city("Porto Alegre"); + ASSERT_EQ(arguments.at(0).toString(), expected_city); + ASSERT_EQ(dut.get_city(), expected_city); } /*****************************************************************************/ -TEST_F(WeatherFile, ParseConditionFromFile) { +TEST_F(WeatherFile, parseWeatherIconUrl) { Weather dut(cm); - QSignalSpy spy(&dut, SIGNAL(condition_changed(const QString&))); + QSignalSpy spy(&dut, SIGNAL(weather_info_updated())); dut.parse_weather(weatherFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_condition(), QString("few clouds")); + ASSERT_EQ( + dut.get_weather_icon_url(), QUrl(WEATHER_ICON_BASE_URL + "02d.png")); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, parseWeatherJsonInvalid) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); + + ASSERT_TRUE(spy.isValid()); + auto crap = QByteArray::fromStdString( + R"({This:"looks_like", "JSON":[], but:"is not!"})"); + dut.parse_weather(crap); + spy.wait(10); + EXPECT_EQ(spy.count(), 0); } /*****************************************************************************/ -TEST_F(WeatherFile, IconURI) { +TEST_F(WeatherFile, parseWeatherNoTemperature) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(temperature_changed(double))); - dut.parse_weather(weatherFile.readAll()); + ASSERT_TRUE(spy.isValid()); + auto no_temp = QByteArray::fromStdString(R"({ + "main": { + "XXXXX": 16, + "temp_min": 16, + "temp_max": 16 + }, + "dt": 1534251600, + "name": "Porto Alegre" + })"); + dut.parse_weather(no_temp); + // no signal should fire! spy.wait(10); - EXPECT_EQ(spy.count(), 1); - ASSERT_EQ( - dut.get_weather_icon_url(), QUrl(WEATHER_ICON_BASE_URL + "02d.png")); + EXPECT_EQ(spy.count(), 0); + // should not have changed + ASSERT_DOUBLE_EQ(dut.get_weather(0)->get_temperature(), 0); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, parseWeatherNoCity) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(city_updated(const QString&))); + ASSERT_TRUE(spy.isValid()); + // forecast does not have the correct format + dut.parse_weather(forecastFile.readAll()); + spy.wait(10); + EXPECT_EQ(spy.count(), 0); } /*****************************************************************************/ -TEST_F(WeatherFile, parseForecasts5) { +TEST_F(WeatherFile, parseForecastsDoesNotTouchCurrent) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(forecast_available())); - ASSERT_TRUE(spy.isValid()); dut.parse_forecast(forecastFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_forecasts().size(), 5); + ASSERT_DOUBLE_EQ(dut.get_weather(0)->get_temperature(), 0); + ASSERT_DOUBLE_EQ(dut.get_weather(1)->get_temperature(), 25.06); } /*****************************************************************************/ -TEST_F(WeatherFile, ForecastOutOfRange) { +TEST_F(WeatherFile, parseForecastBadJSON) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(forecast_available())); ASSERT_TRUE(spy.isValid()); + auto crap = QByteArray::fromStdString( + R"({This:"looks_like", "JSON":[], but:"is not!"})"); + dut.parse_forecast(crap); + spy.wait(10); + EXPECT_EQ(spy.count(), 0); + + auto empty_forecast = + QByteArray::fromStdString(R"({ "main": {}, "list": [] })"); + dut.parse_forecast(empty_forecast); + spy.wait(10); + EXPECT_EQ(spy.count(), 0); +} + +/*****************************************************************************/ +TEST_F(WeatherFile, ForecastOutOfRange) { + Weather dut(cm); + QSignalSpy spy(&dut, SIGNAL(forecast_available())); dut.parse_forecast(forecastFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_TRUE(std::isnan(dut.get_forecast_temperature(-1))); - ASSERT_TRUE(dut.get_forecast_icon_url(-1).isEmpty()); - ASSERT_FALSE(dut.get_forecast_timestamp(-1).isValid()); - - ASSERT_TRUE(std::isnan(dut.get_forecast_temperature(6))); - ASSERT_TRUE(dut.get_forecast_icon_url(6).isEmpty()); - ASSERT_FALSE(dut.get_forecast_timestamp(6).isValid()); + ASSERT_FALSE(dut.get_weather(-1)); + ASSERT_TRUE(dut.get_weather(1)); + ASSERT_FALSE(dut.get_weather(100)); } /*****************************************************************************/ TEST_F(WeatherFile, ForeCastOk) { Weather dut(cm); QSignalSpy spy(&dut, SIGNAL(forecast_available())); - ASSERT_TRUE(spy.isValid()); dut.parse_forecast(forecastFile.readAll()); spy.wait(10); EXPECT_EQ(spy.count(), 1); - ASSERT_EQ(dut.get_forecasts().size(), 5); - ASSERT_EQ( - dut.get_forecast_icon_url(2), QUrl(WEATHER_ICON_BASE_URL + "10n.png")); - ASSERT_DOUBLE_EQ(dut.get_forecast_temperature(2), 25.05); - ASSERT_EQ(dut.get_forecast_timestamp(2).toSecsSinceEpoch(), 1584813600); + ASSERT_EQ(dut.get_weather(2)->get_weather_icon_url(), + QUrl(WEATHER_ICON_BASE_URL + "10n.png")); + + ASSERT_DOUBLE_EQ(dut.get_weather(1)->get_temperature(), 25.06); + ASSERT_DOUBLE_EQ(dut.get_weather(1)->get_min_temperature(), 25.06); + ASSERT_DOUBLE_EQ(dut.get_weather(1)->get_max_temperature(), 26.16); ASSERT_EQ( - dut.get_forecast_icon_url(4), QUrl(WEATHER_ICON_BASE_URL + "10d.png")); - ASSERT_DOUBLE_EQ(dut.get_forecast_temperature(4), 26.51); - ASSERT_EQ(dut.get_forecast_timestamp(4).toSecsSinceEpoch(), 1584835200); + dut.get_weather(2)->get_timestamp().toSecsSinceEpoch(), 1584813600); +} +/*****************************************************************************/ +TEST(WeatherStatus, updateAcceptsBadJson) { + WeatherStatus dut; + ASSERT_NO_THROW(dut.update(QJsonObject())); + + auto empty_json = QByteArray::fromStdString(R"( { })"); + auto doc = QJsonDocument::fromJson(empty_json); + ASSERT_NO_THROW(dut.update(doc.object())); } /*****************************************************************************/ -TEST_F(WeatherFile, ForcastMalformattedJSON) { - Weather dut(cm); +TEST(WeatherStatus, validateBadJson) { auto crap = QByteArray::fromStdString( R"({This:"looks_like", "JSON":[], but:"is not!"})"); - dut.parse_forecast(crap); - ASSERT_EQ(dut.get_forecasts().size(), 0); + QJsonDocument doc = QJsonDocument::fromJson(crap); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); } /*****************************************************************************/ -TEST_F(WeatherFile, ForcastNoInfo) { - Weather dut(cm); - auto crap = QByteArray::fromStdString( - R"({"This":"is valid", "weather":[], "main":{"but":false}})"); - dut.parse_forecast(crap); - ASSERT_EQ(dut.get_forecasts().size(), 0); +TEST(WeatherStatus, validateBadJsonNoMain) { + auto crap_no_main = QByteArray::fromStdString( + R"( { "dt": 1584792000, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ] + })"); + auto doc = QJsonDocument::fromJson(crap_no_main); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); +} + +/*****************************************************************************/ +TEST(WeatherStatus, validateBadJsonEmptyWeather) { + auto crap_empty_weather = QByteArray::fromStdString( + R"( { "dt": 1584792000, + "main": { + "temp": 24.78, + "temp_min": 24.78, + "temp_max": 26.25 + }, + "weather": [] + })"); + QJsonParseError parse_err; + auto doc = QJsonDocument::fromJson(crap_empty_weather, &parse_err); + ASSERT_EQ(parse_err.error, QJsonParseError::NoError); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); +} + +/*****************************************************************************/ +TEST(WeatherStatus, validateBadJsonNoTimestamp) { + auto json_no_dt = QByteArray::fromStdString( + R"( { "XX": 1584792000, + "main": { + "temp": 24.78, + "temp_min": 24.78, + "temp_max": 26.25 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ] + })"); + QJsonParseError parse_err; + auto doc = QJsonDocument::fromJson(json_no_dt, &parse_err); + ASSERT_EQ(parse_err.error, QJsonParseError::NoError); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); +} + +/*****************************************************************************/ +TEST(WeatherStatus, validateBadJsonTempNotDouble) { + auto json_temp_not_double = QByteArray::fromStdString( + R"( { "dt": 1584792000, + "main": { + "temp": "Hallo", + "temp_min": 24.78, + "temp_max": 26.25 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ] + })"); + QJsonParseError parse_err; + auto doc = QJsonDocument::fromJson(json_temp_not_double, &parse_err); + ASSERT_EQ(parse_err.error, QJsonParseError::NoError); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); +} +/*****************************************************************************/ +TEST(WeatherStatus, validateBadJsonTempMinMissing) { + auto json_temp_min = QByteArray::fromStdString( + R"( { "dt": 1584792000, + "main": { + "temp": 15.9, + "temp_max": 26.25 + }, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ] + })"); + QJsonParseError parse_err; + auto doc = QJsonDocument::fromJson(json_temp_min, &parse_err); + ASSERT_EQ(parse_err.error, QJsonParseError::NoError); + EXPECT_THROW( + WeatherStatus::validate_json(doc.object()), std::runtime_error); } /*****************************************************************************/ TEST(WeatherCfg, fromJsonGood) { auto json_string = QString(R"( - { - "API-Key": "Secret", - "locationID": "ABCD", - "updateInterval": 123 - } - )"); + { + "API-Key": "Secret", + "locationID": "ABCD", + "updateInterval": 123 + } + )"); auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); auto dut = WeatherConfig::from_json_object(jdoc.object()); ASSERT_EQ(dut.get_api_token(), QString("Secret")); @@ -208,12 +377,12 @@ TEST(WeatherCfg, fromJsonGood) { /*****************************************************************************/ TEST(WeatherCfg, throwEmptyLocation) { auto json_string = QString(R"( - { - "API-Key": "Secret", - "locationID": "", - "updateInterval": 123 - } - )"); + { + "API-Key": "Secret", + "locationID": "", + "updateInterval": 123 + } + )"); auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); ASSERT_THROW( @@ -223,11 +392,11 @@ TEST(WeatherCfg, throwEmptyLocation) { /*****************************************************************************/ TEST(WeatherCfg, throwNoApiToken) { auto json_string = QString(R"( - { + { "locationID": "ABCD", - "updateInterval": 123 - } - )"); + "updateInterval": 123 + } + )"); auto jdoc = QJsonDocument::fromJson(json_string.toUtf8()); ASSERT_THROW( From d9cd2dd108dfdd528eee83929fdf8ce915fc9326 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Wed, 25 Mar 2020 20:41:17 +0100 Subject: [PATCH 19/26] Fix findings by codacity and coverity Coverity suspected a out of range error adding a int to char, not really but fixed anyway. Of course it is better to initialize members than to assign. Signed-off-by: Thomas Ruschival --- include/weather.hpp | 18 +++++++++--------- libsrc/weather.cpp | 9 +++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/include/weather.hpp b/include/weather.hpp index 72fcb17..e4185dc 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -1,10 +1,10 @@ /****************************************************************************** * \filename - * \brief Download weather information form openweathermaps + * \brief Download weather information form openweathermaps * * \details Periodically polls weather info * - * \copyright (c) 2018 Thomas Ruschival + * \copyright (c) 2019 Thomas Ruschival * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later * SPDX-License-Identifier: GPL-3.0-or-later} * @@ -50,12 +50,12 @@ class WeatherStatus : public QObject { * Cloning constructor - Q_OBJECT does not allow copy construction * @param other where to take the fields from */ - explicit WeatherStatus(const WeatherStatus* other) { - timestamp = other->timestamp; - icon_url = other->icon_url; - temp_max = other->temp_max; - temp_min = other->temp_min; - temperature = other->temperature; + explicit WeatherStatus(const WeatherStatus* other) + : temperature(other->temperature) + , temp_min(other->temp_min) + , temp_max(other->temp_max) + , icon_url(other->icon_url) + , timestamp(other->timestamp) { } /** @@ -292,7 +292,7 @@ public slots: /** * Static array of Weatherstatus - * no + 24*3h forecasts should be enough + * now + WEATHER_FORECAST_COUNT*3h forecasts should be enough */ std::array weather; diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index fa0dc23..1ed11b2 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -1,10 +1,10 @@ /****************************************************************************** * \filename - * \brief parse weather information form openweathermaps + * \brief manage information form openweathermaps * * \details * - * \copyright (c) 2018 Thomas Ruschival + * \copyright (c) 2019 Thomas Ruschival * \license {This file is licensed under GNU PUBLIC LICENSE Version 3 or later * SPDX-License-Identifier: GPL-3.0-or-later} * @@ -171,8 +171,9 @@ QUrl DigitalRooster::create_forecast_url(const WeatherConfig& cfg) { request_str += "&units=metric"; request_str += "&appid="; request_str += cfg.get_api_token(); - request_str += "&lang=en"; // default english - request_str += "&cnt=" + WEATHER_FORECAST_COUNT; // + request_str += "&lang=en"; // default english + request_str += "&cnt="; + request_str += QString(WEATHER_FORECAST_COUNT); // qCDebug(CLASS_LC) << request_str; return QUrl(request_str); } From 467472c15df2ea5d7a6332e9f7a1a4d64610a239 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Thu, 26 Mar 2020 18:37:15 +0100 Subject: [PATCH 20/26] Fix yesterdays quickfix QString += int doesnt work nicely with the API Signed-off-by: Thomas Ruschival --- libsrc/httpclient.cpp | 2 +- libsrc/weather.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libsrc/httpclient.cpp b/libsrc/httpclient.cpp index ec8d35e..311bb1c 100644 --- a/libsrc/httpclient.cpp +++ b/libsrc/httpclient.cpp @@ -66,7 +66,7 @@ void HttpClient::downloadFinished(QNetworkReply* reply) { QUrl url = reply->url(); if (reply->error()) { qCCritical(CLASS_LC) - << "Download of %s failed" << url.toEncoded().constData() + << "Download failed" << url.toEncoded().constData() << qPrintable(reply->errorString()); } else { if (isHttpRedirect(reply)) { diff --git a/libsrc/weather.cpp b/libsrc/weather.cpp index 1ed11b2..5245f64 100644 --- a/libsrc/weather.cpp +++ b/libsrc/weather.cpp @@ -173,7 +173,7 @@ QUrl DigitalRooster::create_forecast_url(const WeatherConfig& cfg) { request_str += cfg.get_api_token(); request_str += "&lang=en"; // default english request_str += "&cnt="; - request_str += QString(WEATHER_FORECAST_COUNT); // + request_str += QString::number(WEATHER_FORECAST_COUNT); // qCDebug(CLASS_LC) << request_str; return QUrl(request_str); } From 2614ce9ee95c26691279a1ef23eb903ba447b012 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Thu, 26 Mar 2020 18:39:22 +0100 Subject: [PATCH 21/26] Fix: ioctl on absolute device path never worked getting the name of the event devices because I used only the file name not the path.... Signed-off-by: Thomas Ruschival --- include/hwif/hardware_configuration.hpp | 4 ++-- libhwif/hardware_configuration.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/hwif/hardware_configuration.hpp b/include/hwif/hardware_configuration.hpp index 9ead1f0..c82573e 100644 --- a/include/hwif/hardware_configuration.hpp +++ b/include/hwif/hardware_configuration.hpp @@ -103,9 +103,9 @@ class HardwareConfiguration { /** * Input device name of rotary button (must match Device Tree) - * Default = "push_btn" + * Default = "gpio_keys" */ - QString dev_push_button_event_name{"push_button"}; + QString dev_push_button_event_name{"gpio_keys"}; }; } /* namespace Hal */ diff --git a/libhwif/hardware_configuration.cpp b/libhwif/hardware_configuration.cpp index 1b1a79d..c7455e0 100644 --- a/libhwif/hardware_configuration.cpp +++ b/libhwif/hardware_configuration.cpp @@ -69,8 +69,8 @@ QString HardwareConfiguration::resolve_name_to_path( qCDebug(CLASS_LC) << Q_FUNC_INFO << device_name; QDir evt_dir = QDir("/dev/input/"); for (auto& file_name : evt_dir.entryList(QDir::System)) { - QFile f(file_name); - qCDebug(CLASS_LC) << "trying " << file_name; + qCDebug(CLASS_LC) << "trying " << evt_dir.absoluteFilePath(file_name); + QFile f(evt_dir.absoluteFilePath(file_name)); if (!f.open(QFile::ReadWrite)) { qCCritical(CLASS_LC) << "Error: open file " << f.fileName() << f.errorString(); From c883d0d268bd5e007f6ff85d2568cd846b99c561 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 29 Mar 2020 11:49:19 +0200 Subject: [PATCH 22/26] WeatherForecast: only one temperature There is no use in providing min/max temperatures if the API does give the same value for min/max temperature forecasts more than 6h in the future Signed-off-by: Thomas Ruschival --- include/weather.hpp | 2 +- qtgui/qml/ForecastWidget.qml | 33 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/include/weather.hpp b/include/weather.hpp index e4185dc..fe31c82 100644 --- a/include/weather.hpp +++ b/include/weather.hpp @@ -150,7 +150,7 @@ class Weather : public QObject { Q_OBJECT Q_PROPERTY(QString city READ get_city NOTIFY city_updated) Q_PROPERTY( - float temperature READ get_temperature NOTIFY temperature_changed) + float temp READ get_temperature NOTIFY temperature_changed) Q_PROPERTY(QUrl weatherIcon READ get_weather_icon_url NOTIFY icon_changed) public: /** diff --git a/qtgui/qml/ForecastWidget.qml b/qtgui/qml/ForecastWidget.qml index 105c2c2..c763938 100644 --- a/qtgui/qml/ForecastWidget.qml +++ b/qtgui/qml/ForecastWidget.qml @@ -24,43 +24,42 @@ Rectangle { anchors.topMargin: 0; Text{ - id: timestamp; - text: "16:00"; - font: Style.font.label; + id: temp; + text: "-12\u00B0C" + font: Style.font.weatherInfo; color: "white"; - Layout.leftMargin: 3; + style: Text.Outline; + styleColor: Style.colors.selected; Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; + z:1; //cover icon if needed } Image { id: condition_icon; fillMode: Image.PreserveAspectFit - - Layout.margins: -10; - Layout.minimumHeight: 64; - Layout.preferredHeight: 72; - Layout.maximumHeight: 72; - Layout.fillWidth: true; + Layout.margins: -8; + Layout.minimumWidth: 50; + Layout.preferredWidth: 60; + Layout.maximumWidth: 70; + Layout.fillHeight: true; Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter; } Text{ - id: temps; - text: "-20 / 12\u00B0C"; - font: Style.font.weatherInfo; + id: timestamp; + text: "23:55"; + font: Style.font.weatherTime; color: "white" - Layout.topMargin: -5; + Layout.topMargin: -12; Layout.columnSpan: 2; Layout.alignment: Qt.AlignHCenter | Qt.AlignTop; } - } function update(weather){ console.log("update: " + weather.timestamp); timestamp.text = Qt.formatTime(weather.timestamp,"hh:mm"); - temps.text = Math.round(weather.temp_min)+" | "+ - Math.round(weather.temp_max)+"\u00B0C"; + temp.text = Math.round(weather.temp)+"\u00B0C"; condition_icon.source = weather.icon_url; } From edb8ddc84951a940fc55511dca0c467bd1de5673 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 29 Mar 2020 11:51:15 +0200 Subject: [PATCH 23/26] Minor Layout/Style fixes - Add alarm button should not cover delete alarm button... Signed-off-by: Thomas Ruschival --- qtgui/qml/AlarmList.qml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/qtgui/qml/AlarmList.qml b/qtgui/qml/AlarmList.qml index c66423e..bb3cea8 100644 --- a/qtgui/qml/AlarmList.qml +++ b/qtgui/qml/AlarmList.qml @@ -5,7 +5,7 @@ import ruschi.Alarm 1.0 ListView { id:alarmlist - property string objectName : "Alarms" + property string objectName : "Alarms" width: stackView.width height: stackView.height antialiasing: true @@ -21,22 +21,22 @@ ListView { id: alarmdelegate } - AlarmEditDialog{ - id: alarmEditDlg; - width: Style.contentWidth*0.8; - x: Math.round((applicationWindow.width - width)/2) - bottomMargin:15; - } + AlarmEditDialog{ + id: alarmEditDlg; + width: Style.contentWidth*0.8; + x: Math.round((applicationWindow.width - width)/2) + bottomMargin:15; + } model: alarmlistmodel RoundButton { text: qsTr("+") highlighted: true - width: 56; - height: 56; - anchors.margins: 10 - anchors.right: parent.right - anchors.bottom: parent.bottom + width: 56; + height: 56; + anchors.margins: 10; + anchors.left: parent.left; + anchors.bottom: parent.bottom; onClicked: { alarmEditDlg.currentAlarm = alarmlistmodel.create_alarm(); alarmlist.currentIndex = alarmlistmodel.rowCount()-1; From c9814f8d3896a0693901a9cb03d1b27825c08904 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 29 Mar 2020 11:53:04 +0200 Subject: [PATCH 24/26] Minor Layout/Style fixes (fonts) Signed-off-by: Thomas Ruschival --- qtgui/qml/AlarmDelegate.qml | 116 +++++++------- qtgui/qml/PlayerControlWidget.qml | 256 +++++++++++++++--------------- qtgui/qml/Style.qml | 51 +++--- 3 files changed, 218 insertions(+), 205 deletions(-) diff --git a/qtgui/qml/AlarmDelegate.qml b/qtgui/qml/AlarmDelegate.qml index c182c5e..9913a6f 100644 --- a/qtgui/qml/AlarmDelegate.qml +++ b/qtgui/qml/AlarmDelegate.qml @@ -8,74 +8,74 @@ import "Jsutil.js" as Util Rectangle{ id: alarmDelegate - width: parent.width; + width: parent.width; height: Style.contentHeight/3; radius: 3; - border.width: 1; - color: alarmEnabled ? - Style.colors.enabled : Style.colors.disabled; + border.width: 1; + color: alarmEnabled ? + Style.colors.enabled : Style.colors.disabled; - MouseArea { - anchors.fill: parent - onPressAndHold: { - alarmlistmodel.currentIndex =index; - console.log("Alarm pressed : "+index); - alarmEditDlg.index = index; - alarmEditDlg.currentAlarm = alarmlistmodel.get_alarm( - alarmlistmodel.currentIndex) - alarmEditDlg.open(); - } - } + MouseArea { + anchors.fill: parent + onPressAndHold: { + alarmlistmodel.currentIndex =index; + console.log("Alarm pressed : "+index); + alarmEditDlg.index = index; + alarmEditDlg.currentAlarm = alarmlistmodel.get_alarm( + alarmlistmodel.currentIndex) + alarmEditDlg.open(); + } + } - RowLayout{ - anchors.fill: parent - anchors.margins: Style.itemMargins.slim; - spacing: Style.itemSpacings.medium; + RowLayout{ + anchors.fill: parent + anchors.margins: Style.itemMargins.slim; + spacing: Style.itemSpacings.medium; - Text { - id: periodicityString; - text: periodstring; - Layout.fillWidth: true; - font: Style.font.label; - } + Text { + id: periodicityString; + text: periodstring; + Layout.fillWidth: true; + font: Style.font.listItemHead; + } - Text { - id: alarmtime; - font: Style.font.boldLabel; - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter; - text: Qt.formatTime(triggerTime, "hh:mm") - } + Text { + id: alarmtime; + font: Style.font.listItemHead; + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter; + text: Qt.formatTime(triggerTime, "hh:mm") + } - Switch{ - id: enaAlarm; + Switch{ + id: enaAlarm; - position: alarmEnabled; - text: alarmEnabled ? qsTr("on") : qsTr("off") + position: alarmEnabled; + text: alarmEnabled ? qsTr("on") : qsTr("off") - onCheckedChanged:{ - alarmlistmodel.set_enabled(index, position) - } - } + onCheckedChanged:{ + alarmlistmodel.set_enabled(index, position) + } + } - DelayButton{ - Layout.minimumHeight: 56 - Layout.minimumWidth: 56 - Layout.maximumHeight: 56 - Layout.maximumWidth: 56 - delay:1000; + DelayButton{ + Layout.minimumHeight: 56 + Layout.minimumWidth: 56 + Layout.maximumHeight: 56 + Layout.maximumWidth: 56 + delay:1000; - contentItem: Text{ - text: "\ufa79" - color: "white" - font.pointSize: 24 - horizontalAlignment: Text.AlignHCenter - font.family: "Material Design Icons" - } + contentItem: Text{ + text: "\ufa79" + color: "white" + font.pointSize: 24 + horizontalAlignment: Text.AlignHCenter + font.family: "Material Design Icons" + } - onActivated:{ - console.log("Deleting idx: " + index) - alarmlistmodel.delete_alarm(index); - } - } - } + onActivated:{ + console.log("Deleting idx: " + index) + alarmlistmodel.delete_alarm(index); + } + } + } } diff --git a/qtgui/qml/PlayerControlWidget.qml b/qtgui/qml/PlayerControlWidget.qml index 175560c..d835cea 100644 --- a/qtgui/qml/PlayerControlWidget.qml +++ b/qtgui/qml/PlayerControlWidget.qml @@ -8,17 +8,17 @@ import ruschi.PodcastEpisode 1.0 import "Jsutil.js" as Util Popup { - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - background: Rectangle { + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + background: Rectangle { color: "#3F51B5"; } - enter: Transition { - NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 ; duration: 300} - } - exit: Transition { - NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 ; duration: 400} - } + enter: Transition { + NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 ; duration: 300} + } + exit: Transition { + NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 ; duration: 400} + } Timer { id: interactiontimer @@ -28,128 +28,126 @@ Popup { onTriggered: playerControlWidget.close(); } - GridLayout{ - columns: 3; - rows: 3; - columnSpacing: Style.itemSpacings.medium; - rowSpacing: Style.itemSpacings.dense; - anchors.margins: 0; - Layout.topMargin: 0; - anchors.bottomMargin: 12; - anchors.fill: parent; - - Text{ - id: currentMediaTitle - text: "" ; - font: Style.font.label; - color: Material.accent; - elide: Text.ElideRight; - - Layout.topMargin: 0; - Layout.columnSpan: 3; - Layout.fillWidth: true; - Layout.alignment: Qt.AlignCenter| Qt.AlignTop - } - - IconButton { - id: backwardBtn - Layout.alignment: Qt.AlignRight| Qt.AlignTop - Layout.minimumWidth: parent.width/3 - 20; - Layout.preferredWidth: parent.width/3 ; - text: "\uf45f" - onClicked: { - interactiontimer.restart() - playerProxy.seek(-10000) - } - } - - IconButton { - id: playBtn - Layout.alignment: Qt.AlignCenter| Qt.AlignTop - Layout.fillWidth: true; - text: "\uf40a" // default to play icon - - onClicked: { - interactiontimer.restart() - if (playerProxy.playbackState == MediaPlayer.PlayingState) { - playerProxy.pause() - } else { - playerProxy.play() - } - } - - function switchPlayButtonIcon(playbackState) { - switch (playbackState) { - case MediaPlayer.PlayingState: - playBtn.text = "\uf3e4" - break - case MediaPlayer.PausedState: - playBtn.text = "\uf40a" - break - case MediaPlayer.StoppedState: - playBtn.text = "\uf40a" - break - default: - console.log("player???") - } - } - } - - IconButton { - id: forwardBtn - Layout.alignment: Qt.AlignLeft| Qt.AlignTop - Layout.minimumWidth: parent.width/3 - 20; - Layout.preferredWidth: parent.width/3 ; - - text: "\uf211" - onClicked: { - interactiontimer.restart() - playerProxy.seek(10000) - } - } - //Row 3 - RowLayout{ - Layout.columnSpan: 3; - Layout.fillWidth: true; - Layout.alignment: Qt.AlignCenter| Qt.AlignTop - Layout.bottomMargin: 10; - Layout.topMargin: Style.itemMargins.slim; - - Text { - id: timeElapsed - text: Util.display_time_ms(playerProxy.position) - font: Style.font.valueLabel; - Layout.alignment: Qt.AlignRight | Qt.AlignTop - color: "white" - } - - Slider { - id: slider - Layout.fillWidth: true; - Layout.alignment: Qt.AlignHCenter|Qt.AlignTop - Layout.topMargin: -10; - enabled: playerProxy.seekable - - onMoved: { - playerProxy.set_position(value * playerProxy.duration) - interactiontimer.restart() - } - } - Text { - id: durationTotal - text: playerProxy.seekable? Util.display_time_ms(playerProxy.duration): "\u221E" - Layout.preferredWidth: timeElapsed.width - font: Style.font.valueLabel; - color: "white" - Layout.alignment: Qt.AlignLeft | Qt.AlignTop - } - }// RowLayout Row 3 - }//Gridlayout + GridLayout{ + columns: 3; + rows: 3; + columnSpacing: Style.itemSpacings.medium; + rowSpacing: Style.itemSpacings.dense; + anchors.margins: 0; + anchors.fill: parent; + + Text{ + id: currentMediaTitle + text: "" ; + font: Style.font.label; + color: Material.accent; + elide: Text.ElideRight; + + Layout.topMargin: 0; + Layout.columnSpan: 3; + Layout.fillWidth: true; + Layout.alignment: Qt.AlignCenter| Qt.AlignTop + } + + IconButton { + id: backwardBtn + Layout.alignment: Qt.AlignRight| Qt.AlignTop + Layout.minimumWidth: parent.width/3 - 20; + Layout.preferredWidth: parent.width/3 ; + text: "\uf45f" + onClicked: { + interactiontimer.restart() + playerProxy.seek(-10000) + } + } + + IconButton { + id: playBtn + Layout.alignment: Qt.AlignCenter| Qt.AlignTop + Layout.fillWidth: true; + text: "\uf40a" // default to play icon + + onClicked: { + interactiontimer.restart() + if (playerProxy.playbackState == MediaPlayer.PlayingState) { + playerProxy.pause() + } else { + playerProxy.play() + } + } + + function switchPlayButtonIcon(playbackState) { + switch (playbackState) { + case MediaPlayer.PlayingState: + playBtn.text = "\uf3e4" + break + case MediaPlayer.PausedState: + playBtn.text = "\uf40a" + break + case MediaPlayer.StoppedState: + playBtn.text = "\uf40a" + break + default: + console.log("player???") + } + } + } + + IconButton { + id: forwardBtn + Layout.alignment: Qt.AlignLeft| Qt.AlignTop + Layout.minimumWidth: parent.width/3 - 20; + Layout.preferredWidth: parent.width/3 ; + + text: "\uf211" + onClicked: { + interactiontimer.restart() + playerProxy.seek(10000) + } + } + //Row 3 + RowLayout{ + Layout.columnSpan: 3; + Layout.fillWidth: true; + Layout.alignment: Qt.AlignCenter| Qt.AlignTop + Layout.bottomMargin: 5; + Layout.topMargin: Style.itemMargins.slim; + + Text { + id: timeElapsed + text: Util.display_time_ms(playerProxy.position) + font: Style.font.valueLabel; + Layout.alignment: Qt.AlignRight | Qt.AlignTop + color: "white" + } + + Slider { + id: slider + Layout.fillWidth: true; + Layout.alignment: Qt.AlignHCenter|Qt.AlignTop + Layout.topMargin: -10; + enabled: playerProxy.seekable + + onMoved: { + playerProxy.set_position(value * playerProxy.duration) + interactiontimer.restart() + } + } + Text { + id: durationTotal + text: playerProxy.seekable? Util.display_time_ms(playerProxy.duration): "\u221E" + Layout.preferredWidth: timeElapsed.width + font: playerProxy.seekable? Style.font.valueLabel: Style.font.listItemHeadListened; + color: "white" + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + } + }// RowLayout Row 3 + }//Gridlayout /***********************************************************************/ function show(visible) { - interactiontimer.start(); - playerControlWidget.open(); + interactiontimer.start(); + playerControlWidget.open(); } function updatePosition(pos) { @@ -157,8 +155,8 @@ Popup { timeElapsed.text = Util.display_time_ms(pos) } - function setCurrentMediaTitle(title) { - currentMediaTitle.text = title; + function setCurrentMediaTitle(title) { + currentMediaTitle.text = title; } /***********************************************************************/ Component.onCompleted: { diff --git a/qtgui/qml/Style.qml b/qtgui/qml/Style.qml index 266f429..e34460d 100644 --- a/qtgui/qml/Style.qml +++ b/qtgui/qml/Style.qml @@ -30,94 +30,109 @@ QtObject { property font title: Qt.font({ weight: Font.Normal, - pixelSize: scaleFont(18) + pixelSize: scaleFont(18), + preferShaping: false }); property font titleBold: Qt.font({ weight: Font.Bold, - pixelSize: scaleFont(18) + pixelSize: scaleFont(18), + preferShaping: false }); property font subtitle: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.DemiBold, - pixelSize: scaleFont(14) + pixelSize: scaleFont(14), + preferShaping: false }); property font weatherInfo: Qt.font({ family: "DejaVu Sans Condensed", - weight: Font.DemiBold, - pixelSize: scaleFont(12) + weight: Font.Bold, + pixelSize: scaleFont(14), + preferShaping: false }); property font weatherTime: Qt.font({ - family: "DejaVu Sans Condensed", - weight: Font.Normal, - pixelSize: scaleFont(12) + family: "DejaVu Sans", + weight: Font.DemiBold, + pixelSize: scaleFont(12), + preferShaping: false }); property font listItemHead: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.DemiBold, letterSpacing:-1, - pixelSize: scaleFont(12) + pixelSize: scaleFont(12), + preferShaping: false }); property font listItemHeadListened: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.Normal, letterSpacing:-1, - pixelSize: scaleFont(12) + pixelSize: scaleFont(12), + preferShaping: false }); property font label: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.DemiBold, letterSpacing:-1, - pixelSize: scaleFont(10) + pixelSize: scaleFont(10), + preferShaping: false }); property font boldLabel: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.Bold, letterSpacing:-1, - pixelSize: scaleFont(10) + pixelSize: scaleFont(10), + preferShaping: false }); property font valueLabel: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.Normal, letterSpacing:-1, - pixelSize: scaleFont(10) + pixelSize: scaleFont(10), + preferShaping: false }); property font flowText: Qt.font({ family: "DejaVu Sans Condensed", weight: Font.Normal, letterSpacing:-1, - pixelSize: scaleFont(10) + pixelSize: scaleFont(10), + preferShaping: false }); // -- elements property font clock: Qt.font({ weight: Font.Bold, - pixelSize: scaleFont(64) + pixelSize: scaleFont(64), + preferShaping: false }) property font tumbler: Qt.font({ weight: Font.Bold, - pixelSize: scaleFont(16) + pixelSize: scaleFont(16), + preferShaping: false }); property font button: Qt.font({ family: "Material Design Icons", weight: Font.DemiBold, - pixelSize: scaleFont(18) + pixelSize: scaleFont(20), + preferShaping: false }); property font sliderValue: Qt.font({ weight: Font.DemiBold, - pixelSize: scaleFont(16) + pixelSize: scaleFont(16), + preferShaping: false }); }// font From 718328e946ca02096cbd2a8c7bf57555c3c4fd17 Mon Sep 17 00:00:00 2001 From: Thomas Ruschival Date: Sun, 29 Mar 2020 11:53:31 +0200 Subject: [PATCH 25/26] AlarmEditDialog: Add Ok/Cancel buttons Signed-off-by: Thomas Ruschival --- qtgui/qml/AlarmEditDialog.qml | 246 ++++++++++++++++++---------------- 1 file changed, 130 insertions(+), 116 deletions(-) diff --git a/qtgui/qml/AlarmEditDialog.qml b/qtgui/qml/AlarmEditDialog.qml index 046e379..848216b 100644 --- a/qtgui/qml/AlarmEditDialog.qml +++ b/qtgui/qml/AlarmEditDialog.qml @@ -8,122 +8,136 @@ import "Jsutil.js" as Util Popup { - property Alarm currentAlarm; - property int index; - focus: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - enter: Transition { - NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 ; duration: 300} - } - exit: Transition { - NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 ; duration: 400} - } + property Alarm currentAlarm; + property int index; + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + enter: Transition { + NumberAnimation { property: "opacity"; from: 0.0; to: 1.0 ; duration: 300} + } + exit: Transition { + NumberAnimation { property: "opacity"; from: 1.0; to: 0.0 ; duration: 400} + } contentItem: GridLayout { - columnSpacing: Style.itemSpacings.medium; - rowSpacing: Style.itemSpacings.medium; - anchors.fill: parent; - anchors.margins: Style.itemMargins.slim; - rows: 3; - columns:2; - - Tumbler{ - id: timeTumbler - Layout.maximumHeight: 100 - Layout.rowSpan: 2 - Layout.alignment: Qt.AlignLeft| Qt.AlignTop - - TumblerColumn { - id: hoursTumbler - model: 24 - width: 46; - delegate: Text { - text: styleData.value - font: Style.font.tumbler; - horizontalAlignment: Text.AlignHCenter - opacity: 0.4 + Math.max(0, 1 - Math.abs(styleData.displacement)) * 0.6 - } - } - TumblerColumn { - id: minutesTumbler - model: 60 - width: 46; - delegate: Text { - text: styleData.value - font: Style.font.tumbler; - horizontalAlignment: Text.AlignHCenter - opacity: 0.4 + Math.max(0, 1 - Math.abs(styleData.displacement)) * 0.6 - } - } - } - //---------- - - Switch{ - id: enaAlarm; - Layout.alignment: Qt.AlignLeft| Qt.AlignTop - position: currentAlarm.enabled - text: currentAlarm.enabled ? qsTr("on") : qsTr("off") - - onCheckedChanged:{ - currentAlarm.enabled= position; - } - } - - ComboBox { - id: period - Layout.alignment: Qt.AlignLeft| Qt.AlignTop - model: ListModel { - id: model - ListElement { text: qsTr("Once") } - ListElement { text: qsTr("Daily") } - ListElement { text: qsTr("Weekend") } - ListElement { text: qsTr("Workdays") } - } - currentIndex: currentAlarm.period_id; - - onActivated: { - console.log("new period:" + currentIndex); - currentAlarm.period_id = currentIndex; - } - } - - ComboBox { - id: stations - Layout.preferredWidth: parent.width - Layout.alignment: Qt.AlignLeft| Qt.AlignTop - Layout.columnSpan: 2 - Layout.bottomMargin: Style.itemMargins.slim; - - model: iradiolistmodel - textRole: "station_name"; - - onActivated: { - currentAlarm.url = iradiolistmodel.get_station_url(currentIndex); - } - } - } // Gridlayout - - onAboutToShow : { - timeTumbler.setCurrentIndexAt(0,Util.get_hours(currentAlarm.time)) - timeTumbler.setCurrentIndexAt(1,Util.get_minutes(currentAlarm.time)) - - for (var i=0; i Date: Sun, 29 Mar 2020 15:29:23 +0200 Subject: [PATCH 26/26] Release 0.9.0 bump version Signed-off-by: Thomas Ruschival --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 531affd..aec3ee5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/CMakeModules/) # Project Name MESSAGE( STATUS "Running ${CMAKE_COMMAND} v${CMAKE_VERSION}" ) PROJECT(DigitalRooster - VERSION 0.8.0 + VERSION 0.9.0 DESCRIPTION "A digital alarm clock and podcast player" LANGUAGES CXX C )