diff --git a/SUPLA.xcodeproj/project.pbxproj b/SUPLA.xcodeproj/project.pbxproj index c83095ec..e222daad 100644 --- a/SUPLA.xcodeproj/project.pbxproj +++ b/SUPLA.xcodeproj/project.pbxproj @@ -710,6 +710,12 @@ A58A630B2C071ACB00A9D02D /* VerticalBlindsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A630A2C071ACB00A9D02D /* VerticalBlindsVC.swift */; }; A58A630E2C071B0600A9D02D /* VerticalBlindsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A630D2C071B0600A9D02D /* VerticalBlindsView.swift */; }; A58A63132C09B56000A9D02D /* VerticalBlindsVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63122C09B56000A9D02D /* VerticalBlindsVMTests.swift */; }; + A58A63172C09BFAF00A9D02D /* GarageDoorVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63162C09BFAF00A9D02D /* GarageDoorVM.swift */; }; + A58A63192C09C03500A9D02D /* GarageDoorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63182C09C03500A9D02D /* GarageDoorState.swift */; }; + A58A631B2C09C09100A9D02D /* GarageDoorVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A631A2C09C09100A9D02D /* GarageDoorVC.swift */; }; + A58A631D2C09C0D600A9D02D /* GarageDoorColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A631C2C09C0D600A9D02D /* GarageDoorColors.swift */; }; + A58A631F2C09C0F200A9D02D /* GarageDoorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A631E2C09C0F200A9D02D /* GarageDoorView.swift */; }; + A58A63222C09E6C400A9D02D /* GarageDoorVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A63212C09E6C400A9D02D /* GarageDoorVMTests.swift */; }; A58A9BFA2A9F77BB00D28848 /* BaseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A9BF92A9F77BB00D28848 /* BaseCell.swift */; }; A58A9BFE2AA0A4BD00D28848 /* UIView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A9BFD2AA0A4BD00D28848 /* UIView+Ext.swift */; }; A58A9C012AA0AD0E00D28848 /* ThermostatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58A9C002AA0AD0E00D28848 /* ThermostatCell.swift */; }; @@ -2045,6 +2051,12 @@ A58A630A2C071ACB00A9D02D /* VerticalBlindsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalBlindsVC.swift; sourceTree = ""; }; A58A630D2C071B0600A9D02D /* VerticalBlindsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalBlindsView.swift; sourceTree = ""; }; A58A63122C09B56000A9D02D /* VerticalBlindsVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalBlindsVMTests.swift; sourceTree = ""; }; + A58A63162C09BFAF00A9D02D /* GarageDoorVM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorVM.swift; sourceTree = ""; }; + A58A63182C09C03500A9D02D /* GarageDoorState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorState.swift; sourceTree = ""; }; + A58A631A2C09C09100A9D02D /* GarageDoorVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorVC.swift; sourceTree = ""; }; + A58A631C2C09C0D600A9D02D /* GarageDoorColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorColors.swift; sourceTree = ""; }; + A58A631E2C09C0F200A9D02D /* GarageDoorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorView.swift; sourceTree = ""; }; + A58A63212C09E6C400A9D02D /* GarageDoorVMTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GarageDoorVMTests.swift; sourceTree = ""; }; A58A9BF92A9F77BB00D28848 /* BaseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCell.swift; sourceTree = ""; }; A58A9BFD2AA0A4BD00D28848 /* UIView+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Ext.swift"; sourceTree = ""; }; A58A9C002AA0AD0E00D28848 /* ThermostatCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermostatCell.swift; sourceTree = ""; }; @@ -3100,6 +3112,7 @@ A5074BBB2BCE5CBF0081B6B1 /* UI */ = { isa = PBXGroup; children = ( + A58A63142C09BCCE00A9D02D /* GarageDoor */, A58A630C2C071AF400A9D02D /* VerticalBlind */, A5AD70192C00B1C300A36318 /* Controls */, A50E5D782BFF551B00303BAE /* Curtain */, @@ -3216,6 +3229,7 @@ A50B5D102BECC58800918D18 /* WindowDetail */ = { isa = PBXGroup; children = ( + A58A63202C09E6A000A9D02D /* GarageDoor */, A58A63112C09B54B00A9D02D /* VerticalBlind */, A5AD702A2C04771500A36318 /* Curtain */, A50B5D312BF54FE600918D18 /* TerraceAwning */, @@ -3957,6 +3971,7 @@ A55A8D6C2BA831AD00C540D4 /* WindowDetail */ = { isa = PBXGroup; children = ( + A58A63152C09BCE600A9D02D /* GarageDoor */, A58A63032C07154700A9D02D /* VerticalBlind */, A50E5D712BFF540700303BAE /* Curtain */, A50B5D342BFB6BDB00918D18 /* ProjectorScreen */, @@ -4032,6 +4047,7 @@ A58A63082C07168400A9D02D /* VerticalBlindMarker.swift */, A50B5D2C2BF49F7700918D18 /* TerraceAwningWindowState.swift */, A50B5D372BFB6D1400918D18 /* ProjectorScreenState.swift */, + A58A63182C09C03500A9D02D /* GarageDoorState.swift */, A50E5D762BFF548200303BAE /* CurtainWindowState.swift */, ); path = Model; @@ -4368,6 +4384,32 @@ path = VerticalBlind; sourceTree = ""; }; + A58A63142C09BCCE00A9D02D /* GarageDoor */ = { + isa = PBXGroup; + children = ( + A58A631C2C09C0D600A9D02D /* GarageDoorColors.swift */, + A58A631E2C09C0F200A9D02D /* GarageDoorView.swift */, + ); + path = GarageDoor; + sourceTree = ""; + }; + A58A63152C09BCE600A9D02D /* GarageDoor */ = { + isa = PBXGroup; + children = ( + A58A63162C09BFAF00A9D02D /* GarageDoorVM.swift */, + A58A631A2C09C09100A9D02D /* GarageDoorVC.swift */, + ); + path = GarageDoor; + sourceTree = ""; + }; + A58A63202C09E6A000A9D02D /* GarageDoor */ = { + isa = PBXGroup; + children = ( + A58A63212C09E6C400A9D02D /* GarageDoorVMTests.swift */, + ); + path = GarageDoor; + sourceTree = ""; + }; A58A9BF82A9F77A400D28848 /* Cells */ = { isa = PBXGroup; children = ( @@ -5967,6 +6009,7 @@ A5F29BC22A24E30100ED700A /* ChangeChannelsVisibilityUseCase.swift in Sources */, A57C4AB72AAF4F7600D9C695 /* FilteredTapGestureDelegate.swift in Sources */, AE7452912827F49100A3AFAD /* ProfilesNavigationCoordinator.swift in Sources */, + A58A631F2C09C0F200A9D02D /* GarageDoorView.swift in Sources */, A55A8DAF2BAC64A700C540D4 /* SADialogTitleLabel.swift in Sources */, 0172677D234FAF78000F1CFB /* SADownloadImpulseCounterMeasurements.m in Sources */, A56233FF2ABAC488001CB948 /* ThermostatIconNameProducer.swift in Sources */, @@ -6088,6 +6131,7 @@ A5074BE42BD251590081B6B1 /* ExecuteFacadeBlindActionUseCase.swift in Sources */, A51BE9032AA746C100718F2F /* RoundedControlButtonView.swift in Sources */, 401CA1C21BA067DB00117AF4 /* AppDelegate.m in Sources */, + A58A63192C09C03500A9D02D /* GarageDoorState.swift in Sources */, A5E490632A4019E6006801FE /* UpdateGroupIconRelationsUseCase.swift in Sources */, A5F29BAE2A24BA5800ED700A /* LegacyWrapper.swift in Sources */, A5F29B9A2A20C26900ED700A /* BaseTableViewController.swift in Sources */, @@ -6151,6 +6195,7 @@ A5F29B9D2A20D2F300ED700A /* CreateProfileGroupsListUseCase.swift in Sources */, A566398D2ABADFD700BA51D7 /* LoadingScrimView.swift in Sources */, 40B9ED3A1FAC79FC00403B1A /* SUPLA.xcdatamodeld in Sources */, + A58A63172C09BFAF00A9D02D /* GarageDoorVM.swift in Sources */, 40B610CC20C7E263002B762A /* SAUIChannelStatus.m in Sources */, AECB1BE727109EA3001A9714 /* CheckBox.swift in Sources */, A5A14A3D2B6134C4004B1598 /* InsertChannelConfigUseCase.swift in Sources */, @@ -6229,6 +6274,7 @@ A5B3A4B52BB558B70001D006 /* RoofWindowView.swift in Sources */, A5AE7A7F2A3998260097FA8B /* NewGestureInfoView.swift in Sources */, A57777CD29E6907C004513E6 /* RuntimeConfig.swift in Sources */, + A58A631B2C09C09100A9D02D /* GarageDoorVC.swift in Sources */, A57668DD2AE995590025509D /* GeneralError.swift in Sources */, A55A8D882BAAFFE700C540D4 /* ChannelIssueItem.swift in Sources */, A5074BB52BCCFD2B0081B6B1 /* IconCell.swift in Sources */, @@ -6313,6 +6359,7 @@ A58A63052C0715E500A9D02D /* VerticalBlindsVM.swift in Sources */, 40A6757F1BA1A5B6004A51C4 /* SuplaApp.m in Sources */, A50B5D502BFCBDFB00918D18 /* OpenedClosedGroupActivePercentageProvider.swift in Sources */, + A58A631D2C09C0D600A9D02D /* GarageDoorColors.swift in Sources */, 018CFD2023281A5900888CB7 /* SAElectricityMeterExtendedValue.m in Sources */, A5FE67632A65D56300147D1F /* UpdateServerHostNameUseCase.swift in Sources */, A5074BC52BCE66070081B6B1 /* DefaultWindowDimens.swift in Sources */, @@ -6530,6 +6577,7 @@ A59AB8BD2A30806800D91F1F /* BaseRepositoryMock.swift in Sources */, A50B5CF92BE9FD4700918D18 /* UpdateChannelGroupTotalValueUseCaseTests.swift in Sources */, A54A06662AF5271900C03DBC /* ThermometerHistoryDetailVMTests.swift in Sources */, + A58A63222C09E6C400A9D02D /* GarageDoorVMTests.swift in Sources */, A59AB8D52A3092C500D91F1F /* TempHumidityMeasurementItemRepositoryMock.swift in Sources */, A5CE732F2B469C36003F882C /* EspHtmlParserTests.swift in Sources */, A59AB8AB2A306FC700D91F1F /* ViewModelTest.swift in Sources */, diff --git a/SUPLA/ChannelCell.m b/SUPLA/ChannelCell.m index aee167ff..26921709 100644 --- a/SUPLA/ChannelCell.m +++ b/SUPLA/ChannelCell.m @@ -302,6 +302,7 @@ -(void) updateCellView { case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: case SUPLA_CHANNELFNC_CURTAIN: case SUPLA_CHANNELFNC_VERTICAL_BLIND: + case SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: self.left_OnlineStatus.hidden = YES; self.right_OnlineStatus.hidden = NO; break; @@ -394,6 +395,7 @@ -(void) updateCellView { case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: case SUPLA_CHANNELFNC_CURTAIN: case SUPLA_CHANNELFNC_VERTICAL_BLIND: + case SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: br = [MGSwipeButton buttonWithTitle:NSLocalizedString(@"Open", nil) icon:nil backgroundColor:[UIColor blackColor]]; bl = [MGSwipeButton buttonWithTitle:NSLocalizedString(@"Close", nil) icon:nil backgroundColor:[UIColor blackColor]]; break; diff --git a/SUPLA/Core/UI/Details/StandardDetailVC.swift b/SUPLA/Core/UI/Details/StandardDetailVC.swift index d72bcd7e..519aa872 100644 --- a/SUPLA/Core/UI/Details/StandardDetailVC.swift +++ b/SUPLA/Core/UI/Details/StandardDetailVC.swift @@ -97,6 +97,8 @@ class StandardDetailVC viewControllers.append(curtainDetail()) case .verticalBlind: viewControllers.append(verticalBlindDetail()) + case .garageDoor: + viewControllers.append(garageDoorDetail()) } } @@ -289,6 +291,17 @@ class StandardDetailVC ) return vc } + + private func garageDoorDetail() -> GarageDoorVC { + let vc = GarageDoorVC(itemBundle: item) + vc.navigationCoordinator = navigationCoordinator + vc.tabBarItem = UITabBarItem( + title: settings.showBottomLabels ? Strings.StandardDetail.tabGeneral : nil, + image: .iconGeneral, + tag: DetailTabTag.Window.rawValue + ) + return vc + } } protocol NavigationItemProvider: AnyObject { diff --git a/SUPLA/Core/UI/TableView/BaseTableViewModel.swift b/SUPLA/Core/UI/TableView/BaseTableViewModel.swift index 21821355..6301ea52 100644 --- a/SUPLA/Core/UI/TableView/BaseTableViewModel.swift +++ b/SUPLA/Core/UI/TableView/BaseTableViewModel.swift @@ -64,7 +64,8 @@ class BaseTableViewModel: BaseViewModel { SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_PROJECTOR_SCREEN, SUPLA_CHANNELFNC_CURTAIN, - SUPLA_CHANNELFNC_VERTICAL_BLIND: + SUPLA_CHANNELFNC_VERTICAL_BLIND, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: return true case SUPLA_CHANNELFNC_LIGHTSWITCH, SUPLA_CHANNELFNC_POWERSWITCH, diff --git a/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift index 635b70c2..e8399c72 100644 --- a/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift +++ b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift @@ -197,7 +197,8 @@ class ChannelBaseTableViewController { SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_PROJECTOR_SCREEN, SUPLA_CHANNELFNC_CURTAIN, - SUPLA_CHANNELFNC_VERTICAL_BLIND: true + SUPLA_CHANNELFNC_VERTICAL_BLIND, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: true default: false } } @@ -120,7 +121,8 @@ final class IconCell: BaseCell { SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_PROJECTOR_SCREEN, SUPLA_CHANNELFNC_CURTAIN, - SUPLA_CHANNELFNC_VERTICAL_BLIND: true + SUPLA_CHANNELFNC_VERTICAL_BLIND, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: true default: false } } diff --git a/SUPLA/Features/Details/WindowDetail/Base/Model/GarageDoorState.swift b/SUPLA/Features/Details/WindowDetail/Base/Model/GarageDoorState.swift new file mode 100644 index 00000000..c6ab613a --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/Model/GarageDoorState.swift @@ -0,0 +1,33 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +struct GarageDoorState: WindowState, Equatable, Changeable { + /** + * The blind roller position in percentage + * 0 - open + * 100 - closed + */ + var position: WindowGroupedValue + + var positionTextFormat: WindowGroupedValueFormat = .percentage + + /** + * Used for groups - shows positions of single roller shutter + */ + var markers: [CGFloat] = [] +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorColors.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorColors.swift new file mode 100644 index 00000000..948351b0 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorColors.swift @@ -0,0 +1,48 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +struct GarageDoorColors { + let building: UIColor + let shadow: UIColor + let slatBackground: UIColor + let slatBorder: UIColor + let markerBorder: UIColor + let markerBackground: UIColor + + static func standard(_ traitCollection: UITraitCollection) -> GarageDoorColors { + GarageDoorColors( + building: .rollerShutterWindow.resolvedColor(with: traitCollection), + shadow: .black, + slatBackground: .rollerShutterSlatBackground.resolvedColor(with: traitCollection), + slatBorder: .rollerShutterSlatBorder.resolvedColor(with: traitCollection), + markerBorder: .black, + markerBackground: .primaryVariant + ) + } + + static func offline(_ traitCollection: UITraitCollection) -> GarageDoorColors { + GarageDoorColors( + building: .surface.resolvedColor(with: traitCollection), + shadow: .black, + slatBackground: .rollerShutterDisabledSlatBackground.resolvedColor(with: traitCollection), + slatBorder: .rollerShutterDisabledSlatBorder.resolvedColor(with: traitCollection), + markerBorder: .black, + markerBackground: UIColor(argb: 0xffb3f1cb) + ) + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorView.swift new file mode 100644 index 00000000..7d3f60d5 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/GarageDoor/GarageDoorView.swift @@ -0,0 +1,227 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import Foundation + +final class GarageDoorView: BaseWindowView { + override var isEnabled: Bool { + didSet { + if (isEnabled) { + colors = GarageDoorColors.standard(traitCollection) + } else { + colors = GarageDoorColors.offline(traitCollection) + } + setNeedsDisplay() + } + } + + override var touchRect: CGRect { dimens.canvasRect } + + private let dimens = RuntimeDimens() + private lazy var colors = GarageDoorColors.standard(traitCollection) + private lazy var garageInsideImage = UIImage(named: .Image.garageContent) + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + dimens.update(frame) + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + guard let context = UIGraphicsGetCurrentContext() else { return } + + context.setShouldAntialias(true) + context.setLineWidth(1) + + let position = (windowState?.markers.isEmpty == true ? windowState?.position.value : windowState?.markers.max()) ?? 0 + let topCorrection = dimens.movementMaxHeight * (100 - position) / 100 + + // Garage + drawPath(context, fillColor: colors.building, withShadow: true) { dimens.garagePath.cgPath } + + // Cliping + context.saveGState() + context.addPath(dimens.doorClipingPath.cgPath) + context.clip() + + // Garage inside + if let garageInsideImage = garageInsideImage { + context.setShadow(offset: .zero, blur: 0) + garageInsideImage.draw(in: dimens.doorRect) + } + + // Slats + for slat in dimens.slats { + let path = UIBezierPath(rect: CGRect(x: slat.minX, y: slat.minY - topCorrection, width: slat.width, height: slat.height)).cgPath + drawPath(context, fillColor: colors.slatBackground) { path } + drawPath(context, strokeColor: colors.slatBorder) { path } + } + + context.restoreGState() // remove cliping + + // Markers + if let markers = windowState?.markers { + for marker in markers { + let markerTopCorrection = dimens.movementMaxHeight * marker / 100 + + dimens.markerPath.apply(CGAffineTransform(translationX: 0, y: markerTopCorrection)) + drawPath(context, fillColor: colors.markerBackground) { dimens.markerPath.cgPath } + drawPath(context, strokeColor: colors.markerBorder) { dimens.markerPath.cgPath } + dimens.markerPath.apply(CGAffineTransform(translationX: 0, y: -markerTopCorrection)) + } + } + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + backgroundColor = .transparent + clipsToBounds = false + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } +} + +private enum DefaultDimens { + static let width: CGFloat = 304 + static let height: CGFloat = 304 + static var ratio: CGFloat { width / height } + + static let wallHeight: CGFloat = 216 + static let doorWidth: CGFloat = 256 + static let doorHeight: CGFloat = 196 + static let slatHeight: CGFloat = 24 + static let doorRadius: CGFloat = 4 + + static let markerHeight: CGFloat = 8 + static let markerWidth: CGFloat = 28 +} + +private class RuntimeDimens { + var frame = CGRect() + var scale: CGFloat = 1 + + var canvasRect: CGRect = .zero + let garagePath: UIBezierPath = .init() + var doorRect: CGRect = .zero + var doorClipingPath: UIBezierPath = .init() + var slats: [CGRect] = [] + var movementMaxHeight: CGFloat = 0 + var markerPath: UIBezierPath = .init() + + func update(_ frame: CGRect) { + if (frame == self.frame) { + return // skip calcuation when frame is same as previous + } + self.frame = frame + + createCanvasRect(frame) + scale = canvasRect.width / DefaultDimens.width + + createGaragePath() + doorRect = createDoorRect() + let doorRadius = DefaultDimens.doorRadius * scale + doorClipingPath = UIBezierPath(roundedRect: doorRect, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: doorRadius, height: doorRadius)) + createSlats() + movementMaxHeight = doorRect.height - DefaultDimens.slatHeight * scale + createMarkerPath() + } + + private func createGaragePath() { + let wallHeight = DefaultDimens.wallHeight * scale + + garagePath.removeAllPoints() + garagePath.move(to: CGPoint(x: canvasRect.minX, y: canvasRect.maxY)) + garagePath.addLine(to: CGPoint(x: canvasRect.minX, y: canvasRect.maxY - wallHeight)) + garagePath.addLine(to: CGPoint(x: canvasRect.midX, y: canvasRect.minY)) + garagePath.addLine(to: CGPoint(x: canvasRect.maxX, y: canvasRect.maxY - wallHeight)) + garagePath.addLine(to: CGPoint(x: canvasRect.maxX, y: canvasRect.maxY)) + garagePath.close() + } + + private func createDoorRect() -> CGRect { + let doorWidth = DefaultDimens.doorWidth * scale + let doorHeight = DefaultDimens.doorHeight * scale + let left = canvasRect.midX - doorWidth / 2 + let top = canvasRect.maxY - doorHeight + return CGRect(x: left, y: top, width: doorWidth, height: doorHeight) + } + + private func createSlats() { + let slatWidth = doorRect.width + let slatHeight = DefaultDimens.slatHeight * scale + let slatCount = Int(ceil(doorRect.height / slatHeight)) + + let top = doorRect.maxY - slatHeight + let left = doorRect.minX + + slats.removeAll() + for i in 0 ..< slatCount { + slats.append(CGRect(x: left, y: top - slatHeight * CGFloat(i), width: slatWidth, height: slatHeight)) + } + } + + private func createMarkerPath() { + let left = doorRect.minX + let top = doorRect.minY + DefaultDimens.slatHeight * scale + + let width = DefaultDimens.markerWidth * scale + let height = DefaultDimens.markerHeight * scale + let halfHeight = height / 2 + + markerPath.removeAllPoints() + markerPath.move(to: CGPoint(x: left, y: top)) + markerPath.addLine(to: CGPoint(x: left + halfHeight, y: top - halfHeight)) + markerPath.addLine(to: CGPoint(x: left + width, y: top - halfHeight)) + markerPath.addLine(to: CGPoint(x: left + width, y: top + halfHeight)) + markerPath.addLine(to: CGPoint(x: left + halfHeight, y: top + halfHeight)) + markerPath.close() + } + + private func createCanvasRect(_ frame: CGRect) { + let size = getSize(frame) + canvasRect = CGRect( + origin: CGPoint(x: (frame.width - size.width) / 2, y: (frame.height - size.height) / 2), + size: size + ) + } + + private func getSize(_ frame: CGRect) -> CGSize { + let ratio = frame.width / frame.height + if (ratio > DefaultDimens.ratio) { + let height = frame.height - WindowDimens.padding * 2 + return CGSize(width: height * DefaultDimens.ratio, height: height) + } else { + let width = frame.width - WindowDimens.padding * 2 + return CGSize(width: width, height: width / DefaultDimens.ratio) + } + } +} diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/ProjectorScreen/ProjectorScreenView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/ProjectorScreen/ProjectorScreenView.swift index c27fde83..ca5e04a7 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/ProjectorScreen/ProjectorScreenView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/ProjectorScreen/ProjectorScreenView.swift @@ -75,6 +75,7 @@ final class ProjectorScreenView: BaseWindowView { // Logo if let logo = logo { let verticalCorrection = screenHeight - dimens.screenMaxHeight + context.setShadow(offset: .zero, blur: 0) logo.draw(in: dimens.logoRect.offsetBy(dx: 0, dy: verticalCorrection)) } // top part diff --git a/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift index 5122ddaf..4f5510ae 100644 --- a/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift +++ b/SUPLA/Features/Details/WindowDetail/Curtain/CurtainVM.swift @@ -85,7 +85,7 @@ private extension SAChannelGroup { } private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { - guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } return if (value.position < 100 && value.closedSensorActive) { CGFloat(100) diff --git a/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVC.swift b/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVC.swift new file mode 100644 index 00000000..3db93002 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVC.swift @@ -0,0 +1,34 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +final class GarageDoorVC: BaseWindowVC { + init(itemBundle: ItemBundle) { + super.init(itemBundle: itemBundle, viewModel: GarageDoorVM()) + } + + override func getWindowView() -> GarageDoorView { GarageDoorView() } + + override func handle(state: GarageDoorViewState) { + windowView.windowState = state.garageDoorState + + slatTiltSlider.isHidden = true + topView.valueBottom = nil + + super.handle(state: state) + } +} diff --git a/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVM.swift b/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVM.swift new file mode 100644 index 00000000..5fbf24be --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/GarageDoor/GarageDoorVM.swift @@ -0,0 +1,96 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +final class GarageDoorVM: BaseWindowVM { + override func defaultViewState() -> GarageDoorViewState { GarageDoorViewState() } + + override func handleChannel(_ channel: SAChannel) { + guard let value = channel.value?.asRollerShutterValue() else { return } + + updateView { + if ($0.manualMoving) { + return $0 + } + + let position = value.hasValidPosition ? value.position : 0 + let positionValue: WindowGroupedValue = .similar(value.online ? CGFloat(position) : 25) + let windowState = $0.garageDoorState + .changing(path: \.position, to: positionValue) + .changing(path: \.positionTextFormat, to: positionTextFormat) + + return updateChannel($0, channel, value) { + $0.changing(path: \.garageDoorState, to: windowState) + } + } + } + + override func handleGroup(_ group: SAChannelGroup, _ onlineSummary: GroupOnlineSummary) { + updateView { + if ($0.manualMoving) { + return $0 + } + + let positions = group.getRollerShutterPositions() + let overallPosition = getGroupPercentage(positions, !$0.garageDoorState.markers.isEmpty) + let windowState = $0.garageDoorState + .changing(path: \.position, to: group.isOnline() ? overallPosition : .similar(25)) + .changing(path: \.positionTextFormat, to: positionTextFormat) + .changing(path: \.markers, to: overallPosition.isDifferent() ? positions : []) + + return updateGroup($0, group, onlineSummary) { + $0.changing(path: \.garageDoorState, to: windowState) + .changing(path: \.positionUnknown, to: overallPosition == .invalid) + } + } + } +} + +struct GarageDoorViewState: BaseWindowViewState { + var remoteId: Int32? = nil + var garageDoorState: GarageDoorState = .init(position: .similar(0)) + var issues: [ChannelIssueItem] = [] + var offline: Bool = true + var showClosingPercentage: Bool = false + var calibrating: Bool = false + var calibrationPossible: Bool = false + var positionUnknown: Bool = false + var touchTime: CGFloat? = nil + var isGroup: Bool = false + var onlineStatusString: String? = nil + var moveStartTime: TimeInterval? = nil + var manualMoving: Bool = false + + var windowState: any WindowState { garageDoorState } +} + +private extension SAChannelGroup { + func getRollerShutterPositions() -> [CGFloat] { + guard let totalValue = total_value as? GroupTotalValue else { return [] } + return totalValue.values.compactMap { valueToPosition($0) } + } + + private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } + + return if (value.position < 100 && value.closedSensorActive) { + CGFloat(100) + } else { + CGFloat(value.position) + } + } +} diff --git a/SUPLA/Features/Details/WindowDetail/ProjectorScreen/ProjectorScreenVM.swift b/SUPLA/Features/Details/WindowDetail/ProjectorScreen/ProjectorScreenVM.swift index 4b0f10a8..fc7694cd 100644 --- a/SUPLA/Features/Details/WindowDetail/ProjectorScreen/ProjectorScreenVM.swift +++ b/SUPLA/Features/Details/WindowDetail/ProjectorScreen/ProjectorScreenVM.swift @@ -85,7 +85,7 @@ private extension SAChannelGroup { } private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { - guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } return if (value.position < 100 && value.closedSensorActive) { CGFloat(100) diff --git a/SUPLA/Features/Details/WindowDetail/RollerShutter/RollerShutterVM.swift b/SUPLA/Features/Details/WindowDetail/RollerShutter/RollerShutterVM.swift index 49abf184..20b71ea0 100644 --- a/SUPLA/Features/Details/WindowDetail/RollerShutter/RollerShutterVM.swift +++ b/SUPLA/Features/Details/WindowDetail/RollerShutter/RollerShutterVM.swift @@ -86,7 +86,7 @@ private extension SAChannelGroup { } private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { - guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } return if (value.position < 100 && value.closedSensorActive) { CGFloat(100) diff --git a/SUPLA/Features/Details/WindowDetail/RoofWindow/RoofWindowVM.swift b/SUPLA/Features/Details/WindowDetail/RoofWindow/RoofWindowVM.swift index 9d570f22..585b9b19 100644 --- a/SUPLA/Features/Details/WindowDetail/RoofWindow/RoofWindowVM.swift +++ b/SUPLA/Features/Details/WindowDetail/RoofWindow/RoofWindowVM.swift @@ -85,7 +85,7 @@ private extension SAChannelGroup { } private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { - guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } return if (value.position < 100 && value.closedSensorActive) { CGFloat(100) diff --git a/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift index 013866a0..b3b601c2 100644 --- a/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift +++ b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift @@ -85,7 +85,7 @@ private extension SAChannelGroup { } private func valueToPosition(_ baseGroupValue: BaseGroupValue) -> CGFloat? { - guard let value = baseGroupValue as? RollerShutterGroupValue else { return nil } + guard let value = baseGroupValue as? ShadingSystemGroupValue else { return nil } return if (value.position < 100 && value.closedSensorActive) { CGFloat(100) diff --git a/SUPLA/Model/CoreData/Extensions/SAChannelGroup+Ext.swift b/SUPLA/Model/CoreData/Extensions/SAChannelGroup+Ext.swift index 800ece62..9c87dca3 100644 --- a/SUPLA/Model/CoreData/Extensions/SAChannelGroup+Ext.swift +++ b/SUPLA/Model/CoreData/Extensions/SAChannelGroup+Ext.swift @@ -30,7 +30,7 @@ extension SAChannelGroup { if let value = $0 as? ShadowingBlindGroupValue { return value.position } - if let value = $0 as? RollerShutterGroupValue { + if let value = $0 as? ShadingSystemGroupValue { return value.closedSensorActive ? 100 : value.position } diff --git a/SUPLA/Resources/Default.strings b/SUPLA/Resources/Default.strings index 34b16406..4e56e6ba 100644 --- a/SUPLA/Resources/Default.strings +++ b/SUPLA/Resources/Default.strings @@ -70,6 +70,7 @@ "channel_caption_projector_screen" = "Projector screen"; "channel_caption_curtain" = "Curtain"; "channel_caption_vertical_blind" = "Vertical blind"; +"channel_caption_garage_door" = "Garage door"; /* Main */ "dialog_new_gesture_info_text" = "Swipe gesture to open details was removed, tap on particular channel to open it."; diff --git a/SUPLA/Resources/Extensions/String+Icons.swift b/SUPLA/Resources/Extensions/String+Icons.swift index 27034b0e..c1b6c21b 100644 --- a/SUPLA/Resources/Extensions/String+Icons.swift +++ b/SUPLA/Resources/Extensions/String+Icons.swift @@ -142,5 +142,6 @@ extension String { struct Image { static let logo = "logo" + static let garageContent = "garage_content" } } diff --git a/SUPLA/Resources/Extensions/UIImage+Supla.swift b/SUPLA/Resources/Extensions/UIImage+Supla.swift index 51146c80..cdec96ad 100644 --- a/SUPLA/Resources/Extensions/UIImage+Supla.swift +++ b/SUPLA/Resources/Extensions/UIImage+Supla.swift @@ -77,4 +77,5 @@ extension UIImage { static let thumbCool = UIImage(named: .Icons.thumbCool) @objc static let logo = UIImage(named: .Image.logo) + @objc static let garageContent = UIImage(named: .Image.garageContent) } diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/Contents.json new file mode 100644 index 00000000..7ab82f5a --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "garage (1).svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "garage (3).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (1).svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (1).svg new file mode 100644 index 00000000..d3946bf4 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (1).svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (3).svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (3).svg new file mode 100644 index 00000000..a7717fe3 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-closed.imageset/garage (3).svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/Contents.json new file mode 100644 index 00000000..f483b1e7 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "garage.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "garage (2).svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage (2).svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage (2).svg new file mode 100644 index 00000000..955b78fe --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage (2).svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage.svg new file mode 100644 index 00000000..05db4603 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_garage_door-open.imageset/garage.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Contents.json new file mode 100644 index 00000000..1eda1a25 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Frame 460.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Frame 461.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 460.svg b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 460.svg new file mode 100644 index 00000000..70c277e2 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 460.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 461.svg b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 461.svg new file mode 100644 index 00000000..35b88d99 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/garage_content.imageset/Frame 461.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/SUPLA/Resources/Strings.swift b/SUPLA/Resources/Strings.swift index c41b5e0d..9cd082f7 100644 --- a/SUPLA/Resources/Strings.swift +++ b/SUPLA/Resources/Strings.swift @@ -307,6 +307,7 @@ struct Strings { static let captionProjectorScreen = "channel_caption_projector_screen".toLocalized() static let captionCurtain = "channel_caption_curtain".toLocalized() static let captionVerticalBlind = "channel_caption_vertical_blind".toLocalized() + static let captionGarageDoor = "channel_caption_garage_door".toLocalized() } } diff --git a/SUPLA/Resources/de.lproj/Localizable.strings b/SUPLA/Resources/de.lproj/Localizable.strings index 82bff2a0..bc8956a1 100644 --- a/SUPLA/Resources/de.lproj/Localizable.strings +++ b/SUPLA/Resources/de.lproj/Localizable.strings @@ -327,6 +327,7 @@ "channel_caption_projector_screen" = "Leinwand"; "channel_caption_curtain" = "Vorhang"; "channel_caption_vertical_blind" = "Vertikaljalousien"; +"channel_caption_garage_door" = "Garagentor"; /* Main */ "dialog_new_gesture_info_text" = "Wischgeste zum Öffnen der Kanaldetails wurde gelöscht, tippe auf dem Kanal, um sie zu sehen."; diff --git a/SUPLA/Resources/pl.lproj/Localizable.strings b/SUPLA/Resources/pl.lproj/Localizable.strings index bc9dc86e..acb3af25 100644 --- a/SUPLA/Resources/pl.lproj/Localizable.strings +++ b/SUPLA/Resources/pl.lproj/Localizable.strings @@ -352,6 +352,7 @@ "channel_caption_projector_screen" = "Ekran projekcyjny"; "channel_caption_curtain" = "Zasłona"; "channel_caption_vertical_blind" = "Żaluzja pionowa"; +"channel_caption_garage_door" = "Brama garażowa"; /* Main */ "dialog_new_gesture_info_text" = "Usunęliśmy gest przesunięcia otwierający szczegóły, aby je zobaczyć dotknij wybrany kanał."; diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift index 3c029e62..1d93f2b8 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift @@ -121,6 +121,8 @@ final class GetChannelBaseDefaultCaptionUseCaseImpl: GetChannelBaseDefaultCaptio return Strings.General.Channel.captionCurtain case SUPLA_CHANNELFNC_VERTICAL_BLIND: return Strings.General.Channel.captionVerticalBlind + case SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: + return Strings.General.Channel.captionGarageDoor default: return NSLocalizedString("Not supported function", comment: "") } diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift index 0ef8173a..9d041552 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift @@ -49,7 +49,8 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_CURTAIN, - SUPLA_CHANNELFNC_VERTICAL_BLIND: + SUPLA_CHANNELFNC_VERTICAL_BLIND, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: return valueWrapper.rollerShutterClosed ? .closed : .opened case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: return valueWrapper.projectorScreenClosed ? .closed : .opened @@ -106,6 +107,7 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_CURTAIN, SUPLA_CHANNELFNC_VERTICAL_BLIND, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR, SUPLA_CHANNELFNC_VALVE_OPENCLOSE, SUPLA_CHANNELFNC_VALVE_PERCENTAGE: .opened case SUPLA_CHANNELFNC_PROJECTOR_SCREEN: .closed diff --git a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift index f3d06241..0c713a7b 100644 --- a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift +++ b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift @@ -44,6 +44,8 @@ final class ProvideDetailTypeUseCaseImpl: ProvideDetailTypeUseCase { return .windowDetail(pages: [.curtain]) case SUPLA_CHANNELFNC_VERTICAL_BLIND: return .windowDetail(pages: [.verticalBlind]) + case SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: + return .windowDetail(pages: [.garageDoor]) case SUPLA_CHANNELFNC_LIGHTSWITCH, SUPLA_CHANNELFNC_POWERSWITCH, @@ -145,4 +147,5 @@ enum DetailPage { case projectorScreen case curtain case verticalBlind + case garageDoor } diff --git a/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift b/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift index d0bb1c21..f7798a14 100644 --- a/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift +++ b/SUPLA/UseCase/Group/ActivePercentage/ShadingSystemGroupActivePercentageProvider.swift @@ -22,14 +22,15 @@ final class ShadingSystemGroupActivePercentageProvider: GroupActivePercentagePro case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, SUPLA_CHANNELFNC_TERRACE_AWNING, - SUPLA_CHANNELFNC_CURTAIN: true + SUPLA_CHANNELFNC_CURTAIN, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: true default: false } } func getActivePercentage(_ valueIndex: Int, _ totalValue: GroupTotalValue) -> Int { totalValue.values - .map { $0 as! RollerShutterGroupValue } + .map { $0 as! ShadingSystemGroupValue } .reduce(0) { result, value in value.position >= 100 || value.closedSensorActive ? result + 1 : result } * 100 / totalValue.values.count diff --git a/SUPLA/UseCase/Group/TotalValue/GroupTotalValue.swift b/SUPLA/UseCase/Group/TotalValue/GroupTotalValue.swift index 88e92768..414f0980 100644 --- a/SUPLA/UseCase/Group/TotalValue/GroupTotalValue.swift +++ b/SUPLA/UseCase/Group/TotalValue/GroupTotalValue.swift @@ -35,7 +35,7 @@ class GroupTotalValue: NSObject, NSSecureCoding { values = coder.decodeObject( of: [ NSArray.self, - RollerShutterGroupValue.self, + ShadingSystemGroupValue.self, ShadowingBlindGroupValue.self, IntegerGroupValue.self, BoolGroupValue.self, @@ -63,7 +63,7 @@ class GroupTotalValue: NSObject, NSSecureCoding { @objc class BaseGroupValue: NSObject {} -@objc class RollerShutterGroupValue: BaseGroupValue, NSSecureCoding { +@objc class ShadingSystemGroupValue: BaseGroupValue, NSSecureCoding { static var supportsSecureCoding: Bool = true @objc let position: Int @@ -86,7 +86,7 @@ class GroupTotalValue: NSObject, NSSecureCoding { } override func isEqual(_ object: Any?) -> Bool { - guard let other = object as? RollerShutterGroupValue else { return false } + guard let other = object as? ShadingSystemGroupValue else { return false } return other.position == position && other.closedSensorActive == closedSensorActive } diff --git a/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift b/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift index 960c3b76..fcc9cd79 100644 --- a/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift +++ b/SUPLA/UseCase/Group/UpdateChannelGroupTotalValueUseCase.swift @@ -124,8 +124,9 @@ private extension SAChannelGroup { return IntegerGroupValue(value: value.asRollerShutterValue().alwaysValidPosition) case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, - SUPLA_CHANNELFNC_CURTAIN: - return RollerShutterGroupValue( + SUPLA_CHANNELFNC_CURTAIN, + SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR: + return ShadingSystemGroupValue( position: value.asRollerShutterValue().alwaysValidPosition, closedSensorActive: value.hiSubValue() == 1 ) diff --git a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift index 03cb9425..a9444e49 100644 --- a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift +++ b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift @@ -80,6 +80,7 @@ final class GetDefaultIconNameUseCaseImpl: GetDefaultIconNameUseCase { StaticIconNameProducer(function: SUPLA_CHANNELFNC_PROJECTOR_SCREEN, name: "fnc_projector_screen"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_CURTAIN, name: "fnc_curtain"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_VERTICAL_BLIND, name: "fnc_vertical_blind"), + StaticIconNameProducer(function: SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR, name: "fnc_garage_door"), PowerSwitchIconNameProducer(), LightSwitchIconNameProducer(), StaircaseTimerIconNameProducer(), diff --git a/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift b/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift index a0797c6b..b72b269f 100644 --- a/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift +++ b/SUPLATests/Tests/Features/Details/WindowDetail/Curtain/CurtainVMTests.swift @@ -91,8 +91,8 @@ final class CurtainVMTests: ViewModelTest group.remote_id = 234 group.online = 1 group.total_value = GroupTotalValue(values: [ - RollerShutterGroupValue(position: 50, closedSensorActive: false), - RollerShutterGroupValue(position: 80, closedSensorActive: false) + ShadingSystemGroupValue(position: 50, closedSensorActive: false), + ShadingSystemGroupValue(position: 80, closedSensorActive: false) ]) settings.showOpeningPercentReturns = true diff --git a/SUPLATests/Tests/Features/Details/WindowDetail/GarageDoor/GarageDoorVMTests.swift b/SUPLATests/Tests/Features/Details/WindowDetail/GarageDoor/GarageDoorVMTests.swift new file mode 100644 index 00000000..067a1b7a --- /dev/null +++ b/SUPLATests/Tests/Features/Details/WindowDetail/GarageDoor/GarageDoorVMTests.swift @@ -0,0 +1,123 @@ +/* + Copyright (C) AC SOFTWARE SP. Z O.O. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +@testable import SUPLA +import XCTest + +final class GarageDoorVMTests: ViewModelTest { + + private lazy var readChannelByRemoteIdUseCase: ReadChannelByRemoteIdUseCaseMock! = ReadChannelByRemoteIdUseCaseMock() + + private lazy var readGroupByRemoteIdUseCase: ReadGroupByRemoteIdUseCaseMock! = ReadGroupByRemoteIdUseCaseMock() + + private lazy var getGroupOnlineSummaryUseCase: GetGroupOnlineSummaryUseCaseMock! = GetGroupOnlineSummaryUseCaseMock() + + private lazy var settings: GlobalSettingsMock! = GlobalSettingsMock() + + private lazy var viewModel: GarageDoorVM! = GarageDoorVM() + + override func setUp() { + + DiContainer.register(ReadChannelByRemoteIdUseCase.self, readChannelByRemoteIdUseCase!) + DiContainer.register(ReadGroupByRemoteIdUseCase.self, readGroupByRemoteIdUseCase!) + DiContainer.register(GetGroupOnlineSummaryUseCase.self, getGroupOnlineSummaryUseCase!) + DiContainer.register(GlobalSettings.self, settings!) + } + + override func tearDown() { + super.tearDown() + + readChannelByRemoteIdUseCase = nil + settings = nil + viewModel = nil + } + + func test_shouldLoadChannel() { + // given + let channel = SAChannel(testContext: nil) + channel.remote_id = 123 + channel.flags = Int64(SUPLA_CHANNEL_FLAG_CALCFG_RECALIBRATE) + channel.value = SAChannelValue(testContext: nil) + channel.value?.value = NSData(data: RollerShutterValue.mockData(position: 50, flags: SuplaRollerShutterFlag.motorProblem.rawValue)) + channel.value?.online = true + + settings.showOpeningPercentReturns = false + readChannelByRemoteIdUseCase.returns = .just(channel) + + // when + observe(viewModel) + viewModel.loadData(remoteId: 123, type: .channel) + + // then + assertStates(expected: [ + GarageDoorViewState(), + GarageDoorViewState( + remoteId: 123, + garageDoorState: GarageDoorState(position: .similar(50)), + issues: [ + ChannelIssueItem( + issueIconType: .warning, + description: Strings.RollerShutterDetail.calibrationFailed + ) + ], + offline: false, + showClosingPercentage: true, + calibrating: false, + calibrationPossible: true + ) + ]) + assertEvents(expected: []) + } + + func test_shouldLoadGroup() { + // given + let groupOnlineSummary = GroupOnlineSummary(onlineCount: 2, count: 3) + let group = SAChannelGroup(testContext: nil) + group.remote_id = 234 + group.online = 1 + group.total_value = GroupTotalValue(values: [ + ShadingSystemGroupValue(position: 50, closedSensorActive: false), + ShadingSystemGroupValue(position: 80, closedSensorActive: false) + ]) + + settings.showOpeningPercentReturns = true + readGroupByRemoteIdUseCase.returns = .just(group) + getGroupOnlineSummaryUseCase.returns = .just(groupOnlineSummary) + + // when + observe(viewModel) + viewModel.loadData(remoteId: 234, type: .group) + + // then + assertStates(expected: [ + GarageDoorViewState(), + GarageDoorViewState( + remoteId: 234, + garageDoorState: GarageDoorState( + position: .different(min: 50, max: 80), + positionTextFormat: .openingPercentage, + markers: [50, 80] + ), + offline: false, + isGroup: true, + onlineStatusString: "2/3" + ) + ]) + } +} + diff --git a/SUPLATests/Tests/Features/Details/WindowDetail/RollerShutter/RollerShutterVMTests.swift b/SUPLATests/Tests/Features/Details/WindowDetail/RollerShutter/RollerShutterVMTests.swift index 6ede676f..63fc04b6 100644 --- a/SUPLATests/Tests/Features/Details/WindowDetail/RollerShutter/RollerShutterVMTests.swift +++ b/SUPLATests/Tests/Features/Details/WindowDetail/RollerShutter/RollerShutterVMTests.swift @@ -107,8 +107,8 @@ final class RollerShutterVMTests: ViewModelTest { thirdGroup.remote_id = 33 thirdGroup.func = SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW thirdGroup.online = 100 - thirdGroup.total_value = GroupTotalValue(values: [RollerShutterGroupValue(position: 10, closedSensorActive: false)]) + thirdGroup.total_value = GroupTotalValue(values: [ShadingSystemGroupValue(position: 10, closedSensorActive: false)]) let firstGroupRelation1 = SAChannelGroupRelation(testContext: nil) firstGroupRelation1.group = firstGroup @@ -93,8 +93,8 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { XCTAssertEqual(firstGroup.online, 66) XCTAssertTrue(firstGroup.total_value is GroupTotalValue) if let groupTotalValue = firstGroup.total_value as? GroupTotalValue, - let firstRelationValue = groupTotalValue.values[0] as? RollerShutterGroupValue, - let secondRelationValue = groupTotalValue.values[1] as? RollerShutterGroupValue + let firstRelationValue = groupTotalValue.values[0] as? ShadingSystemGroupValue, + let secondRelationValue = groupTotalValue.values[1] as? ShadingSystemGroupValue { XCTAssertEqual(groupTotalValue.values.count, 2) XCTAssertEqual(firstRelationValue.position, 18) @@ -399,7 +399,43 @@ final class UpdateChannelGroupTotalValueUseCaseTests: UseCaseTest<[Int32]> { XCTAssertEqual(group.online, 100) XCTAssertTrue(group.total_value is GroupTotalValue) if let groupTotalValue = group.total_value as? GroupTotalValue, - let firstRelationValue = groupTotalValue.values[0] as? RollerShutterGroupValue + let firstRelationValue = groupTotalValue.values[0] as? ShadingSystemGroupValue + { + XCTAssertEqual(groupTotalValue.values.count, 1) + XCTAssertEqual(firstRelationValue.position, 18) + } else { + XCTFail("First group total value not created!") + } + + assertEvents([ + .next([11]), // in third group there are no changes so it should not be present here. + .completed + ]) + } + + func testIfTotalValueIsCreatedForGarageDoor() { + // given + let group = SAChannelGroup(testContext: nil) + group.remote_id = 11 + group.func = SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR + + let groupRelation = SAChannelGroupRelation(testContext: nil) + groupRelation.group = group + groupRelation.value = SAChannelValue.mockRollerShutter(position: 18) + + channelGroupRelationRepository.getAllVisibleRelationsForActiveProfileReturns = .just([ + groupRelation, + ]) + channelGroupRelationRepository.saveObservable = .just(()) + + // when + useCase.invoke().subscribe(observer).disposed(by: disposeBag) + + // then + XCTAssertEqual(group.online, 100) + XCTAssertTrue(group.total_value is GroupTotalValue) + if let groupTotalValue = group.total_value as? GroupTotalValue, + let firstRelationValue = groupTotalValue.values[0] as? ShadingSystemGroupValue { XCTAssertEqual(groupTotalValue.values.count, 1) XCTAssertEqual(firstRelationValue.position, 18) diff --git a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift index 8eb8d78d..26da4421 100644 --- a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift @@ -1731,4 +1731,42 @@ final class GetDefaultIconNameUseCaseTests: XCTestCase { // then XCTAssertEqual(iconName, "fnc_vertical_blind-closed") } + + func test_garageDoorOpened() { + // given + let function = SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .opened, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_garage_door-open") + } + + func test_garageDoorClose() { + // given + let function = SUPLA_CHANNELFNC_ROLLER_GARAGE_DOOR + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .closed, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_garage_door-closed") + } }