diff --git a/SUPLA.xcodeproj/project.pbxproj b/SUPLA.xcodeproj/project.pbxproj index ffe555bc..6455f82c 100644 --- a/SUPLA.xcodeproj/project.pbxproj +++ b/SUPLA.xcodeproj/project.pbxproj @@ -341,6 +341,11 @@ A50B5D222BEE167300918D18 /* DeleteChannelMeasurementsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D212BEE167300918D18 /* DeleteChannelMeasurementsUseCase.swift */; }; A50B5D242BEE4CE600918D18 /* UIViewController+Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D232BEE4CE600918D18 /* UIViewController+Toast.swift */; }; A50B5D262BEE59B700918D18 /* DeleteChannelMeasurementsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D252BEE59B700918D18 /* DeleteChannelMeasurementsUseCaseTests.swift */; }; + A50B5D292BF49F2700918D18 /* TerraceAwningVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D282BF49F2700918D18 /* TerraceAwningVM.swift */; }; + A50B5D2B2BF49F3000918D18 /* TerraceAwningVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D2A2BF49F3000918D18 /* TerraceAwningVC.swift */; }; + A50B5D2D2BF49F7700918D18 /* TerraceAwningWindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D2C2BF49F7700918D18 /* TerraceAwningWindowState.swift */; }; + A50B5D302BF4A10200918D18 /* TerraceAwningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D2F2BF4A10200918D18 /* TerraceAwningView.swift */; }; + A50B5D332BF54FF300918D18 /* TerraceAwningVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B5D322BF54FF300918D18 /* TerraceAwningVMTests.swift */; }; A50CD3D12A4D78610012DD9B /* libc++abi.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = A50CD3D02A4D78610012DD9B /* libc++abi.tbd */; }; A50CD3D32A4D82ED0012DD9B /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50CD3D22A4D82ED0012DD9B /* DateProvider.swift */; }; A50CD3D62A4D99E60012DD9B /* UpdateTokenTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50CD3D52A4D99E60012DD9B /* UpdateTokenTaskTests.swift */; }; @@ -1626,6 +1631,11 @@ A50B5D212BEE167300918D18 /* DeleteChannelMeasurementsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteChannelMeasurementsUseCase.swift; sourceTree = ""; }; A50B5D232BEE4CE600918D18 /* UIViewController+Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Toast.swift"; sourceTree = ""; }; A50B5D252BEE59B700918D18 /* DeleteChannelMeasurementsUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteChannelMeasurementsUseCaseTests.swift; sourceTree = ""; }; + A50B5D282BF49F2700918D18 /* TerraceAwningVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerraceAwningVM.swift; sourceTree = ""; }; + A50B5D2A2BF49F3000918D18 /* TerraceAwningVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerraceAwningVC.swift; sourceTree = ""; }; + A50B5D2C2BF49F7700918D18 /* TerraceAwningWindowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerraceAwningWindowState.swift; sourceTree = ""; }; + A50B5D2F2BF4A10200918D18 /* TerraceAwningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerraceAwningView.swift; sourceTree = ""; }; + A50B5D322BF54FF300918D18 /* TerraceAwningVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerraceAwningVMTests.swift; sourceTree = ""; }; A50CD3D02A4D78610012DD9B /* libc++abi.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++abi.tbd"; path = "usr/lib/libc++abi.tbd"; sourceTree = SDKROOT; }; A50CD3D22A4D82ED0012DD9B /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; A50CD3D52A4D99E60012DD9B /* UpdateTokenTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTokenTaskTests.swift; sourceTree = ""; }; @@ -3010,6 +3020,7 @@ A5074BBB2BCE5CBF0081B6B1 /* UI */ = { isa = PBXGroup; children = ( + A50B5D2E2BF4A02B00918D18 /* TerraceAwning */, A5074BCA2BCE6F2C0081B6B1 /* FacadeBlinds */, A5074BC32BCE64A30081B6B1 /* WindowView */, A5B3A4B72BB57B4D0001D006 /* RoofWindow */, @@ -3121,6 +3132,7 @@ A50B5D102BECC58800918D18 /* WindowDetail */ = { isa = PBXGroup; children = ( + A50B5D312BF54FE600918D18 /* TerraceAwning */, A50B5D1E2BECE8B100918D18 /* FacadeBlinds */, A50B5D1B2BECDAA100918D18 /* RollerShutter */, A50B5D112BECC59100918D18 /* RoofWindow */, @@ -3177,6 +3189,31 @@ path = FacadeBlinds; sourceTree = ""; }; + A50B5D272BF49F0D00918D18 /* TerraceAwning */ = { + isa = PBXGroup; + children = ( + A50B5D282BF49F2700918D18 /* TerraceAwningVM.swift */, + A50B5D2A2BF49F3000918D18 /* TerraceAwningVC.swift */, + ); + path = TerraceAwning; + sourceTree = ""; + }; + A50B5D2E2BF4A02B00918D18 /* TerraceAwning */ = { + isa = PBXGroup; + children = ( + A50B5D2F2BF4A10200918D18 /* TerraceAwningView.swift */, + ); + path = TerraceAwning; + sourceTree = ""; + }; + A50B5D312BF54FE600918D18 /* TerraceAwning */ = { + isa = PBXGroup; + children = ( + A50B5D322BF54FF300918D18 /* TerraceAwningVMTests.swift */, + ); + path = TerraceAwning; + sourceTree = ""; + }; A50CD3D42A4D99CF0012DD9B /* UpdateToken */ = { isa = PBXGroup; children = ( @@ -3780,6 +3817,7 @@ A55A8D6C2BA831AD00C540D4 /* WindowDetail */ = { isa = PBXGroup; children = ( + A50B5D272BF49F0D00918D18 /* TerraceAwning */, A5074BB62BCE58B30081B6B1 /* Base */, A5074BCD2BCE72110081B6B1 /* FacadeBlinds */, A5074BBE2BCE609D0081B6B1 /* RoofWindow */, @@ -3847,6 +3885,7 @@ A50B5D052BEA4DA700918D18 /* RollerShutterWindowState.swift */, A50B5D072BEA4E2A00918D18 /* FacadeBlindWindowState.swift */, A50B5D092BEA4EC700918D18 /* FacadeBlindMarker.swift */, + A50B5D2C2BF49F7700918D18 /* TerraceAwningWindowState.swift */, ); path = Model; sourceTree = ""; @@ -5665,6 +5704,7 @@ A5F29BB22A24C52600ED700A /* UpdateChannelUseCase.swift in Sources */, A5074BE22BCFE6230081B6B1 /* SuplaTiltControlType.swift in Sources */, A57C4AB52AAF458F00D9C695 /* SuplaScheduleProgram.swift in Sources */, + A50B5D292BF49F2700918D18 /* TerraceAwningVM.swift in Sources */, A5F8361F2A2E008C00E5CA71 /* AuthProfileItemInitialMigrationPolicy.swift in Sources */, 40DE1A361BCD1B55004CF43B /* AboutVC.m in Sources */, A5E4905A2A3FA151006801FE /* GetAllIconsToDownloadUseCase.swift in Sources */, @@ -5981,6 +6021,7 @@ A57668E52AEA67E00025509D /* LoadChannelWithChildrenMeasurementsDateRangeUseCase.swift in Sources */, A50B5D082BEA4E2A00918D18 /* FacadeBlindWindowState.swift in Sources */, A5A14A422B614242004B1598 /* SuplaChannelGeneralPurposeMeasurementConfig.swift in Sources */, + A50B5D302BF4A10200918D18 /* TerraceAwningView.swift in Sources */, 018CFD2323281ADB00888CB7 /* SAThermostatExtendedValue.m in Sources */, 40BBA35D20AC6E220054CD20 /* SAChannelGroupRelation+CoreDataProperties.m in Sources */, A5AE7A872A3AD6A10097FA8B /* ChannelHeightCell.swift in Sources */, @@ -6125,6 +6166,7 @@ A57669012AEFD4420025509D /* UsetStateHolder.swift in Sources */, A58A9BFE2AA0A4BD00D28848 /* UIView+Ext.swift in Sources */, A5477DC92AA5E6EE00220B4A /* ChannelWithChildren.swift in Sources */, + A50B5D2B2BF49F3000918D18 /* TerraceAwningVC.swift in Sources */, A55A8D772BA84DCE00C540D4 /* RollerShutterVC.swift in Sources */, A530EE2C2A57E15000F8DAEE /* HeatpolHomeplusIconNameProducer.swift in Sources */, A54149302B63059200B44BD6 /* GpmHistoryDetailVC.swift in Sources */, @@ -6135,6 +6177,7 @@ 0111E924263F374800302356 /* UIColor+SUPLA.m in Sources */, A57785BD29E7FAD0001C631E /* SuplaClientProvider.swift in Sources */, 01D5E0C822DB7FAF00FBE1DC /* SAChartMarkerView.swift in Sources */, + A50B5D2D2BF49F7700918D18 /* TerraceAwningWindowState.swift in Sources */, A56F15F52A2E6E3C00C2E21B /* Migration12to13ModelMapping.xcmappingmodel in Sources */, 01C1719022C7F3A2005983E1 /* SAIncrementalMeasurementItem+CoreDataProperties.m in Sources */, A5A1C0A829F2AAFA0083818D /* VibrationService.swift in Sources */, @@ -6223,6 +6266,7 @@ A55A8DC72BB3139E00C540D4 /* ExecuteRollerShutterActionUseCaseTests.swift in Sources */, A54A065D2AF4E58F00C03DBC /* CStructsConstructor.m in Sources */, A573B0AE2A6022C1001E19D0 /* GetDefaultIconNameUseCaseMock.swift in Sources */, + A50B5D332BF54FF300918D18 /* TerraceAwningVMTests.swift in Sources */, A59AB8AE2A3071A300D91F1F /* GroupListVMTests.swift in Sources */, A5E40B5D2B86075500DB6ABE /* ValueProviderMocks.swift in Sources */, A54A06752AF8D61000C03DBC /* LinkedListTests.swift in Sources */, diff --git a/SUPLA/Core/Extensions/UIView+Ext.swift b/SUPLA/Core/Extensions/UIView+Ext.swift index 01aa0265..a96bb278 100644 --- a/SUPLA/Core/Extensions/UIView+Ext.swift +++ b/SUPLA/Core/Extensions/UIView+Ext.swift @@ -39,4 +39,22 @@ extension UIView { NSLayoutConstraint.deactivate(self.constraints) self.removeConstraints(self.constraints) } + + func drawPath(_ context: CGContext, fillColor: UIColor? = nil, strokeColor: UIColor? = nil, withShadow: Bool = false, _ pathProducer: () -> CGPath) { + context.beginPath() + context.addPath(pathProducer()) + if (withShadow) { + context.setShadow(offset: ShadowValues.offset, blur: ShadowValues.blur) + } else { + context.setShadow(offset: .zero, blur: 0) + } + if let color = fillColor { + context.setFillColor(color.cgColor) + context.drawPath(using: .fill) + } + if let color = strokeColor { + context.setStrokeColor(color.cgColor) + context.drawPath(using: .stroke) + } + } } diff --git a/SUPLA/Core/UI/Details/StandardDetailVC.swift b/SUPLA/Core/UI/Details/StandardDetailVC.swift index 6396ff91..ffc314da 100644 --- a/SUPLA/Core/UI/Details/StandardDetailVC.swift +++ b/SUPLA/Core/UI/Details/StandardDetailVC.swift @@ -89,6 +89,8 @@ class StandardDetailVC viewControllers.append(roofWindowDetail()) case .facadeBlind: viewControllers.append(facadeBlindDetail()) + case .terraceAwning: + viewControllers.append(terraceAwningDetail()) } } @@ -237,6 +239,17 @@ class StandardDetailVC ) return vc } + + private func terraceAwningDetail() -> TerraceAwningVC { + let vc = TerraceAwningVC(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/ChannelBaseTableViewController.swift b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift index e0dd6ea5..0aa1fb78 100644 --- a/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift +++ b/SUPLA/Core/UI/TableView/ChannelBaseTableViewController.swift @@ -193,7 +193,8 @@ class ChannelBaseTableViewController { switch (data?.channel.func) { case SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, - SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER: true + SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, + SUPLA_CHANNELFNC_TERRACE_AWNING: true default: false } } @@ -112,7 +113,8 @@ final class IconCell: BaseCell { switch (data?.channel.func) { case SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, - SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER: true + SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER, + SUPLA_CHANNELFNC_TERRACE_AWNING: true default: false } } diff --git a/SUPLA/Features/Details/WindowDetail/Base/Model/TerraceAwningWindowState.swift b/SUPLA/Features/Details/WindowDetail/Base/Model/TerraceAwningWindowState.swift new file mode 100644 index 00000000..9513b32c --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/Model/TerraceAwningWindowState.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 TerraceAwningWindowState: 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/RoofWindow/RoofWindowView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/RoofWindow/RoofWindowView.swift index 3206851e..1362e431 100644 --- a/SUPLA/Features/Details/WindowDetail/Base/UI/RoofWindow/RoofWindowView.swift +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/RoofWindow/RoofWindowView.swift @@ -178,24 +178,6 @@ final class RoofWindowView: BaseWindowView { } } - private func drawPath(_ context: CGContext, fillColor: UIColor? = nil, strokeColor: UIColor? = nil, withShadow: Bool = false, _ pathProducer: () -> CGPath) { - context.beginPath() - context.addPath(pathProducer()) - if (withShadow) { - context.setShadow(offset: ShadowValues.offset, blur: ShadowValues.blur) - } else { - context.setShadow(offset: .zero, blur: 0) - } - if let color = fillColor { - context.setFillColor(color.cgColor) - context.drawPath(using: .fill) - } - if let color = strokeColor { - context.setStrokeColor(color.cgColor) - context.drawPath(using: .stroke) - } - } - private func dynamicTransformation(_ offset: CGFloat? = nil) -> CGAffineTransform { let xOffset = offset == nil ? openedOffset : offset! let firstTransformation = CATransform3DMakeRotation(degreesToRadians(windowRotationY), 0, 1, 0) diff --git a/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift b/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift new file mode 100644 index 00000000..a0f347bb --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/Base/UI/TerraceAwning/TerraceAwningView.swift @@ -0,0 +1,280 @@ +/* + 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 TerraceAwningView: BaseWindowView { + + override var isEnabled: Bool { + didSet { + if (isEnabled) { + colors = WindowColors.standard(traitCollection) + } else { + colors = WindowColors.offline(traitCollection) + } + setNeedsDisplay() + } + } + + override var touchRect: CGRect { dimens.touchRect } + + private let dimens = RuntimeDimens() + private lazy var colors = WindowColors.standard(traitCollection) + + 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) + + if let markers = windowState?.markers, !markers.isEmpty { + drawAwningShadow(context, position: markers.max()!) + } else { + drawAwningShadow(context, position: windowState?.position.value ?? 0) + } + + drawWindow(context) + + if let markers = windowState?.markers, !markers.isEmpty { + for (index, marker) in markers.sorted(by: { first, second in first > second }).enumerated() { + drawAwningLikeMarker(context, position: marker, withFront: index == 0) + } + } else { + drawAwning(context) + } + } + + private func setupView() { + translatesAutoresizingMaskIntoConstraints = false + backgroundColor = .transparent + } + + private func drawWindow(_ context: CGContext) { + let path = UIBezierPath(roundedRect: dimens.windowRect, cornerRadius: DefaultWindowDimens.windowCornerRadius) + + drawPath(context, fillColor: colors.window, withShadow: true) { path.cgPath } + + let glassMargin: CGFloat = DefaultDimens.glassMargin * dimens.scale + let glassWidth: CGFloat = (dimens.windowRect.width - glassMargin * 3) / 2 + let glassHeight: CGFloat = dimens.windowRect.height - glassMargin * 2 + + let glassRect = CGRect( + origin: CGPoint(x: dimens.windowRect.minX + glassMargin, y: dimens.windowRect.minY + glassMargin), + size: CGSize(width: glassWidth, height: glassHeight) + ) + + drawGlass(context, glassRect) + drawGlass(context, glassRect.offsetBy(dx: glassWidth + glassMargin, dy: 0)) + } + + private func drawGlass(_ context: CGContext, _ glassRect: CGRect) { + context.saveGState() + + context.beginPath() + context.addRect(glassRect) + context.closePath() + context.clip() + + let colors = [colors.glassTop.cgColor, colors.glassBottom.cgColor] as CFArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + let colorLocations: [CGFloat] = [0.0, 1.0] + + let startPoint = CGPoint(x: 0, y: glassRect.minY) + let endPoint = CGPoint(x: 0, y: glassRect.maxY) + + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: colorLocations)! + + context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: []) + context.fillPath() + + context.restoreGState() + } + + private func drawAwning(_ context: CGContext) { + let position = windowState?.position.value ?? 0 + let awningLeft = (dimens.canvasRect.width - dimens.awningClosedWidth) / 2 + let awningTop = dimens.canvasRect.minY + 1 + let frontMinHeight = dimens.awningFrontHeight * 0.6 + + let deepByPosition = dimens.awninigMaxDepp * position / 100 + let widthDeltaByPosition = (dimens.awningOpenedWidth - dimens.awningClosedWidth) * position / 100 + let maxWidthByPosition = dimens.awningClosedWidth + widthDeltaByPosition + let maxWidthMarginByPosition = (dimens.canvasRect.width - maxWidthByPosition) / 2 + + let path = UIBezierPath() + path.move(to: CGPoint(x: awningLeft, y: awningTop)) + path.addLine(to: CGPoint(x: awningLeft + dimens.awningClosedWidth, y: awningTop)) + path.addLine(to: CGPoint(x: awningLeft + dimens.awningClosedWidth + widthDeltaByPosition / 2, y: awningTop + deepByPosition)) + path.addLine(to: CGPoint(x: awningLeft - widthDeltaByPosition / 2, y: awningTop + deepByPosition)) + path.close() + + drawPath(context, fillColor: colors.slatBackground) { path.cgPath } + drawPath(context, strokeColor: colors.slatBorder) { path.cgPath } + + let frontHeight = frontMinHeight + (dimens.awningFrontHeight - frontMinHeight) * position / 100 + let frontRect = CGRect( + origin: CGPoint(x: maxWidthMarginByPosition, y: deepByPosition), + size: CGSize(width: maxWidthByPosition, height: frontHeight) + ) + let frontPath = UIBezierPath(rect: frontRect) + + drawPath(context, fillColor: colors.slatBackground) { frontPath.cgPath } + drawPath(context, strokeColor: colors.slatBorder) { frontPath.cgPath } + } + + private func drawAwningLikeMarker(_ context: CGContext, position: CGFloat, withFront: Bool) { + let awningLeft = (dimens.canvasRect.width - dimens.awningClosedWidth) / 2 + let awningTop = dimens.canvasRect.minY + 1 + let frontMinHeight = dimens.awningFrontHeight * 0.6 + + let deepByPosition = dimens.awninigMaxDepp * position / 100 + let widthDeltaByPosition = (dimens.awningOpenedWidth - dimens.awningClosedWidth) * position / 100 + let maxWidthByPosition = dimens.awningClosedWidth + widthDeltaByPosition + let maxWidthMarginByPosition = (dimens.canvasRect.width - maxWidthByPosition) / 2 + + let path = UIBezierPath() + path.move(to: CGPoint(x: awningLeft, y: awningTop)) + path.addLine(to: CGPoint(x: awningLeft + dimens.awningClosedWidth, y: awningTop)) + path.addLine(to: CGPoint(x: awningLeft + dimens.awningClosedWidth + widthDeltaByPosition / 2, y: awningTop + deepByPosition)) + path.addLine(to: CGPoint(x: awningLeft - widthDeltaByPosition / 2, y: awningTop + deepByPosition)) + path.close() + + drawPath(context, fillColor: colors.slatBackground.copy(alpha: 0.06)) { path.cgPath } + drawPath(context, strokeColor: colors.slatBorder) { path.cgPath } + + if (withFront) { + let frontHeight = frontMinHeight + (dimens.awningFrontHeight - frontMinHeight) * position / 100 + let frontRect = CGRect( + origin: CGPoint(x: maxWidthMarginByPosition, y: deepByPosition), + size: CGSize(width: maxWidthByPosition, height: frontHeight) + ) + let frontPath = UIBezierPath(rect: frontRect) + + drawPath(context, fillColor: colors.slatBackground) { frontPath.cgPath } + drawPath(context, strokeColor: colors.slatBorder) { frontPath.cgPath } + } + } + + private func drawAwningShadow(_ context: CGContext, position: CGFloat) { + let shadowLeft = (dimens.canvasRect.width - dimens.awningClosedWidth) / 2 + let shadowTop = dimens.windowRect.maxY + let frontMinHeight = dimens.awningFrontHeight * 0.6 + + let deepByPosition = dimens.awninigMaxDepp * position / 100 + let widthDeltaByPosition = (dimens.awningOpenedWidth - dimens.awningClosedWidth) * position / 100 + let maxWidthByPosition = dimens.awningClosedWidth + widthDeltaByPosition + let maxWidthMarginByPosition = (dimens.canvasRect.width - maxWidthByPosition) / 2 + + let path = UIBezierPath() + path.move(to: CGPoint(x: shadowLeft, y: shadowTop)) + path.addLine(to: CGPoint(x: shadowLeft + dimens.awningClosedWidth, y: shadowTop)) + path.addLine(to: CGPoint(x: shadowLeft + dimens.awningClosedWidth + widthDeltaByPosition / 2, y: shadowTop + deepByPosition)) + path.addLine(to: CGPoint(x: shadowLeft - widthDeltaByPosition / 2, y: shadowTop + deepByPosition)) + path.close() + + drawPath(context, fillColor: colors.slatBackground.copy(alpha: 0.06)) { path.cgPath } + } + + override class var requiresConstraintBasedLayout: Bool { + return true + } +} + +private enum DefaultDimens { + static let width: CGFloat = 328 + static let height: CGFloat = 352 + static var ratio: CGFloat { width / height } + + static let windowWidth: CGFloat = 182 + static let windowHeight: CGFloat = 240 + static let windowTopDistance: CGFloat = 24 + + static let awningClosedWidth: CGFloat = 200 + static let awningOpenedWidth: CGFloat = 326 + static let awningMaxDepp: CGFloat = 88 + static let awningFrontHeight: CGFloat = 24 + + static let glassMargin: CGFloat = 14 +} + +private class RuntimeDimens { + var scale: CGFloat = 1 + + var canvasRect: CGRect = .zero + var windowRect: CGRect = .zero + var touchRect: CGRect = .zero + var awningClosedWidth: CGFloat = 0 + var awningOpenedWidth: CGFloat = 0 + var awninigMaxDepp: CGFloat = 0 + var awningFrontHeight: CGFloat = 0 + + func update(_ frame: CGRect) { + createCanvasRect(frame) + scale = canvasRect.width / DefaultDimens.width + createWindowRect() + touchRect = CGRect(x: windowRect.minX, y: canvasRect.minY, width: windowRect.width, height: windowRect.height) + awningClosedWidth = DefaultDimens.awningClosedWidth * scale + awningOpenedWidth = DefaultDimens.awningOpenedWidth * scale + awninigMaxDepp = DefaultDimens.awningMaxDepp * scale + awningFrontHeight = DefaultDimens.awningFrontHeight * scale + } + + private func createCanvasRect(_ frame: CGRect) { + let size = getSize(frame) + canvasRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) + } + + private func createWindowRect() { + let windowWidth = DefaultDimens.windowWidth * scale + let windowHeight = DefaultDimens.windowHeight * scale + let windowTop = DefaultDimens.windowTopDistance * scale + let windowLeft = (canvasRect.width - windowWidth) / 2 + + windowRect = CGRect( + origin: CGPoint(x: windowLeft, y: windowTop), + size: CGSize(width: windowWidth, height: windowHeight) + ) + } + + private func getSize(_ frame: CGRect) -> CGSize { + let canvasRatio = frame.width / frame.height + if (canvasRatio > DefaultDimens.ratio) { + return CGSize(width: frame.height * DefaultDimens.ratio, height: frame.height) + } else { + return CGSize(width: frame.width, height: frame.width / DefaultDimens.ratio) + } + } +} diff --git a/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVC.swift b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVC.swift new file mode 100644 index 00000000..d98411a8 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVC.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 TerraceAwningVC: BaseWindowVC { + init(itemBundle: ItemBundle) { + super.init(itemBundle: itemBundle, viewModel: TerraceAwningVM()) + } + + override func getWindowView() -> TerraceAwningView { TerraceAwningView() } + + override func handle(state: TerraceAwningViewState) { + windowView.windowState = state.terraceAwningWindowState + + slatTiltSlider.isHidden = true + topView.valueBottom = nil + + super.handle(state: state) + } +} diff --git a/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.swift new file mode 100644 index 00000000..013866a0 --- /dev/null +++ b/SUPLA/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVM.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 TerraceAwningVM: BaseWindowVM { + override func defaultViewState() -> TerraceAwningViewState { TerraceAwningViewState() } + + 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.terraceAwningWindowState + .changing(path: \.position, to: positionValue) + .changing(path: \.positionTextFormat, to: positionTextFormat) + + return updateChannel($0, channel, value) { + $0.changing(path: \.terraceAwningWindowState, to: windowState) + } + } + } + + override func handleGroup(_ group: SAChannelGroup, _ onlineSummary: GroupOnlineSummary) { + updateView { + if ($0.manualMoving) { + return $0 + } + + let positions = group.getRollerShutterPositions() + let overallPosition = getGroupPercentage(positions, !$0.terraceAwningWindowState.markers.isEmpty) + let windowState = $0.terraceAwningWindowState + .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: \.terraceAwningWindowState, to: windowState) + .changing(path: \.positionUnknown, to: overallPosition == .invalid) + } + } + } +} + +struct TerraceAwningViewState: BaseWindowViewState { + var remoteId: Int32? = nil + var terraceAwningWindowState: TerraceAwningWindowState = .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 { terraceAwningWindowState } +} + +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? RollerShutterGroupValue else { return nil } + + return if (value.position < 100 && value.closedSensorActive) { + CGFloat(100) + } else { + CGFloat(value.position) + } + } +} diff --git a/SUPLA/Resources/Default.strings b/SUPLA/Resources/Default.strings index e06cccb1..74442665 100644 --- a/SUPLA/Resources/Default.strings +++ b/SUPLA/Resources/Default.strings @@ -66,6 +66,7 @@ "channel_caption_general_purpose_measurment" = "Measurement channel"; "channel_caption_general_purpose_meter" = "Meter channel"; "channel_caption_facade_blinds" = "Facade blinds"; +"channel_caption_terrace_awning" = "Terrace awning"; /* 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 f4dac4a7..69d6ee34 100644 --- a/SUPLA/Resources/Extensions/String+Icons.swift +++ b/SUPLA/Resources/Extensions/String+Icons.swift @@ -128,6 +128,8 @@ extension String { static let fncThermostatHeat = "fnc_thermostat_heat" static let fncThermostatCool = "fnc_thermostat_cool" static let fncThermostatDhw = "fnc_thermostat_dhw" + // ShadingSystems + static let fncTerraceAwning = "fnc_terrace_awning" // MARK: other static let thumbHeat = "thumb_heat" diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json new file mode 100644 index 00000000..bac2850e --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "fnc_terrase_awning-open.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "fnc_terrase_awning-open-nm.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg new file mode 100644 index 00000000..4954baf8 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open-nm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg new file mode 100644 index 00000000..9230eed5 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-closed.imageset/fnc_terrase_awning-open.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json new file mode 100644 index 00000000..326b3cca --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "fnc_terrase_awning-closed.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "fnc_terrase_awning-closed-nm.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg new file mode 100644 index 00000000..4d0aa391 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed-nm.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg new file mode 100644 index 00000000..3a0e0683 --- /dev/null +++ b/SUPLA/Resources/Resources.xcassets/Images/FunctionIcons/fnc_terrace_awning-open.imageset/fnc_terrase_awning-closed.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SUPLA/Resources/Strings.swift b/SUPLA/Resources/Strings.swift index 58c6b028..97a01483 100644 --- a/SUPLA/Resources/Strings.swift +++ b/SUPLA/Resources/Strings.swift @@ -303,6 +303,7 @@ struct Strings { static let captionGeneralPurposeMeasurement = "channel_caption_general_purpose_measurment".toLocalized() static let captionGeneralPurposeMeter = "channel_caption_general_purpose_meter".toLocalized() static let captionFacadeBlinds = "channel_caption_facade_blinds".toLocalized() + static let captionTerraceAwning = "channel_caption_terrace_awning".toLocalized() } } diff --git a/SUPLA/Resources/de.lproj/Localizable.strings b/SUPLA/Resources/de.lproj/Localizable.strings index 6d152ae3..d0260529 100644 --- a/SUPLA/Resources/de.lproj/Localizable.strings +++ b/SUPLA/Resources/de.lproj/Localizable.strings @@ -323,6 +323,7 @@ "channel_caption_general_purpose_measurment" = "Messungskanal"; "channel_caption_general_purpose_meter" = "Zählerkanal"; "channel_caption_facade_blinds" = "Fassadenjalousien"; +"channel_caption_terrace_awning" = "Terrassenmarkise"; /* 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 4fea3d3e..f3338df0 100644 --- a/SUPLA/Resources/pl.lproj/Localizable.strings +++ b/SUPLA/Resources/pl.lproj/Localizable.strings @@ -348,6 +348,7 @@ "channel_caption_general_purpose_measurment" = "Kanał pomiarowy"; "channel_caption_general_purpose_meter" = "Kanał licznikowy"; "channel_caption_facade_blinds" = "Żaluzja fasadowa"; +"channel_caption_terrace_awning" = "Markiza tarasowa"; /* 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/SAChannel+CoreDataClass.m b/SUPLA/SAChannel+CoreDataClass.m index d14301bd..763446db 100644 --- a/SUPLA/SAChannel+CoreDataClass.m +++ b/SUPLA/SAChannel+CoreDataClass.m @@ -182,7 +182,8 @@ - (int) imgIsActive { if ( [self isOnline] && (self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER - || self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW) + || self.func == SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW + || self.func == SUPLA_CHANNELFNC_TERRACE_AWNING) && self.rollerShutterValue.position >= 100) { return 1; } diff --git a/SUPLA/SAChannelBase+CoreDataClass.m b/SUPLA/SAChannelBase+CoreDataClass.m index a9d4ecc9..dc5e8324 100644 --- a/SUPLA/SAChannelBase+CoreDataClass.m +++ b/SUPLA/SAChannelBase+CoreDataClass.m @@ -367,6 +367,7 @@ - (int) imgIsActive { case SUPLA_CHANNELFNC_CONTROLLINGTHEROLLERSHUTTER: case SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW: case SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND: + case SUPLA_CHANNELFNC_TERRACE_AWNING: return [self hiSubValue]; case SUPLA_CHANNELFNC_OPENINGSENSOR_GATEWAY: diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift index 9db4156c..57a9f64f 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCase.swift @@ -113,6 +113,8 @@ final class GetChannelBaseDefaultCaptionUseCaseImpl: GetChannelBaseDefaultCaptio return Strings.General.Channel.captionGeneralPurposeMeter case SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND: return Strings.General.Channel.captionFacadeBlinds + case SUPLA_CHANNELFNC_TERRACE_AWNING: + return Strings.General.Channel.captionTerraceAwning default: return NSLocalizedString("Not supported function", comment: "") } diff --git a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift index c5d2b578..3697f4cc 100644 --- a/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift +++ b/SUPLA/UseCase/ChannelBase/GetChannelBaseStateUseCase.swift @@ -50,7 +50,8 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { SUPLA_CHANNELFNC_VALVE_OPENCLOSE, SUPLA_CHANNELFNC_VALVE_PERCENTAGE, SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, - SUPLA_CHANNELFNC_CONTROLLINGTHEGATEWAYLOCK: + SUPLA_CHANNELFNC_CONTROLLINGTHEGATEWAYLOCK, + SUPLA_CHANNELFNC_TERRACE_AWNING: if (activeValue != 0) { return .closed } else { @@ -102,6 +103,7 @@ final class GetChannelBaseStateUseCaseImpl: GetChannelBaseStateUseCase { SUPLA_CHANNELFNC_OPENINGSENSOR_ROLLERSHUTTER, SUPLA_CHANNELFNC_OPENINGSENSOR_WINDOW, SUPLA_CHANNELFNC_OPENINGSENSOR_ROOFWINDOW, + SUPLA_CHANNELFNC_TERRACE_AWNING, SUPLA_CHANNELFNC_VALVE_OPENCLOSE, SUPLA_CHANNELFNC_VALVE_PERCENTAGE: .opened case SUPLA_CHANNELFNC_POWERSWITCH, diff --git a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift index 66ee646a..b72d393a 100644 --- a/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift +++ b/SUPLA/UseCase/Detail/ProvideDetailTypeUseCase.swift @@ -36,6 +36,8 @@ final class ProvideDetailTypeUseCaseImpl: ProvideDetailTypeUseCase { return .windowDetail(pages: [.rollerShutter]) case SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND: return .windowDetail(pages: [.facadeBlind]) + case SUPLA_CHANNELFNC_TERRACE_AWNING: + return .windowDetail(pages: [.terraceAwning]) case SUPLA_CHANNELFNC_LIGHTSWITCH, SUPLA_CHANNELFNC_POWERSWITCH, @@ -133,4 +135,5 @@ enum DetailPage { case rollerShutter case roofWindow case facadeBlind + case terraceAwning } diff --git a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift index 616c8c1f..ef93fe7f 100644 --- a/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift +++ b/SUPLA/UseCase/Icon/GetDefaultIconNameUseCase.swift @@ -76,6 +76,7 @@ final class GetDefaultIconNameUseCaseImpl: GetDefaultIconNameUseCase { StaticIconNameProducer(function: SUPLA_CHANNELFNC_OPENINGSENSOR_ROOFWINDOW, name: "roofwindow"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_CONTROLLINGTHEROOFWINDOW, name: "roofwindow"), StaticIconNameProducer(function: SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, name: "fnc_facade_blind"), + StaticIconNameProducer(function: SUPLA_CHANNELFNC_TERRACE_AWNING, name: "fnc_terrace_awning"), PowerSwitchIconNameProducer(), LightSwitchIconNameProducer(), StaircaseTimerIconNameProducer(), diff --git a/SUPLATests/Tests/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVMTests.swift b/SUPLATests/Tests/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVMTests.swift new file mode 100644 index 00000000..55096a5b --- /dev/null +++ b/SUPLATests/Tests/Features/Details/WindowDetail/TerraceAwning/TerraceAwningVMTests.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 TerraceAwningVMTests: 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: TerraceAwningVM! = TerraceAwningVM() + + 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: [ + TerraceAwningViewState(), + TerraceAwningViewState( + remoteId: 123, + terraceAwningWindowState: TerraceAwningWindowState(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: [ + RollerShutterGroupValue(position: 50, openSensorActive: false), + RollerShutterGroupValue(position: 80, openSensorActive: false) + ]) + + settings.showOpeningPercentReturns = true + readGroupByRemoteIdUseCase.returns = .just(group) + getGroupOnlineSummaryUseCase.returns = .just(groupOnlineSummary) + + // when + observe(viewModel) + viewModel.loadData(remoteId: 234, type: .group) + + // then + assertStates(expected: [ + TerraceAwningViewState(), + TerraceAwningViewState( + remoteId: 234, + terraceAwningWindowState: TerraceAwningWindowState( + position: .different(min: 50, max: 80), + positionTextFormat: .openingPercentage, + markers: [50, 80] + ), + offline: false, + isGroup: true, + onlineStatusString: "2/3" + ) + ]) + } +} + diff --git a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift index cdf301e1..eb25f76d 100644 --- a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseDefaultCaptionUseCaseTests.swift @@ -83,6 +83,7 @@ final class GetChannelBaseDefaultCaptionUseCaseTests: XCTestCase { doTest(function: SUPLA_CHANNELFNC_GENERAL_PURPOSE_MEASUREMENT, Strings.General.Channel.captionGeneralPurposeMeasurement) doTest(function: SUPLA_CHANNELFNC_GENERAL_PURPOSE_METER, Strings.General.Channel.captionGeneralPurposeMeter) doTest(function: SUPLA_CHANNELFNC_CONTROLLINGTHEFACADEBLIND, Strings.General.Channel.captionFacadeBlinds) + doTest(function: SUPLA_CHANNELFNC_TERRACE_AWNING, Strings.General.Channel.captionTerraceAwning) doTest(function: -1, "Not supported function") } diff --git a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift index d0fa2a99..dcfd4685 100644 --- a/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/ChannelBase/GetChannelBaseStateUseCaseTests.swift @@ -296,4 +296,43 @@ final class GetChannelBaseStateUseCaseTests: XCTestCase { XCTAssertEqual(state, .opaque) XCTAssertFalse(state.isActive()) } + + func test_terraceAwningClosedState() { + // given + let function = SUPLA_CHANNELFNC_TERRACE_AWNING + let activeValue: Int32 = 1 + + // when + let state = useCase.invoke(function: function, online: true, activeValue: activeValue) + + // then + XCTAssertEqual(state, .closed) + XCTAssertTrue(state.isActive()) + } + + func test_terraceAwningOpenedState() { + // given + let function = SUPLA_CHANNELFNC_TERRACE_AWNING + let activeValue: Int32 = 0 + + // when + let state = useCase.invoke(function: function, online: true, activeValue: activeValue) + + // then + XCTAssertEqual(state, .opened) + XCTAssertFalse(state.isActive()) + } + + func test_terraceAwningOfflineState() { + // given + let function = SUPLA_CHANNELFNC_TERRACE_AWNING + let activeValue: Int32 = 0 + + // when + let state = useCase.invoke(function: function, online: false, activeValue: activeValue) + + // then + XCTAssertEqual(state, .opened) + XCTAssertFalse(state.isActive()) + } } diff --git a/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift b/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift index ffc66f8e..481052ac 100644 --- a/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Detail/ProvideDetailTypeUseCaseTests.swift @@ -82,6 +82,15 @@ final class ProvideDetailTypeUseCaseTests: XCTestCase { } } + func test_shouldProvideRs_forTerraceAwningFunction() { + doTest(expectedResult: .windowDetail(pages: [.terraceAwning])) { + let channel = SAChannel(testContext: nil) + channel.func = SUPLA_CHANNELFNC_TERRACE_AWNING + + return channel + } + } + func test_shouldProvideEm_forEmFunction() { doTest(expectedResult: .legacy(type: .em)) { let channel = SAChannel(testContext: nil) diff --git a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift index 8992342b..ac4e6ac4 100644 --- a/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift +++ b/SUPLATests/Tests/UseCase/Icon/GetDefaultIconNameUseCaseTests.swift @@ -1579,4 +1579,42 @@ final class GetDefaultIconNameUseCaseTests: XCTestCase { // then XCTAssertEqual(iconName, "fnc_alarm_armament-off") } + + func test_terraceAwningOpened() { + // given + let function = SUPLA_CHANNELFNC_TERRACE_AWNING + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .opened, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_terrace_awning-open") + } + + func test_terraceAwningClose() { + // given + let function = SUPLA_CHANNELFNC_TERRACE_AWNING + + // when + let iconName = useCase.invoke( + iconData: IconData( + function: function, + altIcon: 0, + state: .closed, + type: .single, + subfunction: .notSet + ) + ) + + // then + XCTAssertEqual(iconName, "fnc_terrace_awning-closed") + } }