diff --git a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml
index 95e9346d..f0379c95 100644
--- a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml
+++ b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.22.2' # Specify the Flutter version you want to use
+ flutter-version: '3.27.3' # Specify the Flutter version you want to use
- name: Install dependencies
run: flutter pub get
diff --git a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml
index e28d1bb2..28cf00a2 100644
--- a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml
+++ b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml
@@ -25,7 +25,7 @@ jobs:
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.22.2' # Specify the Flutter version you want to use
+ flutter-version: '3.27.3' # Specify the Flutter version you want to use
- name: Install dependencies
run: flutter pub get
diff --git a/assets/icons/boundary.svg b/assets/icons/boundary.svg
new file mode 100644
index 00000000..73b4ab24
--- /dev/null
+++ b/assets/icons/boundary.svg
@@ -0,0 +1,28 @@
+
diff --git a/assets/icons/close_to_motion.svg b/assets/icons/close_to_motion.svg
new file mode 100644
index 00000000..8ba6c8c6
--- /dev/null
+++ b/assets/icons/close_to_motion.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/communication_fault.svg b/assets/icons/communication_fault.svg
new file mode 100644
index 00000000..e2ab1b40
--- /dev/null
+++ b/assets/icons/communication_fault.svg
@@ -0,0 +1,22 @@
+
diff --git a/assets/icons/cps_custom_mode.svg b/assets/icons/cps_custom_mode.svg
new file mode 100644
index 00000000..4176c939
--- /dev/null
+++ b/assets/icons/cps_custom_mode.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/icons/cps_mode1.svg b/assets/icons/cps_mode1.svg
new file mode 100644
index 00000000..407105df
--- /dev/null
+++ b/assets/icons/cps_mode1.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/cps_mode2.svg b/assets/icons/cps_mode2.svg
new file mode 100644
index 00000000..9464a7ea
--- /dev/null
+++ b/assets/icons/cps_mode2.svg
@@ -0,0 +1,7 @@
+
diff --git a/assets/icons/cps_mode3.svg b/assets/icons/cps_mode3.svg
new file mode 100644
index 00000000..998329fd
--- /dev/null
+++ b/assets/icons/cps_mode3.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/cps_mode4.svg b/assets/icons/cps_mode4.svg
new file mode 100644
index 00000000..3136a806
--- /dev/null
+++ b/assets/icons/cps_mode4.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/far_away_motion.svg b/assets/icons/far_away_motion.svg
new file mode 100644
index 00000000..9458eb0d
--- /dev/null
+++ b/assets/icons/far_away_motion.svg
@@ -0,0 +1,11 @@
+
diff --git a/assets/icons/motion_meter.svg b/assets/icons/motion_meter.svg
new file mode 100644
index 00000000..85469973
--- /dev/null
+++ b/assets/icons/motion_meter.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/moving_speed.svg b/assets/icons/moving_speed.svg
new file mode 100644
index 00000000..6db52050
--- /dev/null
+++ b/assets/icons/moving_speed.svg
@@ -0,0 +1,9 @@
+
diff --git a/assets/icons/presence_judgement_threshold.svg b/assets/icons/presence_judgement_threshold.svg
new file mode 100644
index 00000000..d5537da7
--- /dev/null
+++ b/assets/icons/presence_judgement_threshold.svg
@@ -0,0 +1,15 @@
+
diff --git a/assets/icons/radar_fault.svg b/assets/icons/radar_fault.svg
new file mode 100644
index 00000000..b2295d1a
--- /dev/null
+++ b/assets/icons/radar_fault.svg
@@ -0,0 +1,14 @@
+
diff --git a/assets/icons/self_testing_failure.svg b/assets/icons/self_testing_failure.svg
new file mode 100644
index 00000000..c86c9ec2
--- /dev/null
+++ b/assets/icons/self_testing_failure.svg
@@ -0,0 +1,23 @@
+
diff --git a/assets/icons/self_testing_success.svg b/assets/icons/self_testing_success.svg
new file mode 100644
index 00000000..1f8976b0
--- /dev/null
+++ b/assets/icons/self_testing_success.svg
@@ -0,0 +1,23 @@
+
diff --git a/assets/icons/self_testing_timeout.svg b/assets/icons/self_testing_timeout.svg
new file mode 100644
index 00000000..55c1e632
--- /dev/null
+++ b/assets/icons/self_testing_timeout.svg
@@ -0,0 +1,41 @@
+
diff --git a/assets/icons/sensitivity_feature_1.svg b/assets/icons/sensitivity_feature_1.svg
new file mode 100644
index 00000000..21bebd7a
--- /dev/null
+++ b/assets/icons/sensitivity_feature_1.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_2.svg b/assets/icons/sensitivity_feature_2.svg
new file mode 100644
index 00000000..7370ab6f
--- /dev/null
+++ b/assets/icons/sensitivity_feature_2.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_3.svg b/assets/icons/sensitivity_feature_3.svg
new file mode 100644
index 00000000..23b92c43
--- /dev/null
+++ b/assets/icons/sensitivity_feature_3.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_4.svg b/assets/icons/sensitivity_feature_4.svg
new file mode 100644
index 00000000..7a92045f
--- /dev/null
+++ b/assets/icons/sensitivity_feature_4.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_5.svg b/assets/icons/sensitivity_feature_5.svg
new file mode 100644
index 00000000..5f056602
--- /dev/null
+++ b/assets/icons/sensitivity_feature_5.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_6.svg b/assets/icons/sensitivity_feature_6.svg
new file mode 100644
index 00000000..288b172e
--- /dev/null
+++ b/assets/icons/sensitivity_feature_6.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_7.svg b/assets/icons/sensitivity_feature_7.svg
new file mode 100644
index 00000000..5779dfd2
--- /dev/null
+++ b/assets/icons/sensitivity_feature_7.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_8.svg b/assets/icons/sensitivity_feature_8.svg
new file mode 100644
index 00000000..4816b1ba
--- /dev/null
+++ b/assets/icons/sensitivity_feature_8.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/sensitivity_feature_9.svg b/assets/icons/sensitivity_feature_9.svg
new file mode 100644
index 00000000..978145b4
--- /dev/null
+++ b/assets/icons/sensitivity_feature_9.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/space_type.svg b/assets/icons/space_type.svg
new file mode 100644
index 00000000..af5f6845
--- /dev/null
+++ b/assets/icons/space_type.svg
@@ -0,0 +1,10 @@
+
diff --git a/assets/icons/spatial_motion_value.svg b/assets/icons/spatial_motion_value.svg
new file mode 100644
index 00000000..018da674
--- /dev/null
+++ b/assets/icons/spatial_motion_value.svg
@@ -0,0 +1,18 @@
+
diff --git a/assets/icons/spatial_static_value.svg b/assets/icons/spatial_static_value.svg
new file mode 100644
index 00000000..95ca6112
--- /dev/null
+++ b/assets/icons/spatial_static_value.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/sports_para.svg b/assets/icons/sports_para.svg
new file mode 100644
index 00000000..6f9d5ece
--- /dev/null
+++ b/assets/icons/sports_para.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart
index 0ac3f776..d07efc9b 100644
--- a/lib/pages/device_managment/all_devices/models/devices_model.dart
+++ b/lib/pages/device_managment/all_devices/models/devices_model.dart
@@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switc
import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gateway.dart';
import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/enum/device_types.dart';
@@ -317,6 +318,11 @@ SOS
type: 'BOTH',
),
];
+ case 'CPS':
+ return CeilingSensorHelper.getCeilingSensorFunctions(
+ uuid: uuid ?? '',
+ name: name ?? '',
+ );
default:
return [];
}
diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart
index 8f7cd1c5..b1afbc12 100644
--- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart
+++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart';
@@ -98,6 +99,15 @@ class DeviceDialogHelper {
deviceSelectedFunctions: deviceSelectedFunctions,
uniqueCustomId: data['uniqueCustomId'],
removeComparetors: removeComparetors);
+ case 'CPS':
+ return CeilingSensorHelper.showCeilingSensorDialog(
+ context: context,
+ functions: functions,
+ device: data['device'],
+ deviceSelectedFunctions: deviceSelectedFunctions,
+ uniqueCustomId: data['uniqueCustomId'],
+ dialogType: dialogType,
+ );
case 'GW':
return GatewayHelper.showGatewayFunctionsDialog(
context: context,
diff --git a/lib/pages/routines/models/ceiling_presence_sensor_functions.dart b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart
new file mode 100644
index 00000000..6dbe5cf6
--- /dev/null
+++ b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart
@@ -0,0 +1,889 @@
+import 'package:syncrow_web/pages/routines/models/device_functions.dart';
+import 'package:syncrow_web/utils/constants/assets.dart';
+
+class CpsOperationalValue {
+ final String icon;
+ final String description;
+ final dynamic value;
+
+ CpsOperationalValue({
+ required this.icon,
+ required this.description,
+ required this.value,
+ });
+}
+
+abstract class CpsFunctions extends DeviceFunction {
+ CpsFunctions({
+ required super.deviceId,
+ required super.deviceName,
+ required super.code,
+ required super.operationName,
+ required super.icon,
+ required this.type,
+ });
+
+ final String type;
+
+ List getOperationalValues();
+}
+
+final class CpsRadarSwitchFunction extends CpsFunctions {
+ CpsRadarSwitchFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'radar_switch',
+ operationName: 'Radar Switch',
+ icon: Assets.acPower,
+ );
+
+ @override
+ List getOperationalValues() => [
+ CpsOperationalValue(
+ icon: Assets.assetsAcPower,
+ description: "ON",
+ value: true,
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsAcPowerOFF,
+ description: "OFF",
+ value: false,
+ ),
+ ];
+}
+
+final class CpsSpatialParameterSwitchFunction extends CpsFunctions {
+ CpsSpatialParameterSwitchFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'space_para_switch',
+ operationName: 'Spatial Parameter Switch',
+ icon: Assets.acPower,
+ );
+
+ @override
+ List getOperationalValues() => [
+ CpsOperationalValue(
+ icon: Assets.assetsAcPower,
+ description: "ON",
+ value: true,
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsAcPowerOFF,
+ description: "OFF",
+ value: false,
+ ),
+ ];
+}
+
+final class CpsSensitivityFunction extends CpsFunctions {
+ CpsSensitivityFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 1,
+ max = 10,
+ step = 1,
+ super(
+ code: 'sensitivity',
+ operationName: 'Sensitivity',
+ icon: Assets.sensitivity,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ static const _images = [
+ Assets.sensitivityFeature1,
+ Assets.sensitivityFeature1,
+ Assets.sensitivityFeature2,
+ Assets.sensitivityFeature3,
+ Assets.sensitivityFeature4,
+ Assets.sensitivityFeature5,
+ Assets.sensitivityFeature6,
+ Assets.sensitivityFeature7,
+ Assets.sensitivityFeature8,
+ Assets.sensitivityFeature9,
+ Assets.sensitivityFeature9,
+ ];
+
+ @override
+ List getOperationalValues() {
+ final values = [];
+ for (var value = min; value <= max; value += step) {
+ values.add(
+ CpsOperationalValue(
+ icon: _images[value],
+ description: '$value',
+ value: value,
+ ),
+ );
+ }
+ return values;
+ }
+}
+
+final class CpsMovingSpeedFunction extends CpsFunctions {
+ CpsMovingSpeedFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0,
+ max = 32,
+ step = 1,
+ super(
+ code: 'moving_speed',
+ operationName: 'Moving Speed',
+ icon: Assets.speedoMeter,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ @override
+ List getOperationalValues() {
+ return List.generate(
+ (max - min) ~/ step + 1,
+ (index) => CpsOperationalValue(
+ icon: Assets.speedoMeter,
+ description: '${min + (index * step)}',
+ value: min + (index * step),
+ ),
+ );
+ }
+}
+
+final class CpsSpatialStaticValueFunction extends CpsFunctions {
+ CpsSpatialStaticValueFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0,
+ max = 255,
+ step = 1,
+ super(
+ code: 'space_static_val',
+ operationName: 'Spacial Static Value',
+ icon: Assets.spatialStaticValue,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ @override
+ List getOperationalValues() {
+ return List.generate(
+ (max - min) ~/ step + 1,
+ (index) => CpsOperationalValue(
+ icon: Assets.spatialStaticValue,
+ description: '${min + (index * step)}',
+ value: min + (index * step),
+ ),
+ );
+ }
+}
+
+final class CpsSpatialMotionValueFunction extends CpsFunctions {
+ CpsSpatialMotionValueFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0,
+ max = 255,
+ step = 1,
+ super(
+ code: 'space_move_val',
+ operationName: 'Spatial Motion Value',
+ icon: Assets.spatialMotionValue,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ @override
+ List getOperationalValues() {
+ return List.generate(
+ (max - min) ~/ step + 1,
+ (index) => CpsOperationalValue(
+ icon: Assets.spatialMotionValue,
+ description: '${min + (index * step)}',
+ value: min + (index * step),
+ ),
+ );
+ }
+}
+
+final class CpsMaxDistanceOfDetectionFunction extends CpsFunctions {
+ CpsMaxDistanceOfDetectionFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 10.0,
+ step = 0.5,
+ super(
+ code: 'moving_max_dis',
+ operationName: 'Maximum Distance Of Detection',
+ icon: Assets.currentDistanceIcon,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.currentDistanceIcon,
+ description: '${value.toStringAsFixed(1)} M',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsMaxDistanceOfStaticDetectionFunction extends CpsFunctions {
+ CpsMaxDistanceOfStaticDetectionFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 10.0,
+ step = 0.5,
+ super(
+ code: 'static_max_dis',
+ operationName: 'Maximum Distance Of Static Detection',
+ icon: Assets.currentDistanceIcon,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.currentDistanceIcon,
+ description: '${value.toStringAsFixed(1)} M',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsDetectionRangeFunction extends CpsFunctions {
+ CpsDetectionRangeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 25.5,
+ step = 0.1,
+ super(
+ code: 'moving_range',
+ operationName: 'Detection Range',
+ icon: Assets.farDetection,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.farDetection,
+ description: '${value.toStringAsFixed(1)} M',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsDistanceOfMovingObjectsFunction extends CpsFunctions {
+ CpsDistanceOfMovingObjectsFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 25.5,
+ step = 0.1,
+ super(
+ code: 'presence_range',
+ operationName: 'Distance Of Moving Objects',
+ icon: Assets.currentDistanceIcon,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.currentDistanceIcon,
+ description: '${value.toStringAsFixed(1)} M',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsPresenceJudgementThrsholdFunction extends CpsFunctions {
+ CpsPresenceJudgementThrsholdFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0,
+ max = 255,
+ step = 5,
+ super(
+ code: 'presence_reference',
+ operationName: 'Presence Judgement Threshold',
+ icon: Assets.presenceJudgementThrshold,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ @override
+ List getOperationalValues() {
+ return List.generate(
+ (max - min) ~/ step + 1,
+ (index) => CpsOperationalValue(
+ icon: Assets.presenceJudgementThrshold,
+ description: '${min + (index * step)}',
+ value: min + (index * step),
+ ),
+ );
+ }
+}
+
+final class CpsMotionAmplitudeTriggerThresholdFunction extends CpsFunctions {
+ CpsMotionAmplitudeTriggerThresholdFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0,
+ max = 255,
+ step = 5,
+ super(
+ code: 'moving_reference',
+ operationName: 'Motion Amplitude Trigger Threshold',
+ icon: Assets.presenceJudgementThrshold,
+ );
+
+ final int min;
+ final int max;
+ final int step;
+
+ @override
+ List getOperationalValues() {
+ return List.generate(
+ (max - min) ~/ step + 1,
+ (index) => CpsOperationalValue(
+ icon: Assets.presenceJudgementThrshold,
+ description: '${min + (index * step)}',
+ value: min + (index * step),
+ ),
+ );
+ }
+}
+
+final class CpsPerpetualBoundaryFunction extends CpsFunctions {
+ CpsPerpetualBoundaryFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.00,
+ max = 5.00,
+ step = 0.50,
+ super(
+ code: 'perceptual_boundary',
+ operationName: 'Perpetual Boundary',
+ icon: Assets.boundary,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.boundary,
+ description: '${value.toStringAsFixed(1)}M',
+ value: value + 1200,
+ );
+ },
+ );
+ }
+}
+
+final class CpsMotionTriggerBoundaryFunction extends CpsFunctions {
+ CpsMotionTriggerBoundaryFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 5.0,
+ step = 0.5,
+ super(
+ code: 'moving_boundary',
+ operationName: 'Motion Trigger Boundary',
+ icon: Assets.motionMeter,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.motionMeter,
+ description: '${value.toStringAsFixed(1)} M',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsMotionTriggerTimeFunction extends CpsFunctions {
+ CpsMotionTriggerTimeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 2.0,
+ step = 0.1,
+ super(
+ code: 'moving_rigger_time',
+ operationName: 'Motion Trigger Time',
+ icon: Assets.motionMeter,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.motionMeter,
+ description: '${value.toStringAsFixed(3)} sec',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsMotionToStaticTimeFunction extends CpsFunctions {
+ CpsMotionToStaticTimeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 50.0,
+ step = 1.0,
+ super(
+ code: 'moving_static_time',
+ operationName: 'Motion To Static Time',
+ icon: Assets.motionMeter,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.motionMeter,
+ description: '${value.toStringAsFixed(0)} sec',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsEnteringNoBodyStateTimeFunction extends CpsFunctions {
+ CpsEnteringNoBodyStateTimeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 0.0,
+ max = 300.0,
+ step = 5.0,
+ super(
+ code: 'none_body_time',
+ operationName: 'Entering Nobody State Time',
+ icon: Assets.motionMeter,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.motionMeter,
+ description: '${value.toStringAsFixed(0)} sec',
+ value: value,
+ );
+ },
+ );
+ }
+}
+
+final class CpsSelfTestResultFunctions extends CpsFunctions {
+ CpsSelfTestResultFunctions({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'checking_result',
+ operationName: 'Self-Test Result',
+ icon: Assets.selfTestResult,
+ );
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ description: 'Self Testing',
+ icon: Assets.selfTestResult,
+ value: 'check',
+ ),
+ CpsOperationalValue(
+ description: 'Self Testing Success',
+ icon: Assets.selfTestingSuccess,
+ value: 'check_success',
+ ),
+ CpsOperationalValue(
+ description: 'Self Testing Failure',
+ icon: Assets.selfTestingFailure,
+ value: 'check_failure',
+ ),
+ CpsOperationalValue(
+ description: 'Self Testing Timeout',
+ icon: Assets.selfTestingTimeout,
+ value: 'check_timeout',
+ ),
+ CpsOperationalValue(
+ description: 'Communication Fault',
+ icon: Assets.communicationFault,
+ value: 'communication_fault',
+ ),
+ CpsOperationalValue(
+ description: 'Radar Fault',
+ icon: Assets.radarFault,
+ value: 'radar_fault',
+ ),
+ ];
+ }
+}
+
+final class CpsNobodyTimeFunction extends CpsFunctions {
+ CpsNobodyTimeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'nobody_time',
+ operationName: 'Entering Nobody Time',
+ icon: Assets.assetsNobodyTime,
+ );
+
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: 'None',
+ value: 'none',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '10sec',
+ value: '10s',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '30sec',
+ value: '30s',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '1min',
+ value: '1min',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '2min',
+ value: '2min',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '5min',
+ value: '5min',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '10min',
+ value: '10min',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '30min',
+ value: '30min',
+ ),
+ CpsOperationalValue(
+ icon: Assets.assetsNobodyTime,
+ description: '1hour',
+ value: '1hr',
+ ),
+ ];
+ }
+}
+
+final class CpsMovementFunctions extends CpsFunctions {
+ CpsMovementFunctions({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'body_movement',
+ operationName: 'Movement',
+ icon: Assets.motion,
+ );
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ description: 'None',
+ icon: Assets.nobodyTime,
+ value: 'none',
+ ),
+ CpsOperationalValue(
+ description: 'Close To',
+ icon: Assets.closeToMotion,
+ value: 'close_to',
+ ),
+ CpsOperationalValue(
+ description: 'Far Away',
+ icon: Assets.farAwayMotion,
+ value: 'far_away',
+ ),
+ ];
+ }
+}
+
+final class CpsCustomModeFunction extends CpsFunctions {
+ CpsCustomModeFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'custom_mode',
+ operationName: 'Custom Mode',
+ icon: Assets.cpsCustomMode,
+ );
+
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ icon: Assets.cpsMode1,
+ description: 'Mode 1',
+ value: 'mode1',
+ ),
+ CpsOperationalValue(
+ icon: Assets.cpsMode2,
+ description: 'Mode 2',
+ value: 'mode2',
+ ),
+ CpsOperationalValue(
+ icon: Assets.cpsMode3,
+ description: 'Mode 3',
+ value: 'mode3',
+ ),
+ CpsOperationalValue(
+ icon: Assets.cpsMode4,
+ description: 'Mode 4',
+ value: 'mode4',
+ ),
+ ];
+ }
+}
+
+final class CpsSpaceTypeFunctions extends CpsFunctions {
+ CpsSpaceTypeFunctions({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'scene',
+ operationName: 'Space Type',
+ icon: Assets.spaceType,
+ );
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ description: 'Office',
+ icon: Assets.office,
+ value: 'office',
+ ),
+ CpsOperationalValue(
+ description: 'Parlour',
+ icon: Assets.parlour,
+ value: 'parlour',
+ ),
+ CpsOperationalValue(
+ description: 'Bathroom',
+ icon: Assets.bathroom,
+ value: 'bathroom',
+ ),
+ CpsOperationalValue(
+ description: 'Bedroom',
+ icon: Assets.bedroom,
+ value: 'bedroom',
+ ),
+ CpsOperationalValue(
+ description: 'DIY',
+ icon: Assets.dyi,
+ value: 'dyi',
+ ),
+ ];
+ }
+}
+
+class CpsPresenceStatusFunctions extends CpsFunctions {
+ CpsPresenceStatusFunctions({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : super(
+ code: 'presence_state',
+ operationName: 'Presence Status',
+ icon: Assets.presenceSensor,
+ );
+
+ @override
+ List getOperationalValues() {
+ return [
+ CpsOperationalValue(
+ icon: Assets.nobodyTime,
+ description: 'None',
+ value: 'none',
+ ),
+ CpsOperationalValue(
+ icon: Assets.presenceState,
+ description: 'Presence',
+ value: 'presence',
+ ),
+ CpsOperationalValue(
+ icon: Assets.motion,
+ description: 'Motion',
+ value: 'motion',
+ ),
+ ];
+ }
+}
+
+final class CpsSportsParaFunction extends CpsFunctions {
+ CpsSportsParaFunction({
+ required super.deviceId,
+ required super.deviceName,
+ required super.type,
+ }) : min = 1,
+ max = 100,
+ step = 1,
+ super(
+ code: 'sports_para',
+ operationName: 'Sports Para',
+ icon: Assets.sportsPara,
+ );
+
+ final double min;
+ final double max;
+ final double step;
+
+ @override
+ List getOperationalValues() {
+ final count = ((max - min) / step).round() + 1;
+ return List.generate(
+ count,
+ (index) {
+ final value = (min + (index * step));
+ return CpsOperationalValue(
+ icon: Assets.motionMeter,
+ description: value.toStringAsFixed(0),
+ value: value,
+ );
+ },
+ );
+ }
+}
diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart
new file mode 100644
index 00000000..99ea2f04
--- /dev/null
+++ b/lib/pages/routines/widgets/condition_toggle.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:syncrow_web/utils/color_manager.dart';
+
+class ConditionToggle extends StatelessWidget {
+ final String? currentCondition;
+ final void Function(String condition) onChanged;
+
+ const ConditionToggle({
+ required this.onChanged,
+ this.currentCondition,
+ super.key,
+ });
+
+ static const _conditions = ["<", "==", ">"];
+
+ @override
+ Widget build(BuildContext context) {
+ return ToggleButtons(
+ onPressed: (index) => onChanged(_conditions[index]),
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ selectedBorderColor: ColorsManager.primaryColorWithOpacity,
+ selectedColor: Colors.white,
+ fillColor: ColorsManager.primaryColorWithOpacity,
+ color: ColorsManager.primaryColorWithOpacity,
+ constraints: const BoxConstraints(
+ minHeight: 40.0,
+ minWidth: 40.0,
+ ),
+ isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(),
+ children: _conditions.map((c) => Text(c)).toList(),
+ );
+ }
+}
diff --git a/lib/pages/routines/widgets/dialog_footer.dart b/lib/pages/routines/widgets/dialog_footer.dart
index 15db9732..e5a548f7 100644
--- a/lib/pages/routines/widgets/dialog_footer.dart
+++ b/lib/pages/routines/widgets/dialog_footer.dart
@@ -8,12 +8,12 @@ class DialogFooter extends StatelessWidget {
final int? dialogWidth;
const DialogFooter({
- Key? key,
+ super.key,
required this.onCancel,
required this.onConfirm,
required this.isConfirmEnabled,
this.dialogWidth,
- }) : super(key: key);
+ });
@override
Widget build(BuildContext context) {
@@ -28,21 +28,19 @@ class DialogFooter extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
- Expanded(
- child: _buildFooterButton(
- context,
- 'Cancel',
- onCancel,
- ),
+ _buildFooterButton(
+ context: context,
+ text: 'Cancel',
+ onTap: onCancel,
),
if (isConfirmEnabled) ...[
Container(width: 1, height: 50, color: ColorsManager.greyColor),
- Expanded(
- child: _buildFooterButton(
- context,
- 'Confirm',
- onConfirm,
- ),
+ _buildFooterButton(
+ context: context,
+ text: 'Confirm',
+ onTap: onConfirm,
+ textColor:
+ isConfirmEnabled ? ColorsManager.primaryColorWithOpacity : Colors.red,
),
],
],
@@ -50,24 +48,24 @@ class DialogFooter extends StatelessWidget {
);
}
- Widget _buildFooterButton(
- BuildContext context,
- String text,
- VoidCallback? onTap,
- ) {
- return GestureDetector(
- onTap: onTap,
- child: SizedBox(
- height: 50,
- child: Center(
- child: Text(
- text,
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(
- color: text == 'Confirm'
- ? ColorsManager.primaryColorWithOpacity
- : ColorsManager.textGray,
- ),
- ),
+ Widget _buildFooterButton({
+ required BuildContext context,
+ required String text,
+ required VoidCallback? onTap,
+ Color? textColor,
+ }) {
+ return Expanded(
+ child: TextButton(
+ style: TextButton.styleFrom(
+ foregroundColor: ColorsManager.primaryColorWithOpacity,
+ disabledForegroundColor: ColorsManager.primaryColor,
+ ),
+ onPressed: onTap,
+ child: Text(
+ text,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: textColor ?? ColorsManager.textGray,
+ ),
),
),
);
diff --git a/lib/pages/routines/widgets/function_slider.dart b/lib/pages/routines/widgets/function_slider.dart
new file mode 100644
index 00000000..50167a7b
--- /dev/null
+++ b/lib/pages/routines/widgets/function_slider.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+class FunctionSlider extends StatelessWidget {
+ final dynamic initialValue;
+ final (double min, double max) range;
+ final void Function(double value) onChanged;
+ final double dividendOfRange;
+
+ const FunctionSlider({
+ required this.onChanged,
+ required this.initialValue,
+ required this.range,
+ required this.dividendOfRange,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final (min, max) = range;
+ final bool isValidRange = max > min;
+ final double value = initialValue is int
+ ? (initialValue as int).toDouble()
+ : (initialValue as double);
+
+ final int? divisions = isValidRange ? ((max - min) / dividendOfRange).round() : null;
+
+ return Slider(
+ value: value.clamp(min, max),
+ min: min,
+ max: max,
+ divisions: divisions,
+ onChanged: isValidRange ? onChanged : null,
+ );
+ }
+}
diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart
index 007e4dc5..f7a4ddc1 100644
--- a/lib/pages/routines/widgets/if_container.dart
+++ b/lib/pages/routines/widgets/if_container.dart
@@ -70,8 +70,9 @@ class IfContainer extends StatelessWidget {
'1G',
'2G',
'3G',
- 'WPS'
- 'GW',
+ 'WPS',
+ 'GW',
+ 'CPS',
].contains(state.ifItems[index]['productType'])) {
context
.read()
@@ -121,7 +122,7 @@ class IfContainer extends StatelessWidget {
if (result != null) {
context.read().add(AddToIfContainer(mutableData, false));
- } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW']
+ } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS']
.contains(mutableData['productType'])) {
context.read().add(AddToIfContainer(mutableData, false));
}
diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart
index f22c8ae3..3294a73a 100644
--- a/lib/pages/routines/widgets/routine_devices.dart
+++ b/lib/pages/routines/widgets/routine_devices.dart
@@ -17,7 +17,7 @@ class _RoutineDevicesState extends State {
context.read().add(FetchDevicesInRoutine());
}
- static const _allowedProductTypes = {'AC', '1G', '2G', '3G', 'WPS', 'GW'};
+ static const _allowedProductTypes = {'AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS'};
@override
Widget build(BuildContext context) {
diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart
new file mode 100644
index 00000000..c18706f0
--- /dev/null
+++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart
@@ -0,0 +1,219 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
+import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
+import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
+import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart';
+import 'package:syncrow_web/pages/routines/models/device_functions.dart';
+import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
+import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_value_selector.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart';
+
+class CeilingSensorDialog extends StatefulWidget {
+ const CeilingSensorDialog({
+ required this.uniqueCustomId,
+ required this.functions,
+ required this.deviceSelectedFunctions,
+ required this.device,
+ required this.dialogType,
+ super.key,
+ });
+
+ final String? uniqueCustomId;
+ final List functions;
+ final List deviceSelectedFunctions;
+ final AllDevicesModel? device;
+ final String dialogType;
+
+ @override
+ State createState() => _CeilingSensorDialogState();
+}
+
+class _CeilingSensorDialogState extends State {
+ late final List _cpsFunctions;
+ late final String _dialogHeaderText;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _cpsFunctions = widget.functions.whereType().where((function) {
+ if (widget.dialogType == 'THEN') {
+ return function.type == 'THEN' || function.type == 'BOTH';
+ }
+ return function.type == 'IF' || function.type == 'BOTH';
+ }).toList();
+
+ final isIfDialog = widget.dialogType == 'IF';
+ _dialogHeaderText = isIfDialog ? 'Conditions' : 'Functions';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ contentPadding: EdgeInsets.zero,
+ content: BlocBuilder(
+ builder: (context, state) {
+ final selectedFunction = state.selectedFunction;
+
+ return Container(
+ width: selectedFunction != null ? 600 : 360,
+ height: 450,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ padding: const EdgeInsets.only(top: 20),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ DialogHeader('Presence Sensor $_dialogHeaderText'),
+ Expanded(child: _buildMainContent(context, state)),
+ DialogFooter(
+ onCancel: () => Navigator.pop(context),
+ onConfirm: state.addedFunctions.isNotEmpty
+ ? () {
+ final functions = _updateValuesForAddedFunctions(
+ state.addedFunctions,
+ );
+ context.read().add(
+ AddFunctionToRoutine(
+ functions,
+ '${widget.uniqueCustomId}',
+ ),
+ );
+
+ Navigator.pop(context, {
+ 'deviceId': widget.functions.first.deviceId,
+ });
+ }
+ : null,
+ isConfirmEnabled: selectedFunction != null,
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildMainContent(BuildContext context, FunctionBlocState state) {
+ final selectedFunction = state.selectedFunction;
+ final selectedOperationName = state.selectedOperationName;
+ final selectedFunctionData = state.addedFunctions.firstWhere(
+ (f) => f.functionCode == selectedFunction,
+ orElse: () => DeviceFunctionData(
+ entityId: '',
+ functionCode: selectedFunction ?? '',
+ operationName: '',
+ value: null,
+ ),
+ );
+ final selectedCpsFunctions = _cpsFunctions.firstWhere(
+ (f) => f.code == selectedFunction,
+ orElse: () => CpsMovementFunctions(
+ deviceId: '',
+ deviceName: '',
+ type: '',
+ ),
+ );
+ final operations = selectedCpsFunctions.getOperationalValues();
+ final isSensitivityFunction = selectedFunction == 'sensitivity';
+ final isToggleFunction = isSensitivityFunction
+ ? widget.dialogType == 'THEN'
+ : CeilingSensorHelper.toggleCodes.contains(selectedFunction);
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ CpsFunctionsList(cpsFunctions: _cpsFunctions),
+ if (state.selectedFunction != null)
+ Expanded(
+ child: isToggleFunction
+ ? CpsDialogValueSelector(
+ operations: operations,
+ selectedFunction: selectedFunction ?? '',
+ selectedFunctionData: selectedFunctionData,
+ cpsFunctions: _cpsFunctions,
+ operationName: selectedOperationName ?? '',
+ device: widget.device,
+ )
+ : CpsDialogSliderSelector(
+ operations: operations,
+ selectedFunction: selectedFunction ?? '',
+ selectedFunctionData: selectedFunctionData,
+ cpsFunctions: _cpsFunctions,
+ operationName: selectedOperationName ?? '',
+ device: widget.device,
+ dialogType: widget.dialogType,
+ ),
+ ),
+ ],
+ );
+ }
+
+ static const _mappableSteppedFunctions = {
+ 'static_max_dis',
+ 'presence_reference',
+ 'moving_reference',
+ 'perceptual_boundary',
+ 'moving_boundary',
+ 'moving_rigger_time',
+ 'moving_static_time',
+ 'none_body_time',
+ 'moving_max_dis',
+ 'moving_range',
+ 'presence_range',
+ };
+
+ List _updateValuesForAddedFunctions(
+ List addedFunctions,
+ ) {
+ return addedFunctions.map((function) {
+ final shouldMapValue = _mappableSteppedFunctions.contains(
+ function.functionCode,
+ );
+ if (shouldMapValue) {
+ final mappedValue = _mapSteppedValue(
+ value: function.value,
+ inputStep: CpsSliderHelpers.dividendOfRange(function.functionCode),
+ inputRange: CpsSliderHelpers.sliderRange(function.functionCode),
+ outputRange: CpsSliderHelpers.mappedRange(function.functionCode),
+ );
+ return DeviceFunctionData(
+ value: mappedValue,
+ entityId: function.entityId,
+ functionCode: function.functionCode,
+ operationName: function.operationName,
+ condition: function.condition,
+ actionExecutor: function.actionExecutor,
+ valueDescription: function.valueDescription,
+ );
+ }
+ return function;
+ }).toList();
+ }
+
+ int _mapSteppedValue({
+ required (double min, double max) inputRange,
+ required double inputStep,
+ required (double min, double max, double dividend) outputRange,
+ required double value,
+ }) {
+ final (inputMin, inputMax) = inputRange;
+ final (outputMin, outputMax, outputStep) = outputRange;
+
+ final clampedValue = value.clamp(inputMin, inputMax);
+
+ final stepsFromMin = ((clampedValue - inputMin) / inputStep).round();
+
+ final mappedValue = outputMin + (stepsFromMin * outputStep);
+
+ return mappedValue.clamp(outputMin, outputMax).round();
+ }
+}
diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart
new file mode 100644
index 00000000..940daf23
--- /dev/null
+++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart
@@ -0,0 +1,176 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
+import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
+import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart';
+import 'package:syncrow_web/pages/routines/models/device_functions.dart';
+import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart';
+
+abstract final class CeilingSensorHelper {
+ const CeilingSensorHelper._();
+
+ static Future