Compare commits

..

56 Commits

Author SHA1 Message Date
3e32968209 Update pull_request_template.md 2025-06-04 14:47:35 +03:00
beb5239c4f enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to dev or main, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself. 2025-06-04 14:47:35 +03:00
ad8e06ac40 Sp 1457 fe edit popup buttons labels to be the same as the design (#231)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1457](https://syncrow.atlassian.net/browse/SP-1457)

## Description

<!--- Describe your changes in detail -->
Edit popup buttons labels to be the same as the design and change
setting icon

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [X]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1457]:
https://syncrow.atlassian.net/browse/SP-1457?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-04 11:05:37 +03:00
5f8eb9de06 Add settings button SVG and refactor settings icon implementation in dynamic table 2025-06-04 10:32:31 +03:00
8e8fdf0fc6 Rename dialog buttons for clarity: 'Cancel' to 'Back' and 'Confirm' to 'Save' 2025-06-04 09:26:56 +03:00
0d0d51463d Merge pull request #224 from SyncrowIOT/SP-1597-FE-Add-Device-Settings-Column-and-Build-Device-Settings-Dialog-UI
Sp 1597 fe add device settings column and build device settings dialog UI
2025-06-03 16:56:24 +03:00
8827f571f4 Merge pull request #229 from SyncrowIOT/SP-1658-The-Analytics-charts-padding-is-not-aligned-with-the-design
Sp 1658 the analytics charts padding is not aligned with the design
2025-06-03 16:51:05 +03:00
7472aff704 Merge pull request #228 from SyncrowIOT/SP-1671-energy-consumption-api-returns-a-sibling-space-data-instead-of-selected-space-on-analytics-page
SP-1671-energy-consumption-api-returns-a-sibling-space-data-instead-of-selected-space-on-analytics-page
2025-06-03 16:48:26 +03:00
575ba2aed2 Merge pull request #227 from SyncrowIOT/SP-1509-Attatch-SpaceUuid-To-Dropdown
SP-1509 attatch space uuid to analytics device dropdown on energy man…
2025-06-03 16:46:53 +03:00
eb708edc83 Merge pull request #226 from SyncrowIOT/SP-1510-show-date-on-bottom-titles-of-occupancy-chart
SP-1510-show date instead of index in occupancy chart.
2025-06-03 16:44:56 +03:00
e86c25c74a includes min in all left titles charts. 2025-06-03 16:18:57 +03:00
c2c58e6a7a SP-1658-the-analytics-chart-padding-is-not-aligned-with-the-design. 2025-06-03 16:17:14 +03:00
0135b6711e removed getting energy management data using communityUuid. 2025-06-03 16:01:45 +03:00
46feb0ea28 SP-1509 attatch space uuid to analytics device dropdown on energy management tab. 2025-06-03 15:20:30 +03:00
74ae9d7ce1 Merge pull request #225 from SyncrowIOT/SP-1175-FE-Identify-and-remove-all-instances-of-backend-error-toast-messages-in-the-frontend-code-web
change the validation from static code to backend
2025-06-03 15:04:03 +03:00
710f316f8d Merge pull request #223 from SyncrowIOT/SP-1600-FE-Single-Batch-Control-Migration
Sp 1600 fe single batch control migration
2025-06-03 12:27:10 +03:00
7cc46d464f SP-1510-show date instead of index in occupancy chart. 2025-06-03 12:24:38 +03:00
0c82a19a1d Merge pull request #218 from SyncrowIOT/SP-1593-FE-Create-Recommendation-Section-Based-on-AQI-Level-and-Ensure-Layout-Responsiveness
Sp 1593 fe create recommendation section based on aqi level and ensure layout responsiveness
2025-06-03 11:22:39 +03:00
d1df33b31e Refactor WallSensorBloc to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and real-time status updates from Firebase, including optimized parsing logic for device status values. 2025-06-03 11:15:06 +03:00
6a36405530 Refactor TwoGangSwitchBloc to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and real-time status updates from Firebase, including parsing logic for device status values. 2025-06-03 10:48:01 +03:00
3c98365338 change the validation from static code to backend 2025-06-03 10:44:34 +03:00
88a7607395 Refactor TwoGangGlassSwitchBloc to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates, including real-time status listening from Firebase. 2025-06-03 10:33:33 +03:00
f58ddf76da Refactor LivingRoomBloc to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates, including real-time status listening from Firebase. 2025-06-03 10:19:10 +03:00
a71a66034c Refactor ThreeGangGlassSwitchBloc to integrate new service dependencies and utilize a factory for instantiation. Enhanced event handling methods for improved error management and state updates. 2025-06-03 09:49:26 +03:00
b06a23cc60 Refactor WallLightSwitchBloc to integrate new service dependencies and utilize a factory for instantiation. Improved event handling methods for better error management and state updates. 2025-06-02 16:40:13 +03:00
5595bb7f25 Refactor OneGangGlassSwitchBloc to utilize new service dependencies and implement a factory for instantiation. Enhanced event handling methods for improved error management and state updates. 2025-06-02 16:35:55 +03:00
8e11749ed7 Prepared for aqi distribution API Integration. 2025-06-02 16:13:58 +03:00
7bc9079212 reverted a comment. 2025-06-02 14:30:07 +03:00
97801872e0 Implemented an initial remote implementation of RangeOfAqiService. 2025-06-02 14:29:04 +03:00
fa9210f387 added fromJson factory methods to RangeOfAqi, and to RangeOfAqiValue data models. 2025-06-02 14:28:50 +03:00
57b6f01177 SP-1593 Implemented the agreed upon api contract. 2025-06-02 14:26:47 +03:00
77d39bfc53 Refactor CurtainBloc to use new service dependencies and implement a factory for instantiation. Updated event handling methods for improved error management and state updates. 2025-06-02 11:26:30 +03:00
3bd2bd114b migrate CeilingSensorBloc to use the new services. 2025-06-02 11:13:56 +03:00
f98636a2e5 Migrated AcBloc single/batch controls the new services. 2025-06-02 10:44:43 +03:00
19548e99ab indentation and formatting of WaterHeaterBloc. 2025-06-02 10:20:05 +03:00
b60c674496 Created a factory for the WaterHeaterBloc, and injected the necessary dependenices. 2025-06-02 10:12:53 +03:00
6f3dfb607e Extracted single/batch control services creation into a factory for ease of reusablility for the sake of this migration. 2025-06-02 10:11:23 +03:00
62dabf1ce2 Made values in DeviceControlDialog selectable for a better UX. 2025-06-02 10:10:50 +03:00
066f967cd1 shows tooltip with data. 2025-06-01 14:28:40 +03:00
e28f3c3c03 reduced bar width size. 2025-06-01 14:28:40 +03:00
2be15e648a added loading widget to AqiDistributionChartTitle. 2025-06-01 14:28:40 +03:00
2e12d73151 randomize generated fake data in FakeAirQualityDistributionService. 2025-06-01 14:28:40 +03:00
c50ed693ae loads and clears aqi distribution in FetchAirQualityDataHelper. 2025-06-01 14:28:40 +03:00
8dc7d2b3d0 Connected AirQualityDistributionBloc into AqiDistributionChartBox. 2025-06-01 14:28:40 +03:00
accafb150e . 2025-06-01 14:24:07 +03:00
736e0c3d9c Injected AirQualityDistributionBloc into AnalyticsPage. 2025-06-01 14:23:14 +03:00
455d9c1f01 Created AirQualityDistributionBloc. 2025-06-01 14:22:25 +03:00
4479ed04b7 Created a AirQualityDistributionService along with its fake implementation. 2025-06-01 14:22:25 +03:00
286dea3f51 created a GetAirQualityDistributionParam. 2025-06-01 14:22:25 +03:00
44c4648941 made the first element of the bar rods to have only a top sides radius to match the design. 2025-06-01 14:22:25 +03:00
ca1feb9600 made charts based on states and not based on metrics. 2025-06-01 14:22:25 +03:00
7b31914e1c made progress towards aqi distribution chart. 2025-06-01 14:22:25 +03:00
10f35d3747 added more mock data to AqiDistributionChart. 2025-06-01 14:22:25 +03:00
1998a629b6 added some opacity to metric colors. 2025-06-01 14:22:25 +03:00
5940e52826 Implemented an initial version of AqiDistributionChart. 2025-06-01 14:22:25 +03:00
7c55e8bbf9 Prepared widgets for the aqi distribution chart. 2025-06-01 14:22:25 +03:00
106 changed files with 3067 additions and 2138 deletions

View File

@ -7,11 +7,7 @@
-->
## Jira Ticket
<!-- Add your Jira ticket number as a link (e.g., [PROJ-123](https://jira.company.com/browse/PROJ-123)) -->
## Status
**READY/IN DEVELOPMENT/HOLD**
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Description
@ -27,4 +23,4 @@
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
- [ ] 🗑️ Chore

View File

@ -4,10 +4,6 @@ on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:

View File

@ -4,18 +4,12 @@ on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- dev
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:

29
.github/workflows/pr-check.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Pull Request Check
on:
pull_request:
branches:
- dev
- main
jobs:
setup_flutter:
runs-on: ubuntu-latest
name: Setup Flutter and Dependencies
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: true
lfs: false
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.3'
- name: Install dependencies
run: flutter pub get
- name: Run Flutter Build
run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart

View File

@ -0,0 +1,24 @@
<svg width="40" height="26" viewBox="0 0 40 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_9292_1537)">
<rect x="3" y="3" width="34" height="20" rx="10" fill="#F5F6F7"/>
<g clip-path="url(#clip0_9292_1537)">
<path d="M20.4393 20H19.5607C18.85 20 18.2718 19.4218 18.2718 18.7112V18.414C17.9697 18.3174 17.6762 18.1956 17.3942 18.0497L17.1835 18.2603C16.6733 18.7711 15.8561 18.7562 15.3607 18.2601L14.7397 17.6391C14.2434 17.1434 14.2291 16.3264 14.7398 15.8163L14.9503 15.6058C14.8044 15.3238 14.6826 15.0303 14.586 14.7281H14.2888C13.5782 14.7281 13 14.15 13 13.4393V12.5607C13 11.85 13.5782 11.2719 14.2888 11.2719H14.586C14.6826 10.9697 14.8044 10.6762 14.9503 10.3942L14.7397 10.1836C14.2293 9.67374 14.2434 8.85666 14.7399 8.36072L15.3609 7.73969C15.8574 7.24247 16.6745 7.23006 17.1838 7.73986L17.3942 7.95032C17.6762 7.80441 17.9698 7.68257 18.2719 7.58602V7.28879C18.2719 6.57816 18.85 6 19.5607 6H20.4393C21.15 6 21.7281 6.57816 21.7281 7.28879V7.58605C22.0302 7.68257 22.3238 7.80441 22.6058 7.95035L22.8164 7.73969C23.3266 7.22886 24.1439 7.24384 24.6393 7.73988L25.2603 8.36086C25.7566 8.85657 25.7708 9.67358 25.2601 10.1837L25.0497 10.3942C25.1956 10.6762 25.3174 10.9697 25.414 11.2719H25.7112C26.4218 11.2719 27 11.85 27 12.5607V13.4393C27 14.15 26.4218 14.7281 25.7112 14.7281H25.414C25.3174 15.0303 25.1956 15.3238 25.0497 15.6058L25.2603 15.8164C25.7707 16.3263 25.7566 17.1434 25.2601 17.6393L24.6391 18.2603C24.1426 18.7576 23.3255 18.77 22.8162 18.2602L22.6058 18.0497C22.3238 18.1956 22.0302 18.3175 21.7281 18.414V18.7113C21.7281 19.4218 21.15 20 20.4393 20ZM17.5313 17.1882C17.9231 17.4199 18.3447 17.595 18.7845 17.7085C18.9656 17.7552 19.0922 17.9185 19.0922 18.1056V18.7112C19.0922 18.9695 19.3024 19.1797 19.5607 19.1797H20.4393C20.6976 19.1797 20.9078 18.9695 20.9078 18.7112V18.1056C20.9078 17.9185 21.0344 17.7552 21.2155 17.7085C21.6553 17.595 22.0769 17.4199 22.4687 17.1882C22.6299 17.0929 22.8351 17.1188 22.9675 17.2513L23.3965 17.6803C23.5815 17.8654 23.8785 17.8611 24.0589 17.6805L24.6803 17.059C24.8603 16.8793 24.8663 16.5822 24.6805 16.3966L24.2513 15.9675C24.1189 15.8351 24.093 15.6298 24.1883 15.4687C24.42 15.0769 24.595 14.6553 24.7085 14.2155C24.7552 14.0344 24.9186 13.9078 25.1056 13.9078H25.7112C25.9695 13.9078 26.1797 13.6977 26.1797 13.4394V12.5607C26.1797 12.3024 25.9695 12.0922 25.7112 12.0922H25.1056C24.9186 12.0922 24.7552 11.9657 24.7085 11.7846C24.595 11.3447 24.42 10.9231 24.1883 10.5314C24.093 10.3702 24.1189 10.165 24.2513 10.0326L24.6803 9.60358C24.8658 9.41835 24.8609 9.1214 24.6805 8.94118L24.0591 8.31979C23.879 8.13943 23.5819 8.13415 23.3967 8.31962L22.9676 8.74879C22.8352 8.88121 22.6299 8.90713 22.4687 8.81181C22.077 8.58013 21.6553 8.4051 21.2155 8.2916C21.0344 8.24487 20.9079 8.08152 20.9079 7.89446V7.28879C20.9079 7.03048 20.6977 6.82031 20.4394 6.82031H19.5607C19.3024 6.82031 19.0922 7.03048 19.0922 7.28879V7.8944C19.0922 8.08146 18.9657 8.24481 18.7845 8.29154C18.3447 8.40505 17.9231 8.58007 17.5314 8.81176C17.3701 8.90705 17.1649 8.88113 17.0325 8.74873L16.6036 8.31973C16.4186 8.13456 16.1216 8.13886 15.9412 8.31954L15.3197 8.94096C15.1398 9.12071 15.1337 9.41775 15.3196 9.60336L15.7487 10.0325C15.8811 10.1649 15.9071 10.3702 15.8118 10.5313C15.5801 10.9231 15.4051 11.3447 15.2916 11.7845C15.2448 11.9656 15.0815 12.0922 14.8944 12.0922H14.2888C14.0305 12.0922 13.8203 12.3024 13.8203 12.5607V13.4393C13.8203 13.6976 14.0305 13.9078 14.2888 13.9078H14.8944C15.0815 13.9078 15.2448 14.0344 15.2915 14.2155C15.405 14.6553 15.5801 15.0769 15.8117 15.4686C15.907 15.6298 15.8811 15.8351 15.7487 15.9675L15.3197 16.3965C15.1343 16.5817 15.1391 16.8786 15.3195 17.0589L15.9409 17.6802C16.121 17.8606 16.4181 17.8659 16.6033 17.6804L17.0325 17.2512C17.13 17.1537 17.333 17.0709 17.5313 17.1882Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M19.9992 16.0461C18.3196 16.0461 16.9531 14.6796 16.9531 13C16.9531 11.3204 18.3196 9.95391 19.9992 9.95391C21.6789 9.95391 23.0453 11.3204 23.0453 13C23.0453 14.6796 21.6789 16.0461 19.9992 16.0461ZM19.9992 10.7742C18.7719 10.7742 17.7734 11.7727 17.7734 13C17.7734 14.2273 18.7719 15.2258 19.9992 15.2258C21.2265 15.2258 22.225 14.2273 22.225 13C22.225 11.7727 21.2265 10.7742 19.9992 10.7742Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
</g>
<defs>
<filter id="filter0_d_9292_1537" x="0" y="0" width="40" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_9292_1537"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_9292_1537" result="shape"/>
</filter>
<clipPath id="clip0_9292_1537">
<rect width="14" height="14" fill="white" transform="translate(13 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,57 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AirQualityDataModel extends Equatable {
const AirQualityDataModel({
required this.date,
required this.data,
});
final DateTime date;
final List<AirQualityPercentageData> data;
factory AirQualityDataModel.fromJson(Map<String, dynamic> json) {
return AirQualityDataModel(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
static final Map<String, Color> metricColors = {
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
};
@override
List<Object?> get props => [date, data];
}
class AirQualityPercentageData extends Equatable {
const AirQualityPercentageData({
required this.type,
required this.name,
required this.percentage,
});
final String type;
final String name;
final double percentage;
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
return AirQualityPercentageData(
type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
);
}
@override
List<Object?> get props => [type, name, percentage];
}

View File

@ -23,17 +23,18 @@ class AnalyticsDevice {
return AnalyticsDevice(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
deviceTuyaUuid: json['deviceTuyaUuid'] as String?,
isActive: json['isActive'] as bool?,
productDevice: json['productDevice'] != null
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: (json['spaces'] as List<dynamic>?)
?.map((e) => e['uuid'])
.firstOrNull
?.toString(),
productDevice: json['productDevice'] != null
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: json['spaceUuid'] as String?,
);
}
}
@ -60,8 +61,12 @@ class ProductDevice {
factory ProductDevice.fromJson(Map<String, dynamic> json) {
return ProductDevice(
uuid: json['uuid'] as String?,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: null,
catName: json['catName'] as String?,
prodId: json['prodId'] as String?,
name: json['name'] as String?,

View File

@ -1,18 +1,49 @@
import 'package:equatable/equatable.dart';
class RangeOfAqi extends Equatable {
final double min;
final double avg;
final double max;
final DateTime date;
final List<RangeOfAqiValue> data;
const RangeOfAqi({
required this.min,
required this.avg,
required this.max,
required this.data,
required this.date,
});
factory RangeOfAqi.fromJson(Map<String, dynamic> json) {
return RangeOfAqi(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map((e) => RangeOfAqiValue.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@override
List<Object?> get props => [min, avg, max, date];
List<Object?> get props => [data, date];
}
class RangeOfAqiValue extends Equatable {
final String type;
final double min;
final double average;
final double max;
const RangeOfAqiValue({
required this.type,
required this.min,
required this.average,
required this.max,
});
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
return RangeOfAqiValue(
type: json['type'] as String,
min: (json['min'] as num).toDouble(),
average: (json['average'] as num).toDouble(),
max: (json['max'] as num).toDouble(),
);
}
@override
List<Object?> get props => [type, min, average, max];
}

View File

@ -0,0 +1,81 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
part 'air_quality_distribution_event.dart';
part 'air_quality_distribution_state.dart';
class AirQualityDistributionBloc
extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> {
final AirQualityDistributionService _aqiDistributionService;
AirQualityDistributionBloc(
this._aqiDistributionService,
) : super(const AirQualityDistributionState()) {
on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution);
on<ClearAirQualityDistribution>(_onClearAirQualityDistribution);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
}
Future<void> _onLoadAirQualityDistribution(
LoadAirQualityDistribution event,
Emitter<AirQualityDistributionState> emit,
) async {
try {
emit(state.copyWith(status: AirQualityDistributionStatus.loading));
final result = await _aqiDistributionService.getAirQualityDistribution(
event.param,
);
emit(
state.copyWith(
status: AirQualityDistributionStatus.success,
chartData: result,
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
),
);
} catch (e) {
emit(
AirQualityDistributionState(
status: AirQualityDistributionStatus.failure,
errorMessage: e.toString(),
selectedAqiType: state.selectedAqiType,
),
);
}
}
Future<void> _onClearAirQualityDistribution(
ClearAirQualityDistribution event,
Emitter<AirQualityDistributionState> emit,
) async {
emit(const AirQualityDistributionState());
}
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<AirQualityDistributionState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
),
);
}
List<AirQualityDataModel> _arrangeChartDataByType(
List<AirQualityDataModel> data,
AqiType aqiType,
) {
final filteredData = data.map(
(data) => AirQualityDataModel(
date: data.date,
data: data.data.where((value) => value.type == aqiType.code).toList(),
),
);
return filteredData.toList();
}
}

View File

@ -0,0 +1,30 @@
part of 'air_quality_distribution_bloc.dart';
sealed class AirQualityDistributionEvent extends Equatable {
const AirQualityDistributionEvent();
@override
List<Object> get props => [];
}
final class LoadAirQualityDistribution extends AirQualityDistributionEvent {
final GetAirQualityDistributionParam param;
const LoadAirQualityDistribution(this.param);
@override
List<Object> get props => [param];
}
final class UpdateAqiTypeEvent extends AirQualityDistributionEvent {
const UpdateAqiTypeEvent(this.aqiType);
final AqiType aqiType;
@override
List<Object> get props => [aqiType];
}
final class ClearAirQualityDistribution extends AirQualityDistributionEvent {
const ClearAirQualityDistribution();
}

View File

@ -0,0 +1,43 @@
part of 'air_quality_distribution_bloc.dart';
enum AirQualityDistributionStatus {
initial,
loading,
success,
failure,
}
class AirQualityDistributionState extends Equatable {
const AirQualityDistributionState({
this.status = AirQualityDistributionStatus.initial,
this.chartData = const [],
this.filteredChartData = const [],
this.errorMessage,
this.selectedAqiType = AqiType.aqi,
});
final AirQualityDistributionStatus status;
final List<AirQualityDataModel> chartData;
final List<AirQualityDataModel> filteredChartData;
final String? errorMessage;
final AqiType selectedAqiType;
AirQualityDistributionState copyWith({
AirQualityDistributionStatus? status,
List<AirQualityDataModel>? chartData,
List<AirQualityDataModel>? filteredChartData,
String? errorMessage,
AqiType? selectedAqiType,
}) {
return AirQualityDistributionState(
status: status ?? this.status,
chartData: chartData ?? this.chartData,
filteredChartData: filteredChartData ?? this.filteredChartData,
errorMessage: errorMessage ?? this.errorMessage,
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
);
}
@override
List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
}

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
}
final RangeOfAqiService _rangeOfAqiService;
@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
Emitter<RangeOfAqiState> emit,
) async {
emit(
RangeOfAqiState(
status: RangeOfAqiStatus.loading,
rangeOfAqi: state.rangeOfAqi,
),
state.copyWith(status: RangeOfAqiStatus.loading),
);
try {
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi));
emit(
state.copyWith(
status: RangeOfAqiStatus.loaded,
rangeOfAqi: rangeOfAqi,
filteredRangeOfAqi: _arrangeChartDataByType(
rangeOfAqi,
state.selectedAqiType,
),
),
);
} catch (e) {
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e'));
emit(
state.copyWith(
status: RangeOfAqiStatus.failure,
errorMessage: '$e',
),
);
}
}
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<RangeOfAqiState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType),
),
);
}
List<RangeOfAqi> _arrangeChartDataByType(
List<RangeOfAqi> rangeOfAqi,
AqiType aqiType,
) {
final filteredRangeOfAqi = rangeOfAqi.map(
(data) => RangeOfAqi(
date: data.date,
data: data.data.where((value) => value.type == aqiType.code).toList(),
),
);
return filteredRangeOfAqi.toList();
}
void _onClearRangeOfAqiEvent(
ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit,

View File

@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
List<Object> get props => [param];
}
class UpdateAqiTypeEvent extends RangeOfAqiEvent {
const UpdateAqiTypeEvent(this.aqiType);
final AqiType aqiType;
@override
List<Object> get props => [aqiType];
}
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
const ClearRangeOfAqiEvent();
}

View File

@ -5,14 +5,35 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure }
final class RangeOfAqiState extends Equatable {
const RangeOfAqiState({
this.rangeOfAqi = const [],
this.filteredRangeOfAqi = const [],
this.status = RangeOfAqiStatus.initial,
this.errorMessage,
this.selectedAqiType = AqiType.aqi,
});
final RangeOfAqiStatus status;
final List<RangeOfAqi> rangeOfAqi;
final List<RangeOfAqi> filteredRangeOfAqi;
final String? errorMessage;
final AqiType selectedAqiType;
RangeOfAqiState copyWith({
RangeOfAqiStatus? status,
List<RangeOfAqi>? rangeOfAqi,
List<RangeOfAqi>? filteredRangeOfAqi,
String? errorMessage,
AqiType? selectedAqiType,
}) {
return RangeOfAqiState(
status: status ?? this.status,
rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi,
filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi,
errorMessage: errorMessage ?? this.errorMessage,
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
);
}
@override
List<Object?> get props => [status, rangeOfAqi, errorMessage];
List<Object?> get props =>
[status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType];
}

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
@ -13,8 +14,10 @@ abstract final class FetchAirQualityDataHelper {
static void loadAirQualityData(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices(
@ -26,7 +29,11 @@ abstract final class FetchAirQualityDataHelper {
context,
spaceUuid: spaceUuid,
date: date,
aqiType: AqiType.aqi,
);
loadAirQualityDistribution(
context,
spaceUuid: spaceUuid,
date: date,
);
}
@ -37,7 +44,9 @@ abstract final class FetchAirQualityDataHelper {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<AirQualityDistributionBloc>().add(
const ClearAirQualityDistribution(),
);
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
}
@ -67,16 +76,26 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, {
required String spaceUuid,
required DateTime date,
required AqiType aqiType,
}) {
context.read<RangeOfAqiBloc>().add(
LoadRangeOfAqiEvent(
GetRangeOfAqiParam(
date: date,
spaceUuid: spaceUuid,
aqiType: aqiType,
),
),
);
}
static void loadAirQualityDistribution(
BuildContext context, {
required String spaceUuid,
required DateTime date,
}) {
context.read<AirQualityDistributionBloc>().add(
LoadAirQualityDistribution(
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget {
height: height * 1.2,
child: const AirQualityEndSideWidget(),
),
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()),
SizedBox(
height: height * 0.5,
child: const RangeOfAqiChartBox(),
),
SizedBox(
height: height * 0.5,
child: const AqiDistributionChartBox(),
),
],
),
);
@ -46,7 +53,7 @@ class AirQualityView extends StatelessWidget {
spacing: 20,
children: [
Expanded(child: RangeOfAqiChartBox()),
Expanded(child: Placeholder()),
Expanded(child: AqiDistributionChartBox()),
],
),
),

View File

@ -0,0 +1,174 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AqiDistributionChart extends StatelessWidget {
const AqiDistributionChart({super.key, required this.chartData});
final List<AirQualityDataModel> chartData;
static const _rodStackItemsSpacing = 0.4;
static const _barWidth = 13.0;
static final _barBorderRadius = BorderRadius.circular(22);
@override
Widget build(BuildContext context) {
final sortedData = List<AirQualityDataModel>.from(chartData)
..sort(
(a, b) => a.date.compareTo(b.date),
);
return BarChart(
BarChartData(
maxY: 100.1,
gridData: EnergyManagementChartsHelper.gridData(
horizontalInterval: 20,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: _buildBarGroups(sortedData),
),
duration: Duration.zero,
);
}
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
return List.generate(sortedData.length, (index) {
final data = sortedData[index];
final stackItems = <BarChartRodData>[];
double currentY = 0;
bool isFirstElement = true;
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + percentageData.percentage ,
color: AirQualityDataModel.metricColors[percentageData.name]!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
return BarChartGroupData(
x: index,
barRods: stackItems,
groupVertically: true,
);
});
}
BarTouchData _barTouchData(BuildContext context) {
return BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x.toInt()];
final List<TextSpan> children = [];
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
);
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
children.add(TextSpan(
text:
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
style: textStyle,
));
}
return BarTooltipItem(
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
children: children,
);
},
),
);
}
FlTitlesData _titlesData(BuildContext context) {
final titlesData = EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 20,
);
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 20,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${value.toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.lightGreyColor,
),
),
),
),
),
);
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: Text(
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
),
),
),
reservedSize: 36,
),
);
return titlesData.copyWith(
leftTitles: leftTitles,
bottomTitles: bottomTitles,
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiDistributionChartBox extends StatelessWidget {
const AqiDistributionChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AirQualityDistributionBloc, AirQualityDistributionState>(
builder: (context, state) {
return Container(
padding: const EdgeInsetsDirectional.all(30),
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.errorMessage != null) ...[
AnalyticsErrorWidget(state.errorMessage),
const SizedBox(height: 10),
],
AqiDistributionChartTitle(
isLoading: state.status == AirQualityDistributionStatus.loading,
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: AqiDistributionChart(chartData: state.filteredChartData),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
class AqiDistributionChartTitle extends StatelessWidget {
const AqiDistributionChartTitle({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return Row(
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(
title: Text('Distribution over Air Quality Index'),
),
),
),
FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
context
.read<AirQualityDistributionBloc>()
.add(UpdateAqiTypeEvent(value));
}
},
),
),
],
);
}
}

View File

@ -3,17 +3,18 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
enum AqiType {
aqi('AQI', ''),
pm25('PM2.5', 'µg/m³'),
pm10('PM10', 'µg/m³'),
hcho('HCHO', 'mg/m³'),
tvoc('TVOC', 'µg/m³'),
co2('CO2', 'ppm');
aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³', 'hcho'),
tvoc('TVOC', 'µg/m³', 'tvoc'),
co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit);
const AqiType(this.value, this.unit, this.code);
final String value;
final String unit;
final String code;
}
class AqiTypeDropdown extends StatefulWidget {

View File

@ -13,23 +13,37 @@ class RangeOfAqiChart extends StatelessWidget {
required this.chartData,
});
List<(List<double> values, Color color, Color? dotColor)> get _lines => [
(
chartData.map((e) => e.max).toList(),
ColorsManager.maxPurple,
ColorsManager.maxPurpleDot,
),
(
chartData.map((e) => e.avg).toList(),
Colors.white,
null,
),
(
chartData.map((e) => e.min).toList(),
ColorsManager.minBlue,
ColorsManager.minBlueDot,
),
];
List<(List<double> values, Color color, Color? dotColor)> get _lines {
final sortedData = List<RangeOfAqi>.from(chartData)
..sort((a, b) => a.date.compareTo(b.date));
return [
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.max ?? 0;
}).toList(),
ColorsManager.maxPurple,
ColorsManager.maxPurpleDot,
),
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.average ?? 0;
}).toList(),
Colors.white,
null,
),
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.min ?? 0;
}).toList(),
ColorsManager.minBlue,
ColorsManager.minBlueDot,
),
];
}
@override
Widget build(BuildContext context) {

View File

@ -32,7 +32,7 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
],
),
);

View File

@ -1,15 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class RangeOfAqiChartTitle extends StatelessWidget {
const RangeOfAqiChartTitle({required this.isLoading, super.key});
const RangeOfAqiChartTitle({
required this.isLoading,
super.key,
});
final bool isLoading;
static const List<(Color color, String title, bool hasBorder)> _colors = [
@ -66,12 +69,9 @@ class RangeOfAqiChartTitle extends StatelessWidget {
if (spaceUuid == null) return;
FetchAirQualityDataHelper.loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
aqiType: value ?? AqiType.aqi,
);
if (value != null) {
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
}
},
),
),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
@ -39,6 +40,7 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
context,
communityUuid: community.uuid,
spaceUuid: space.uuid ?? '',
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
@ -13,6 +14,7 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@ -101,6 +103,11 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FakeRangeOfAqiService(),
),
),
BlocProvider(
create: (context) => AirQualityDistributionBloc(
FakeAirQualityDistributionService(),
),
),
],
child: const AnalyticsPageForm(),
);

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
const Spacer(),
Visibility(
key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement,
visible: selectedTab == AnalyticsPageTab.energyManagement ||
selectedTab == AnalyticsPageTab.airQuality,
child: Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
}
onDateSelected: (value) {
_onDateChanged(context, value, selectedTab);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
child: child,
);
}
void _onDateChanged(
BuildContext context,
DateTime date,
AnalyticsPageTab selectedTab,
) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: date),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final communities = spaceTreeState.selectedCommunities;
final spaces = spaceTreeState.selectedSpaces;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
switch (selectedTab) {
case AnalyticsPageTab.energyManagement:
_onEnergyManagementDateChanged(
context,
date: date,
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
break;
case AnalyticsPageTab.airQuality:
_onAirQualityDateChanged(
context,
date: date,
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
default:
break;
}
}
}
void _onEnergyManagementDateChanged(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: date),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: date,
communityId: communityUuid,
spaceId: spaceUuid,
);
}
void _onAirQualityDateChanged(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
}) {
FetchAirQualityDataHelper.loadAirQualityData(
context,
date: date,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
shouldFetchAnalyticsDevices: false,
);
}
}

View File

@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper {
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: false,
minIncluded: true,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(

View File

@ -16,7 +16,6 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_
abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._();
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
}
@ -48,7 +47,6 @@ abstract final class FetchEnergyManagementDataHelper {
loadTotalEnergyConsumption(
context,
selectedDate: selectedDate0,
communityId: communityId,
spaceId: spaceId,
);
final selectedDevice = getSelectedDevice(context);
@ -61,7 +59,6 @@ abstract final class FetchEnergyManagementDataHelper {
}
loadEnergyConsumptionPerDevice(
context,
communityId: communityId,
spaceId: spaceId,
selectedDate: selectedDate0,
);
@ -84,12 +81,10 @@ abstract final class FetchEnergyManagementDataHelper {
static void loadTotalEnergyConsumption(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetTotalEnergyConsumptionParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<TotalEnergyConsumptionBloc>().add(
@ -100,12 +95,10 @@ abstract final class FetchEnergyManagementDataHelper {
static void loadEnergyConsumptionPerDevice(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetEnergyConsumptionPerDeviceParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<EnergyConsumptionPerDeviceBloc>().add(

View File

@ -23,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
),
padding: const EdgeInsets.all(30),
child: Column(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
@ -52,7 +51,9 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
),
],
),
const SizedBox(height: 20),
const Divider(height: 0),
const SizedBox(height: 20),
Expanded(
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
),

View File

@ -41,7 +41,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
.color;
return Tooltip(
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
message: '${device.name}\n${device.spaceUuid ?? ''}',
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
);
}

View File

@ -41,7 +41,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
AnalyticsErrorWidget(state.errorMessage),
AnalyticsSidebarHeader(
title: 'Smart Power Clamp',
showSpaceUuid: true,
showSpaceUuidInDevicesDropdown: true,
onChanged: (device) {
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
context,

View File

@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
),
padding: const EdgeInsets.all(30),
child: Column(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
const Spacer(flex: 4),
],
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
TotalEnergyConsumptionChart(chartData: state.chartData),
],
),

View File

@ -16,7 +16,7 @@ class OccupancyChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
maxY: 100.0,
maxY: 100.001,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 20,
@ -134,7 +134,7 @@ class OccupancyChart extends StatelessWidget {
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: Text(
(value + 1).toString(),
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
fontSize: 8,

View File

@ -22,7 +22,6 @@ class OccupancyChartBox extends StatelessWidget {
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -65,7 +64,9 @@ class OccupancyChartBox extends StatelessWidget {
),
],
),
const Divider(height: 0),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Expanded(child: OccupancyChart(chartData: state.chartData)),
],
),

View File

@ -22,7 +22,6 @@ class OccupancyHeatMapBox extends StatelessWidget {
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -66,7 +65,9 @@ class OccupancyHeatMapBox extends StatelessWidget {
),
],
),
const Divider(height: 0),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map(

View File

@ -0,0 +1,9 @@
class GetAirQualityDistributionParam {
final DateTime date;
final String spaceUuid;
const GetAirQualityDistributionParam({
required this.date,
required this.spaceUuid,
});
}

View File

@ -2,18 +2,15 @@ class GetEnergyConsumptionPerDeviceParam {
const GetEnergyConsumptionPerDeviceParam({
this.monthDate,
this.spaceId,
this.communityId,
});
final DateTime? monthDate;
final String? spaceId;
final String? communityId;
Map<String, dynamic> toJson() => {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': true,
};
}

View File

@ -1,16 +1,12 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetRangeOfAqiParam extends Equatable {
final DateTime date;
final String spaceUuid;
final AqiType aqiType;
const GetRangeOfAqiParam(
{
const GetRangeOfAqiParam({
required this.date,
required this.spaceUuid,
required this.aqiType,
});
@override

View File

@ -1,12 +1,10 @@
class GetTotalEnergyConsumptionParam {
final DateTime? monthDate;
final String? spaceId;
final String? communityId;
const GetTotalEnergyConsumptionParam({
this.monthDate,
this.spaceId,
this.communityId,
});
Map<String, dynamic> toJson() {
@ -14,7 +12,6 @@ class GetTotalEnergyConsumptionParam {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': false,
};
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
abstract interface class AirQualityDistributionService {
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
);
}

View File

@ -0,0 +1,95 @@
import 'dart:math';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
class FakeAirQualityDistributionService implements AirQualityDistributionService {
final _random = Random();
@override
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
) async {
return Future.delayed(
const Duration(milliseconds: 400),
() => List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final values = _generateRandomPercentages();
final nullMask = List.generate(6, (_) => _shouldBeNull());
if (nullMask.every((isNull) => isNull)) {
nullMask[_random.nextInt(6)] = false;
}
final nonNullValues = _redistributePercentages(values, nullMask);
return AirQualityDataModel(
date: date,
data: [
AirQualityPercentageData(
type: AqiType.aqi.code,
percentage: nonNullValues[0],
name: 'good',
),
AirQualityPercentageData(
name: 'moderate',
type: AqiType.co2.code,
percentage: nonNullValues[1],
),
AirQualityPercentageData(
name: 'poor',
percentage: nonNullValues[2],
type: AqiType.hcho.code,
),
AirQualityPercentageData(
name: 'unhealthy',
percentage: nonNullValues[3],
type: AqiType.pm10.code,
),
AirQualityPercentageData(
name: 'severe',
type: AqiType.pm25.code,
percentage: nonNullValues[4],
),
AirQualityPercentageData(
name: 'hazardous',
percentage: nonNullValues[5],
type: AqiType.co2.code,
),
],
);
}),
);
}
List<double> _redistributePercentages(
List<double> originalValues,
List<bool> nullMask,
) {
double nonNullSum = 0;
for (int i = 0; i < originalValues.length; i++) {
if (!nullMask[i]) {
nonNullSum += originalValues[i];
}
}
return List.generate(originalValues.length, (i) {
if (nullMask[i]) return 0;
return (originalValues[i] / nonNullSum * 100).roundToDouble();
});
}
bool _shouldBeNull() => _random.nextDouble() < 0.6;
List<double> _generateRandomPercentages() {
final values = List.generate(6, (_) => _random.nextDouble());
final sum = values.reduce((a, b) => a + b);
return values.map((value) => (value / sum * 100).roundToDouble()).toList();
}
}

View File

@ -0,0 +1,36 @@
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteAirQualityDistributionService implements AirQualityDistributionService {
RemoteAirQualityDistributionService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return AirQualityDataModel.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -18,10 +19,15 @@ class FakeRangeOfAqiService implements RangeOfAqiService {
final avg = (min + avgDelta).clamp(0.0, 301.0);
final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi(
min: min,
avg: avg,
max: max,
return RangeOfAqi(
data: [
RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max),
],
date: date,
);
});

View File

@ -0,0 +1,34 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteRangeOfAqiService implements RangeOfAqiService {
const RemoteRangeOfAqiService(this._httpService);
final HTTPService _httpService;
@override
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
try {
final response = await _httpService.get(
path: 'endpoint',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return RangeOfAqi.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -11,14 +11,17 @@ class AnalyticsErrorWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Visibility(
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
child: Text(
errorMessage ?? 'Something went wrong',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400,
fontSize: 8,
child: Padding(
padding: const EdgeInsetsDirectional.only(bottom: 10),
child: Text(
errorMessage ?? 'Something went wrong',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400,
fontSize: 8,
),
),
),
);

View File

@ -10,13 +10,13 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsSidebarHeader extends StatelessWidget {
const AnalyticsSidebarHeader({
required this.title,
this.showSpaceUuid = false,
this.showSpaceUuidInDevicesDropdown = false,
this.onChanged,
super.key,
});
final String title;
final bool showSpaceUuid;
final bool showSpaceUuidInDevicesDropdown;
final void Function(AnalyticsDevice device)? onChanged;
@override
@ -49,6 +49,7 @@ class AnalyticsSidebarHeader extends StatelessWidget {
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AnalyticsDeviceDropdown(
showSpaceUuid: showSpaceUuidInDevicesDropdown,
onChanged: (value) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(value),

View File

@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/auth_api.dart';
import 'package:syncrow_web/utils/constants/strings_manager.dart';
import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart';
@ -99,7 +100,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
}
Future<void> changePassword(ChangePasswordEvent event, Emitter<AuthState> emit) async {
Future<void> changePassword(
ChangePasswordEvent event, Emitter<AuthState> emit) async {
emit(LoadingForgetState());
try {
var response = await AuthenticationAPI.verifyOtp(
@ -113,14 +115,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
emit(SuccessForgetState());
}
} on DioException catch (e) {
final errorData = e.response!.data;
String errorMessage = errorData['error']['message'] ?? 'something went wrong';
} on APIException catch (e) {
final errorMessage = e.message;
validate = errorMessage;
emit(AuthInitialState());
}
}
String? validateCode(String? value) {
if (value == null || value.isEmpty) {
return 'Code is required';
@ -149,6 +151,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
static UserModel? user;
bool showValidationMessage = false;
void _login(LoginButtonPressed event, Emitter<AuthState> emit) async {
emit(AuthLoading());
if (isChecked) {
@ -165,21 +168,20 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
password: event.password,
),
);
} on DioException catch (e) {
final errorData = e.response!.data;
String errorMessage = errorData['error']['message'];
if (errorMessage == "Access denied for web platform") {
validate = errorMessage;
} else {
validate = 'Invalid Credentials!';
}
} on APIException catch (e) {
validate = e.message;
emit(LoginInitial());
return;
} catch (e) {
validate = 'Something went wrong';
emit(LoginInitial());
return;
}
if (token.accessTokenIsNotEmpty) {
FlutterSecureStorage storage = const FlutterSecureStorage();
await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken);
await storage.write(
key: Token.loginAccessTokenKey, value: token.accessToken);
const FlutterSecureStorage().write(
key: UserModel.userUuidKey,
value: Token.decodeToken(token.accessToken)['uuid'].toString());
@ -195,6 +197,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
}
checkBoxToggle(
CheckBoxEvent event,
Emitter<AuthState> emit,

View File

@ -162,31 +162,34 @@ class _DynamicTableState extends State<DynamicTable> {
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _horizontalBodyScrollController,
child: SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex, widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
child: Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children: List.generate(widget.data.length,
(rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(rowIndex,
widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
),
),
),
@ -211,7 +214,6 @@ class _DynamicTableState extends State<DynamicTable> {
onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
),
);
}
@ -282,7 +284,6 @@ class _DynamicTableState extends State<DynamicTable> {
padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
@ -303,7 +304,6 @@ class _DynamicTableState extends State<DynamicTable> {
required int rowIndex,
required int columnIndex,
}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@ -313,9 +313,13 @@ class _DynamicTableState extends State<DynamicTable> {
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return _buildSettingsIcon(rowIndex, size);
return buildSettingsIcon(
width: 120,
height: 60,
iconSize: 40,
onTap: () => widget.onSettingsPressed?.call(rowIndex),
);
}
Color? statusColor;
switch (content) {
@ -368,22 +372,63 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildSettingsIcon(int rowIndex, double size) {
return Container(
height: size,
width: 120,
padding: const EdgeInsets.all(5.0),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0),
Widget buildSettingsIcon(
{double width = 120,
double height = 60,
double iconSize = 40,
VoidCallback? onTap}) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
margin: const EdgeInsets.only(right: 15),
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
),
),
width: width,
child: Padding(
padding: const EdgeInsets.only(
right: 16.0,
left: 17.0,
),
child: Container(
width: 50,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(height / 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.17),
blurRadius: 14,
offset: const Offset(0, 4),
),
],
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings, // ضع المسار الصحيح هنا
width: 40,
height: 22,
color: ColorsManager
.primaryColor, // نفس لون الأيقونة في الصورة
),
),
),
),
),
),
),
color: Colors.white,
),
alignment: Alignment.center,
child: IconButton(
icon: SvgPicture.asset(Assets.settings),
onPressed: () => widget.onSettingsPressed?.call(rowIndex),
),
],
);
}
}

View File

@ -1,21 +1,27 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart';
import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class AcBloc extends Bloc<AcsEvent, AcsState> {
late AcStatusModel deviceStatus;
final String deviceId;
Timer? _timer;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
Timer? _countdownTimer;
AcBloc({required this.deviceId}) : super(AcsInitialState()) {
AcBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(AcsInitialState()) {
on<AcFetchDeviceStatusEvent>(_onFetchAcStatus);
on<AcFetchBatchStatusEvent>(_onFetchAcBatchStatus);
on<AcControlEvent>(_onAcControl);
@ -34,14 +40,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
int scheduledMinutes = 0;
FutureOr<void> _onFetchAcStatus(
AcFetchDeviceStatusEvent event, Emitter<AcsState> emit) async {
AcFetchDeviceStatusEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status);
if (deviceStatus.countdown1 != 0) {
// Convert API value to minutes
final totalMinutes = deviceStatus.countdown1 * 6;
scheduledHours = totalMinutes ~/ 60;
scheduledMinutes = totalMinutes % 60;
@ -62,30 +68,24 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
}
_listenToChanges(deviceId) {
void _listenToChanges(deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
if (_timer != null) {
await Future.delayed(const Duration(seconds: 1));
}
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
AcStatusModel.fromJson(usersMap['productUuid'], statusList);
deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(AcStatusUpdated(deviceStatus));
}
@ -93,146 +93,44 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
} catch (_) {}
}
void _onAcStatusUpdated(AcStatusUpdated event, Emitter<AcsState> emit) {
void _onAcStatusUpdated(
AcStatusUpdated event,
Emitter<AcsState> emit,
) {
deviceStatus = event.deviceStatus;
emit(ACStatusLoaded(status: deviceStatus));
}
FutureOr<void> _onAcControl(
AcControlEvent event, Emitter<AcsState> emit) async {
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value, emit);
AcControlEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
await _runDebounce(
isBatch: false,
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
);
}
try {
final success = await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required dynamic value,
required dynamic oldValue,
required Emitter<AcsState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
if (e is DioException && e.response != null) {
debugPrint('Error response: ${e.response?.data}');
}
_revertValueAndEmit(id, code, oldValue, emit);
if (!success) {
emit(const AcsFailedState(error: 'Failed to control device'));
}
});
}
void _revertValueAndEmit(
String deviceId, String code, dynamic oldValue, Emitter<AcsState> emit) {
_updateLocalValue(code, oldValue, emit);
emit(ACStatusLoaded(status: deviceStatus));
}
void _updateLocalValue(String code, dynamic value, Emitter<AcsState> emit) {
switch (code) {
case 'switch':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(acSwitch: value);
}
break;
case 'temp_set':
if (value is int) {
deviceStatus = deviceStatus.copyWith(tempSet: value);
}
break;
case 'mode':
if (value is String) {
deviceStatus = deviceStatus.copyWith(
modeString: value,
);
}
break;
case 'level':
if (value is String) {
deviceStatus = deviceStatus.copyWith(
fanSpeedsString: value,
);
}
break;
case 'child_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(childLock: value);
}
case 'countdown_time':
if (value is int) {
deviceStatus = deviceStatus.copyWith(countdown1: value);
}
break;
default:
break;
}
emit(ACStatusLoaded(status: deviceStatus));
}
dynamic _getValueByCode(String code) {
switch (code) {
case 'switch':
return deviceStatus.acSwitch;
case 'temp_set':
return deviceStatus.tempSet;
case 'mode':
return deviceStatus.modeString;
case 'level':
return deviceStatus.fanSpeedsString;
case 'child_lock':
return deviceStatus.childLock;
case 'countdown_time':
return deviceStatus.countdown1;
default:
return null;
} catch (e) {
emit(AcsFailedState(error: e.toString()));
}
}
FutureOr<void> _onFetchAcBatchStatus(
AcFetchBatchStatusEvent event, Emitter<AcsState> emit) async {
AcFetchBatchStatusEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
AcStatusModel.fromJson(event.devicesIds.first, status.status);
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status);
emit(ACStatusLoaded(status: deviceStatus));
} catch (e) {
emit(AcsFailedState(error: e.toString()));
@ -240,25 +138,32 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
FutureOr<void> _onAcBatchControl(
AcBatchControlEvent event, Emitter<AcsState> emit) async {
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value, emit);
AcBatchControlEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
await _runDebounce(
isBatch: true,
deviceId: event.devicesIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
);
try {
final success = await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: event.value,
);
if (!success) {
emit(const AcsFailedState(error: 'Failed to control devices'));
}
} catch (e) {
emit(AcsFailedState(error: e.toString()));
}
}
FutureOr<void> _onFactoryReset(
AcFactoryResetEvent event, Emitter<AcsState> emit) async {
Future<void> _onFactoryReset(
AcFactoryResetEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
try {
final response = await DevicesManagementApi().factoryReset(
@ -275,9 +180,11 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
}
void _onClose(OnClose event, Emitter<AcsState> emit) {
void _onClose(
OnClose event,
Emitter<AcsState> emit,
) {
_countdownTimer?.cancel();
_timer?.cancel();
}
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter<AcsState> emit) {
@ -300,7 +207,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
));
}
void _handleDecreaseTime(DecreaseTimeEvent event, Emitter<AcsState> emit) {
void _handleDecreaseTime(
DecreaseTimeEvent event,
Emitter<AcsState> emit,
) {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
int totalMinutes = (scheduledHours * 60) + scheduledMinutes;
@ -315,7 +225,9 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
Future<void> _handleToggleTimer(
ToggleScheduleEvent event, Emitter<AcsState> emit) async {
ToggleScheduleEvent event,
Emitter<AcsState> emit,
) async {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
@ -331,37 +243,44 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
try {
final scaledValue = totalMinutes ~/ 6;
await _runDebounce(
isBatch: false,
deviceId: deviceId,
code: 'countdown_time',
value: scaledValue,
oldValue: scaledValue,
emit: emit,
final success = await controlDeviceService.controlDevice(
deviceUuid: deviceId,
status: Status(code: 'countdown_time', value: scaledValue),
);
_startCountdownTimer(emit);
emit(currentState.copyWith(isTimerActive: timerActive));
if (success) {
_startCountdownTimer(emit);
emit(currentState.copyWith(isTimerActive: timerActive));
} else {
timerActive = false;
emit(const AcsFailedState(error: 'Failed to set timer'));
}
} catch (e) {
timerActive = false;
emit(AcsFailedState(error: e.toString()));
}
} else {
await _runDebounce(
isBatch: false,
deviceId: deviceId,
code: 'countdown_time',
value: 0,
oldValue: 0,
emit: emit,
);
_countdownTimer?.cancel();
scheduledHours = 0;
scheduledMinutes = 0;
emit(currentState.copyWith(
isTimerActive: timerActive,
scheduledHours: 0,
scheduledMinutes: 0,
));
try {
final success = await controlDeviceService.controlDevice(
deviceUuid: deviceId,
status: Status(code: 'countdown_time', value: 0),
);
if (success) {
_countdownTimer?.cancel();
scheduledHours = 0;
scheduledMinutes = 0;
emit(currentState.copyWith(
isTimerActive: timerActive,
scheduledHours: 0,
scheduledMinutes: 0,
));
} else {
emit(const AcsFailedState(error: 'Failed to stop timer'));
}
} catch (e) {
emit(AcsFailedState(error: e.toString()));
}
}
}
@ -385,7 +304,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
});
}
void _handleUpdateTimer(UpdateTimerEvent event, Emitter<AcsState> emit) {
void _handleUpdateTimer(
UpdateTimerEvent event,
Emitter<AcsState> emit,
) {
if (state is ACStatusLoaded) {
final currentState = state as ACStatusLoaded;
emit(currentState.copyWith(
@ -400,7 +322,6 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
ApiCountdownValueEvent event, Emitter<AcsState> emit) {
if (state is ACStatusLoaded) {
final totalMinutes = event.apiValue * 6;
final scheduledHours = totalMinutes ~/ 60;
scheduledMinutes = totalMinutes % 60;
_startCountdownTimer(
emit,
@ -409,6 +330,43 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
}
void _updateDeviceFunctionFromCode(String code, dynamic value) {
switch (code) {
case 'switch':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(acSwitch: value);
}
break;
case 'temp_set':
if (value is int) {
deviceStatus = deviceStatus.copyWith(tempSet: value);
}
break;
case 'mode':
if (value is String) {
deviceStatus = deviceStatus.copyWith(modeString: value);
}
break;
case 'level':
if (value is String) {
deviceStatus = deviceStatus.copyWith(fanSpeedsString: value);
}
break;
case 'child_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(childLock: value);
}
break;
case 'countdown_time':
if (value is int) {
deviceStatus = deviceStatus.copyWith(countdown1: value);
}
break;
default:
break;
}
}
@override
Future<void> close() {
add(OnClose());

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart';
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
abstract final class AcBlocFactory {
const AcBlocFactory._();
static AcBloc create({
required String deviceId,
}) {
return AcBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart';
import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -26,8 +26,9 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) =>
AcBloc(deviceId: devicesIds.first)..add(AcFetchBatchStatusEvent(devicesIds)),
create: (context) => AcBlocFactory.create(
deviceId: devicesIds.first,
)..add(AcFetchBatchStatusEvent(devicesIds)),
child: BlocBuilder<AcBloc, AcsState>(
builder: (context, state) {
if (state is ACStatusLoaded) {

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart';
import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart';
import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_mode.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/control_list/current_temp.dart';
import 'package:syncrow_web/pages/device_managment/ac/view/control_list/fan_speed.dart';
@ -24,8 +25,9 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout {
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) => AcBloc(deviceId: device.uuid!)
..add(AcFetchDeviceStatusEvent(device.uuid!)),
create: (context) => AcBlocFactory.create(
deviceId: device.uuid!,
)..add(AcFetchDeviceStatusEvent(device.uuid!)),
child: BlocBuilder<AcBloc, AcsState>(
builder: (context, state) {
final acBloc = BlocProvider.of<AcBloc>(context);

View File

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
@ -7,14 +5,21 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_e
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class CeilingSensorBloc extends Bloc<CeilingSensorEvent, CeilingSensorState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late CeilingSensorModel deviceStatus;
Timer? _timer;
CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) {
CeilingSensorBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(CeilingInitialState()) {
on<CeilingInitialEvent>(_fetchCeilingSensorStatus);
on<CeilingFetchDeviceStatusEvent>(_fetchCeilingSensorBatchControl);
on<CeilingChangeValueEvent>(_changeValue);
@ -26,35 +31,34 @@ class CeilingSensorBloc extends Bloc<CeilingSensorEvent, CeilingSensorState> {
on<StatusUpdated>(_onStatusUpdated);
}
void _fetchCeilingSensorStatus(
CeilingInitialEvent event, Emitter<CeilingSensorState> emit) async {
Future<void> _fetchCeilingSensorStatus(
CeilingInitialEvent event,
Emitter<CeilingSensorState> emit,
) async {
emit(CeilingLoadingInitialState());
try {
var response =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
final response = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = CeilingSensorModel.fromJson(response.status);
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
_listenToChanges(event.deviceId);
} catch (e) {
emit(CeilingFailedState(error: e.toString()));
return;
}
}
_listenToChanges(deviceId) {
void _listenToChanges(String deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
if (event.snapshot.value == null) return;
final usersMap = event.snapshot.value as Map<dynamic, dynamic>;
final statusList = <Status>[];
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = CeilingSensorModel.fromJson(statusList);
@ -65,149 +69,127 @@ class CeilingSensorBloc extends Bloc<CeilingSensorEvent, CeilingSensorState> {
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<CeilingSensorState> emit) {
void _onStatusUpdated(
StatusUpdated event,
Emitter<CeilingSensorState> emit,
) {
deviceStatus = event.deviceStatus;
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
}
void _changeValue(
CeilingChangeValueEvent event, Emitter<CeilingSensorState> emit) async {
Future<void> _changeValue(
CeilingChangeValueEvent event,
Emitter<CeilingSensorState> emit,
) async {
emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus));
if (event.code == 'sensitivity') {
deviceStatus.sensitivity = event.value;
} else if (event.code == 'none_body_time') {
deviceStatus.noBodyTime = event.value;
} else if (event.code == 'moving_max_dis') {
deviceStatus.maxDistance = event.value;
} else if (event.code == 'scene') {
deviceStatus.spaceType = getSpaceType(event.value);
}
_updateDeviceFunctionFromCode(event.code, event.value);
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
await _runDeBouncer(
deviceId: deviceId,
code: event.code,
value: event.value,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
emit(CeilingFailedState(error: e.toString()));
}
}
Future<void> _onBatchControl(
CeilingBatchControlEvent event, Emitter<CeilingSensorState> emit) async {
CeilingBatchControlEvent event,
Emitter<CeilingSensorState> emit,
) async {
emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus));
if (event.code == 'sensitivity') {
deviceStatus.sensitivity = event.value;
} else if (event.code == 'none_body_time') {
deviceStatus.noBodyTime = event.value;
} else if (event.code == 'moving_max_dis') {
deviceStatus.maxDistance = event.value;
} else if (event.code == 'scene') {
deviceStatus.spaceType = getSpaceType(event.value);
}
_updateDeviceFunctionFromCode(event.code, event.value);
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
await _runDeBouncer(
deviceId: event.deviceIds,
code: event.code,
value: event.value,
emit: emit,
isBatch: true,
);
}
_runDeBouncer({
required dynamic deviceId,
required String code,
required dynamic value,
required Emitter<CeilingSensorState> emit,
required bool isBatch,
}) {
late String id;
try {
final success = await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
add(CeilingInitialEvent(id));
}
if (response == true && code == 'scene') {
emit(CeilingLoadingInitialState());
await Future.delayed(const Duration(seconds: 1));
add(CeilingInitialEvent(id));
}
} catch (_) {
await Future.delayed(const Duration(milliseconds: 500));
add(CeilingInitialEvent(id));
if (!success) {
emit(const CeilingFailedState(error: 'Failed to control devices'));
}
});
} catch (e) {
emit(CeilingFailedState(error: e.toString()));
}
}
FutureOr<void> _getDeviceReports(GetCeilingDeviceReportsEvent event,
Emitter<CeilingSensorState> emit) async {
void _updateDeviceFunctionFromCode(String code, dynamic value) {
switch (code) {
case 'sensitivity':
deviceStatus.sensitivity = value;
break;
case 'none_body_time':
deviceStatus.noBodyTime = value;
break;
case 'moving_max_dis':
deviceStatus.maxDistance = value;
break;
case 'scene':
deviceStatus.spaceType = getSpaceType(value);
break;
default:
break;
}
}
Future<void> _getDeviceReports(
GetCeilingDeviceReportsEvent event,
Emitter<CeilingSensorState> emit,
) async {
if (event.code.isEmpty) {
emit(ShowCeilingDescriptionState(description: reportString));
return;
} else {
emit(CeilingReportsLoadingState());
// final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch;
// final to = DateTime.now().millisecondsSinceEpoch;
}
try {
// await DevicesManagementApi.getDeviceReportsByDate(deviceId, event.code, from.toString(), to.toString())
await DevicesManagementApi.getDeviceReports(deviceId, event.code)
.then((value) {
emit(CeilingReportsState(deviceReport: value));
});
} catch (e) {
emit(CeilingReportsFailedState(error: e.toString()));
return;
}
emit(CeilingReportsLoadingState());
try {
final value = await DevicesManagementApi.getDeviceReports(
deviceId,
event.code,
);
emit(CeilingReportsState(deviceReport: value));
} catch (e) {
emit(CeilingReportsFailedState(error: e.toString()));
}
}
void _showDescription(
ShowCeilingDescriptionEvent event, Emitter<CeilingSensorState> emit) {
ShowCeilingDescriptionEvent event,
Emitter<CeilingSensorState> emit,
) {
emit(ShowCeilingDescriptionState(description: event.description));
}
void _backToGridView(
BackToCeilingGridViewEvent event, Emitter<CeilingSensorState> emit) {
BackToCeilingGridViewEvent event,
Emitter<CeilingSensorState> emit,
) {
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
}
FutureOr<void> _fetchCeilingSensorBatchControl(
CeilingFetchDeviceStatusEvent event,
Emitter<CeilingSensorState> emit) async {
Future<void> _fetchCeilingSensorBatchControl(
CeilingFetchDeviceStatusEvent event,
Emitter<CeilingSensorState> emit,
) async {
emit(CeilingLoadingInitialState());
try {
var response =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final response = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = CeilingSensorModel.fromJson(response.status);
emit(CeilingUpdateState(ceilingSensorModel: deviceStatus));
} catch (e) {
emit(CeilingFailedState(error: e.toString()));
return;
}
}
FutureOr<void> _onFactoryReset(
CeilingFactoryResetEvent event, Emitter<CeilingSensorState> emit) async {
Future<void> _onFactoryReset(
CeilingFactoryResetEvent event,
Emitter<CeilingSensorState> emit,
) async {
emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus));
try {
final response = await DevicesManagementApi().factoryReset(

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart';
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
abstract final class CeilingSensorBlocFactory {
const CeilingSensorBlocFactory._();
static CeilingSensorBloc create({
required String deviceId,
}) {
return CeilingSensorBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart';
@ -23,8 +23,9 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) => CeilingSensorBloc(deviceId: devicesIds.first)
..add(CeilingFetchDeviceStatusEvent(devicesIds)),
create: (context) => CeilingSensorBlocFactory.create(
deviceId: devicesIds.first,
)..add(CeilingFetchDeviceStatusEvent(devicesIds)),
child: BlocBuilder<CeilingSensorBloc, CeilingSensorState>(
builder: (context, state) {
if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) {
@ -110,7 +111,6 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv
),
),
),
// FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4),
FactoryResetWidget(
callFactoryReset: () {
context.read<CeilingSensorBloc>().add(

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart';
@ -28,8 +29,9 @@ class CeilingSensorControlsView extends StatelessWidget
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) => CeilingSensorBloc(deviceId: device.uuid ?? '')
..add(CeilingInitialEvent(device.uuid ?? '')),
create: (context) => CeilingSensorBlocFactory.create(
deviceId: device.uuid ?? '',
)..add(CeilingInitialEvent(device.uuid ?? '')),
child: BlocBuilder<CeilingSensorBloc, CeilingSensorState>(
builder: (context, state) {
if (state is CeilingLoadingInitialState ||

View File

@ -1,17 +1,25 @@
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class CurtainBloc extends Bloc<CurtainEvent, CurtainState> {
late bool deviceStatus;
final String deviceId;
Timer? _timer;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
CurtainBloc({required this.deviceId}) : super(CurtainInitial()) {
CurtainBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(CurtainInitial()) {
on<CurtainFetchDeviceStatus>(_onFetchDeviceStatus);
on<CurtainFetchBatchStatus>(_onFetchBatchStatus);
on<CurtainControl>(_onCurtainControl);
@ -20,32 +28,31 @@ class CurtainBloc extends Bloc<CurtainEvent, CurtainState> {
on<StatusUpdated>(_onStatusUpdated);
}
FutureOr<void> _onFetchDeviceStatus(
CurtainFetchDeviceStatus event, Emitter<CurtainState> emit) async {
Future<void> _onFetchDeviceStatus(
CurtainFetchDeviceStatus event,
Emitter<CurtainState> emit,
) async {
emit(CurtainStatusLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus = _checkStatus(status.status[0].value);
emit(CurtainStatusLoaded(deviceStatus));
} catch (e) {
emit(CurtainError(e.toString()));
}
}
void _listenToChanges(String deviceId) {
void _listenToChanges(String deviceId, Emitter<CurtainState> emit) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
List<Status> statusList = [];
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
@ -57,7 +64,7 @@ class CurtainBloc extends Bloc<CurtainEvent, CurtainState> {
}
}
if (statusList.isNotEmpty) {
bool newStatus = _checkStatus(statusList[0].value);
final newStatus = _checkStatus(statusList[0].value);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
@ -71,76 +78,32 @@ class CurtainBloc extends Bloc<CurtainEvent, CurtainState> {
}
}
void _onStatusUpdated(StatusUpdated event, Emitter<CurtainState> emit) {
void _onStatusUpdated(
StatusUpdated event,
Emitter<CurtainState> emit,
) {
emit(CurtainStatusLoading());
deviceStatus = event.deviceStatus;
emit(CurtainStatusLoaded(deviceStatus));
}
FutureOr<void> _onCurtainControl(
CurtainControl event, Emitter<CurtainState> emit) async {
final oldValue = deviceStatus;
Future<void> _onCurtainControl(
CurtainControl event,
Emitter<CurtainState> emit,
) async {
emit(CurtainStatusLoading());
_updateLocalValue(event.value, emit);
emit(CurtainStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<CurtainState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
try {
final controlValue = event.value ? 'open' : 'close';
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: controlValue),
);
} catch (e) {
_updateLocalValue(!event.value, emit);
emit(CurtainControlError(e.toString()));
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
final controlValue = value ? 'open' : 'close';
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, controlValue);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: controlValue));
}
if (!response) {
_revertValueAndEmit(id, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, oldValue, emit);
}
});
}
void _revertValueAndEmit(
String deviceId, bool oldValue, Emitter<CurtainState> emit) {
_updateLocalValue(oldValue, emit);
emit(CurtainStatusLoaded(deviceStatus));
emit(const CurtainControlError('Failed to control the device.'));
}
void _updateLocalValue(bool value, Emitter<CurtainState> emit) {
@ -152,41 +115,44 @@ class CurtainBloc extends Bloc<CurtainEvent, CurtainState> {
return command.toLowerCase() == 'open';
}
FutureOr<void> _onFetchBatchStatus(
CurtainFetchBatchStatus event, Emitter<CurtainState> emit) async {
Future<void> _onFetchBatchStatus(
CurtainFetchBatchStatus event,
Emitter<CurtainState> emit,
) async {
emit(CurtainStatusLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = _checkStatus(status.status[0].value);
emit(CurtainStatusLoaded(deviceStatus));
} catch (e) {
emit(CurtainError(e.toString()));
}
}
FutureOr<void> _onCurtainBatchControl(
CurtainBatchControl event, Emitter<CurtainState> emit) async {
final oldValue = deviceStatus;
Future<void> _onCurtainBatchControl(
CurtainBatchControl event,
Emitter<CurtainState> emit,
) async {
emit(CurtainStatusLoading());
_updateLocalValue(event.value, emit);
emit(CurtainStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.devicesIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
try {
final controlValue = event.value ? 'open' : 'stop';
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: controlValue,
);
} catch (e) {
_updateLocalValue(!event.value, emit);
emit(CurtainControlError(e.toString()));
}
}
FutureOr<void> _onFactoryReset(
CurtainFactoryReset event, Emitter<CurtainState> emit) async {
Future<void> _onFactoryReset(
CurtainFactoryReset event,
Emitter<CurtainState> emit,
) async {
emit(CurtainStatusLoading());
try {
final response = await DevicesManagementApi().factoryReset(

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart';
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
abstract final class CurtainBlocFactory {
const CurtainBlocFactory._();
static CurtainBloc create({
required String deviceId,
}) {
return CurtainBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart';
import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -18,7 +19,7 @@ class CurtainBatchStatusView extends StatelessWidget with HelperResponsiveLayout
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
CurtainBloc(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)),
CurtainBlocFactory.create(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)),
child: BlocBuilder<CurtainBloc, CurtainState>(
builder: (context, state) {
if (state is CurtainStatusLoading) {

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/common/curtain_toggle.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart';
import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart';
import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class CurtainStatusControlsView extends StatelessWidget
@ -15,7 +16,7 @@ class CurtainStatusControlsView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CurtainBloc(deviceId: deviceId)
create: (context) => CurtainBlocFactory.create(deviceId: deviceId)
..add(CurtainFetchDeviceStatus(deviceId)),
child: BlocBuilder<CurtainBloc, CurtainState>(
builder: (context, state) {

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
abstract final class DeviceBlocDependenciesFactory {
const DeviceBlocDependenciesFactory._();
static ControlDeviceService createControlDeviceService() {
return DebouncedControlDeviceService(
decoratee: RemoteControlDeviceService(),
);
}
static BatchControlDevicesService createBatchControlDevicesService() {
return DebouncedBatchControlDevicesService(
decoratee: RemoteBatchControlDevicesService(),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
abstract final class FlushMountedPresenceSensorBlocFactory {
const FlushMountedPresenceSensorBlocFactory._();
@ -10,12 +9,8 @@ abstract final class FlushMountedPresenceSensorBlocFactory {
}) {
return FlushMountedPresenceSensorBloc(
deviceId: deviceId,
controlDeviceService: DebouncedControlDeviceService(
decoratee: RemoteControlDeviceService(),
),
batchControlDevicesService: DebouncedBatchControlDevicesService(
decoratee: RemoteBatchControlDevicesService(),
),
controlDeviceService: DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService: DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -1,11 +1,13 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'one_gang_glass_switch_event.dart';
@ -13,13 +15,16 @@ part 'one_gang_glass_switch_state.dart';
class OneGangGlassSwitchBloc
extends Bloc<OneGangGlassSwitchEvent, OneGangGlassSwitchState> {
OneGangGlassStatusModel deviceStatus;
Timer? _timer;
late OneGangGlassStatusModel deviceStatus;
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
OneGangGlassSwitchBloc({required String deviceId})
: deviceStatus = OneGangGlassStatusModel(
uuid: deviceId, switch1: false, countDown: 0),
super(OneGangGlassSwitchInitial()) {
OneGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(OneGangGlassSwitchInitial()) {
on<OneGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<OneGangGlassSwitchControl>(_onControl);
on<OneGangGlassSwitchBatchControl>(_onBatchControl);
@ -28,160 +33,140 @@ class OneGangGlassSwitchBloc
on<StatusUpdated>(_onStatusUpdated);
}
Future<void> _onFetchDeviceStatus(OneGangGlassSwitchFetchDeviceEvent event,
Emitter<OneGangGlassSwitchState> emit) async {
Future<void> _onFetchDeviceStatus(
OneGangGlassSwitchFetchDeviceEvent event,
Emitter<OneGangGlassSwitchState> emit,
) async {
emit(OneGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId);
deviceStatus =
OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(OneGangGlassSwitchError(e.toString()));
}
}
_listenToChanges(deviceId) {
void _listenToChanges(
String deviceId,
Emitter<OneGangGlassSwitchState> emit,
) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = OneGangGlassStatusModel.fromJson(
usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
});
} catch (_) {}
} catch (e) {
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
}
}
void _onStatusUpdated(
StatusUpdated event, Emitter<OneGangGlassSwitchState> emit) {
StatusUpdated event,
Emitter<OneGangGlassSwitchState> emit,
) {
emit(OneGangGlassSwitchLoading());
deviceStatus = event.deviceStatus;
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
}
Future<void> _onControl(OneGangGlassSwitchControl event,
Emitter<OneGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onControl(
OneGangGlassSwitchControl event,
Emitter<OneGangGlassSwitchState> emit,
) async {
emit(OneGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
}
Future<void> _onFactoryReset(OneGangGlassFactoryResetEvent event,
Emitter<OneGangGlassSwitchState> emit) async {
emit(OneGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi()
.factoryReset(event.factoryReset, event.deviceId);
if (!response) {
emit(OneGangGlassSwitchError('Failed to reset device'));
} else {
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
}
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(OneGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(OneGangGlassSwitchBatchControl event,
Emitter<OneGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onBatchControl(
OneGangGlassSwitchBatchControl event,
Emitter<OneGangGlassSwitchState> emit,
) async {
emit(OneGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(OneGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
OneGangGlassSwitchFetchBatchStatusEvent event,
Emitter<OneGangGlassSwitchState> emit) async {
OneGangGlassSwitchFetchBatchStatusEvent event,
Emitter<OneGangGlassSwitchState> emit,
) async {
emit(OneGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = OneGangGlassStatusModel.fromJson(
event.deviceIds.first, status.status);
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus =
OneGangGlassStatusModel.fromJson(event.deviceIds.first, status.status);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(OneGangGlassSwitchError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<OneGangGlassSwitchState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(milliseconds: 500), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
Future<void> _onFactoryReset(
OneGangGlassFactoryResetEvent event,
Emitter<OneGangGlassSwitchState> emit,
) async {
emit(OneGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(OneGangGlassSwitchError('Failed to reset device'));
} else {
add(OneGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
});
}
void _revertValueAndEmit(String deviceId, String code, bool oldValue,
Emitter<OneGangGlassSwitchState> emit) {
_updateLocalValue(code, oldValue);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(OneGangGlassSwitchError(e.toString()));
}
}
void _updateLocalValue(String code, bool value) {
@ -189,19 +174,4 @@ class OneGangGlassSwitchBloc
deviceStatus = deviceStatus.copyWith(switch1: value);
}
}
bool _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
default:
return false;
}
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart';
abstract final class OneGangGlassSwitchBlocFactory {
const OneGangGlassSwitchBlocFactory._();
static OneGangGlassSwitchBloc create({
required String deviceId,
}) {
return OneGangGlassSwitchBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -16,7 +16,7 @@ class OneGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => OneGangGlassSwitchBloc(deviceId: deviceIds.first)
create: (context) => OneGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(OneGangGlassSwitchFetchBatchStatusEvent(deviceIds)),
child: BlocBuilder<OneGangGlassSwitchBloc, OneGangGlassSwitchState>(
builder: (context, state) {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -9,13 +10,13 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la
class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout {
final String deviceId;
const OneGangGlassSwitchControlView({required this.deviceId, Key? key}) : super(key: key);
const OneGangGlassSwitchControlView({required this.deviceId, super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
OneGangGlassSwitchBloc(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)),
OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<OneGangGlassSwitchBloc, OneGangGlassSwitchState>(
builder: (context, state) {
if (state is OneGangGlassSwitchLoading) {

View File

@ -6,12 +6,21 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class WallLightSwitchBloc
extends Bloc<WallLightSwitchEvent, WallLightSwitchState> {
WallLightSwitchBloc({required this.deviceId})
: super(WallLightSwitchInitial()) {
class WallLightSwitchBloc extends Bloc<WallLightSwitchEvent, WallLightSwitchState> {
late WallLightStatusModel deviceStatus;
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
WallLightSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(WallLightSwitchInitial()) {
on<WallLightSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<WallLightSwitchControl>(_onControl);
on<WallLightSwitchFetchBatchEvent>(_onFetchBatchStatus);
@ -20,143 +29,114 @@ class WallLightSwitchBloc
on<StatusUpdated>(_onStatusUpdated);
}
late WallLightStatusModel deviceStatus;
final String deviceId;
Timer? _timer;
FutureOr<void> _onFetchDeviceStatus(WallLightSwitchFetchDeviceEvent event,
Emitter<WallLightSwitchState> emit) async {
Future<void> _onFetchDeviceStatus(
WallLightSwitchFetchDeviceEvent event,
Emitter<WallLightSwitchState> emit,
) async {
emit(WallLightSwitchLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus =
WallLightStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus = WallLightStatusModel.fromJson(event.deviceId, status.status);
emit(WallLightSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(WallLightSwitchError(e.toString()));
}
}
_listenToChanges(deviceId) {
void _listenToChanges(
String deviceId,
Emitter<WallLightSwitchState> emit,
) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
WallLightStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = WallLightStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
});
} catch (_) {}
} catch (e) {
emit(WallLightSwitchError('Failed to listen to changes: $e'));
}
}
void _onStatusUpdated(
StatusUpdated event, Emitter<WallLightSwitchState> emit) {
StatusUpdated event,
Emitter<WallLightSwitchState> emit,
) {
emit(WallLightSwitchLoading());
deviceStatus = event.deviceStatus;
emit(WallLightSwitchStatusLoaded(deviceStatus));
}
FutureOr<void> _onControl(
WallLightSwitchControl event, Emitter<WallLightSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onControl(
WallLightSwitchControl event,
Emitter<WallLightSwitchState> emit,
) async {
emit(WallLightSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(WallLightSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(WallLightSwitchError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<WallLightSwitchState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(milliseconds: 500), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(String deviceId, String code, bool oldValue,
Emitter<WallLightSwitchState> emit) {
_updateLocalValue(code, oldValue);
Future<void> _onBatchControl(
WallLightSwitchBatchControl event,
Emitter<WallLightSwitchState> emit,
) async {
emit(WallLightSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(WallLightSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
if (code == 'switch_1') {
deviceStatus = deviceStatus.copyWith(switch1: value);
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(WallLightSwitchError(e.toString()));
}
}
bool _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
default:
return false;
}
}
Future<void> _onFetchBatchStatus(WallLightSwitchFetchBatchEvent event,
Emitter<WallLightSwitchState> emit) async {
Future<void> _onFetchBatchStatus(
WallLightSwitchFetchBatchEvent event,
Emitter<WallLightSwitchState> emit,
) async {
emit(WallLightSwitchLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
WallLightStatusModel.fromJson(event.devicesIds.first, status.status);
emit(WallLightSwitchStatusLoaded(deviceStatus));
@ -165,32 +145,10 @@ class WallLightSwitchBloc
}
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
FutureOr<void> _onBatchControl(WallLightSwitchBatchControl event,
Emitter<WallLightSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value);
emit(WallLightSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.devicesIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
}
FutureOr<void> _onFactoryReset(
WallLightFactoryReset event, Emitter<WallLightSwitchState> emit) async {
Future<void> _onFactoryReset(
WallLightFactoryReset event,
Emitter<WallLightSwitchState> emit,
) async {
emit(WallLightSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
@ -198,12 +156,18 @@ class WallLightSwitchBloc
event.deviceId,
);
if (!response) {
emit(WallLightSwitchError('Failed'));
emit(WallLightSwitchError('Failed to reset device'));
} else {
emit(WallLightSwitchStatusLoaded(deviceStatus));
add(WallLightSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(WallLightSwitchError(e.toString()));
}
}
void _updateLocalValue(String code, bool value) {
if (code == 'switch_1') {
deviceStatus = deviceStatus.copyWith(switch1: value);
}
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart';
abstract final class WallLightSwitchBlocFactory {
const WallLightSwitchBlocFactory._();
static WallLightSwitchBloc create({
required String deviceId,
}) {
return WallLightSwitchBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -18,7 +18,7 @@ class WallLightBatchControlView extends StatelessWidget with HelperResponsiveLay
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WallLightSwitchBloc(deviceId: deviceIds.first)
create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(WallLightSwitchFetchBatchEvent(deviceIds)),
child: BlocBuilder<WallLightSwitchBloc, WallLightSwitchState>(
builder: (context, state) {

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -15,7 +16,7 @@ class WallLightDeviceControl extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WallLightSwitchBloc(deviceId: deviceId)
create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceId)
..add(WallLightSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<WallLightSwitchBloc, WallLightSwitchState>(
builder: (context, state) {

View File

@ -157,7 +157,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
),
),
const SizedBox(width: 10),
Text(
SelectableText(
value,
style: TextStyle(
fontSize: 16,

View File

@ -1,11 +1,14 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'three_gang_glass_switch_event.dart';
@ -13,19 +16,16 @@ part 'three_gang_glass_switch_state.dart';
class ThreeGangGlassSwitchBloc
extends Bloc<ThreeGangGlassSwitchEvent, ThreeGangGlassSwitchState> {
ThreeGangGlassStatusModel deviceStatus;
Timer? _timer;
late ThreeGangGlassStatusModel deviceStatus;
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
ThreeGangGlassSwitchBloc({required String deviceId})
: deviceStatus = ThreeGangGlassStatusModel(
uuid: deviceId,
switch1: false,
countDown1: 0,
switch2: false,
countDown2: 0,
switch3: false,
countDown3: 0),
super(ThreeGangGlassSwitchInitial()) {
ThreeGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(ThreeGangGlassSwitchInitial()) {
on<ThreeGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<ThreeGangGlassSwitchControl>(_onControl);
on<ThreeGangGlassSwitchBatchControl>(_onBatchControl);
@ -34,188 +34,154 @@ class ThreeGangGlassSwitchBloc
on<StatusUpdated>(_onStatusUpdated);
}
Future<void> _onFetchDeviceStatus(ThreeGangGlassSwitchFetchDeviceEvent event,
Emitter<ThreeGangGlassSwitchState> emit) async {
Future<void> _onFetchDeviceStatus(
ThreeGangGlassSwitchFetchDeviceEvent event,
Emitter<ThreeGangGlassSwitchState> emit,
) async {
emit(ThreeGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus =
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(ThreeGangGlassSwitchError(e.toString()));
}
}
_listenToChanges(deviceId) {
void _listenToChanges(
String deviceId,
Emitter<ThreeGangGlassSwitchState> emit,
) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = ThreeGangGlassStatusModel.fromJson(
usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
});
} catch (_) {}
} catch (e) {
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
}
}
void _onStatusUpdated(
StatusUpdated event, Emitter<ThreeGangGlassSwitchState> emit) {
StatusUpdated event,
Emitter<ThreeGangGlassSwitchState> emit,
) {
emit(ThreeGangGlassSwitchLoading());
deviceStatus = event.deviceStatus;
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
}
Future<void> _onControl(ThreeGangGlassSwitchControl event,
Emitter<ThreeGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onControl(
ThreeGangGlassSwitchControl event,
Emitter<ThreeGangGlassSwitchState> emit,
) async {
emit(ThreeGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(ThreeGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(ThreeGangGlassSwitchBatchControl event,
Emitter<ThreeGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onBatchControl(
ThreeGangGlassSwitchBatchControl event,
Emitter<ThreeGangGlassSwitchState> emit,
) async {
emit(ThreeGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(ThreeGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
ThreeGangGlassSwitchFetchBatchStatusEvent event,
Emitter<ThreeGangGlassSwitchState> emit) async {
ThreeGangGlassSwitchFetchBatchStatusEvent event,
Emitter<ThreeGangGlassSwitchState> emit,
) async {
emit(ThreeGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = ThreeGangGlassStatusModel.fromJson(
event.deviceIds.first, status.status);
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus =
ThreeGangGlassStatusModel.fromJson(event.deviceIds.first, status.status);
emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(ThreeGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFactoryReset(ThreeGangGlassFactoryReset event,
Emitter<ThreeGangGlassSwitchState> emit) async {
Future<void> _onFactoryReset(
ThreeGangGlassFactoryReset event,
Emitter<ThreeGangGlassSwitchState> emit,
) async {
emit(ThreeGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi()
.factoryReset(event.factoryReset, event.deviceId);
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(ThreeGangGlassSwitchError('Failed'));
emit(ThreeGangGlassSwitchError('Failed to reset device'));
} else {
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
add(ThreeGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(ThreeGangGlassSwitchError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<ThreeGangGlassSwitchState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(milliseconds: 500), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(String deviceId, String code, bool oldValue,
Emitter<ThreeGangGlassSwitchState> emit) {
_updateLocalValue(code, oldValue);
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
if (code == 'switch_1') {
deviceStatus = deviceStatus.copyWith(switch1: value);
} else if (code == 'switch_2') {
deviceStatus = deviceStatus.copyWith(switch2: value);
} else if (code == 'switch_3') {
deviceStatus = deviceStatus.copyWith(switch3: value);
}
}
bool _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
return deviceStatus.switch2;
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
case 'switch_3':
return deviceStatus.switch3;
default:
return false;
deviceStatus = deviceStatus.copyWith(switch3: value);
break;
}
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
}

View File

@ -1,7 +1,10 @@
part of 'three_gang_glass_switch_bloc.dart';
@immutable
abstract class ThreeGangGlassSwitchEvent {}
abstract class ThreeGangGlassSwitchEvent extends Equatable {
@override
List<Object?> get props => [];
}
class ThreeGangGlassSwitchFetchDeviceEvent extends ThreeGangGlassSwitchEvent {
final String deviceId;
@ -19,6 +22,9 @@ class ThreeGangGlassSwitchControl extends ThreeGangGlassSwitchEvent {
required this.code,
required this.value,
});
@override
List<Object?> get props => [deviceId, code, value];
}
class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent {
@ -31,6 +37,9 @@ class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent {
required this.code,
required this.value,
});
@override
List<Object?> get props => [deviceIds, code, value];
}
class ThreeGangGlassSwitchFetchBatchStatusEvent
@ -38,6 +47,9 @@ class ThreeGangGlassSwitchFetchBatchStatusEvent
final List<String> deviceIds;
ThreeGangGlassSwitchFetchBatchStatusEvent(this.deviceIds);
@override
List<Object?> get props => [deviceIds];
}
class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent {
@ -48,6 +60,9 @@ class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent {
required this.deviceId,
required this.factoryReset,
});
@override
List<Object?> get props => [deviceId, factoryReset];
}
class StatusUpdated extends ThreeGangGlassSwitchEvent {

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart';
abstract final class ThreeGangGlassSwitchBlocFactory {
const ThreeGangGlassSwitchBlocFactory._();
static ThreeGangGlassSwitchBloc create({
required String deviceId,
}) {
return ThreeGangGlassSwitchBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -16,7 +17,7 @@ class ThreeGangGlassSwitchBatchControlView extends StatelessWidget with HelperRe
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ThreeGangGlassSwitchBloc(deviceId: deviceIds.first)
create: (context) => ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(ThreeGangGlassSwitchFetchBatchStatusEvent(deviceIds)),
child: BlocBuilder<ThreeGangGlassSwitchBloc, ThreeGangGlassSwitchState>(
builder: (context, state) {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -16,7 +17,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ThreeGangGlassSwitchBloc(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)),
ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<ThreeGangGlassSwitchBloc, ThreeGangGlassSwitchState>(
builder: (context, state) {
if (state is ThreeGangGlassSwitchLoading) {

View File

@ -1,12 +1,14 @@
// ignore_for_file: invalid_use_of_visible_for_testing_member
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'dart:developer';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'living_room_event.dart';
@ -15,9 +17,14 @@ part 'living_room_state.dart';
class LivingRoomBloc extends Bloc<LivingRoomEvent, LivingRoomState> {
late LivingRoomStatusModel deviceStatus;
final String deviceId;
Timer? _timer;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
LivingRoomBloc({required this.deviceId}) : super(LivingRoomInitial()) {
LivingRoomBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(LivingRoomInitial()) {
on<LivingRoomFetchDeviceStatusEvent>(_onFetchDeviceStatus);
on<LivingRoomControl>(_livingRoomControl);
on<LivingRoomBatchControl>(_livingRoomBatchControl);
@ -26,156 +33,108 @@ class LivingRoomBloc extends Bloc<LivingRoomEvent, LivingRoomState> {
on<StatusUpdated>(_onStatusUpdated);
}
FutureOr<void> _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event,
Emitter<LivingRoomState> emit) async {
Future<void> _onFetchDeviceStatus(
LivingRoomFetchDeviceStatusEvent event,
Emitter<LivingRoomState> emit,
) async {
emit(LivingRoomDeviceStatusLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus =
LivingRoomStatusModel.fromJson(event.deviceId, status.status);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(deviceId);
deviceStatus = LivingRoomStatusModel.fromJson(event.deviceId, status.status);
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
} catch (e) {
emit(LivingRoomDeviceManagementError(e.toString()));
}
}
FutureOr<void> _livingRoomControl(
LivingRoomControl event, Emitter<LivingRoomState> emit) async {
final oldValue = _getValueByCode(event.code);
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(
Status(code: element['code'], value: element['value']),
);
});
deviceStatus = LivingRoomStatusModel.fromJson(deviceId, statusList);
add(StatusUpdated(deviceStatus));
});
} catch (_) {
log('Error listening to changes');
}
}
void _onStatusUpdated(
StatusUpdated event,
Emitter<LivingRoomState> emit,
) {
deviceStatus = event.deviceStatus;
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
}
Future<void> _livingRoomControl(
LivingRoomControl event,
Emitter<LivingRoomState> emit,
) async {
emit(LivingRoomDeviceStatusLoading());
_updateLocalValue(event.code, event.value);
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(LivingRoomDeviceManagementError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required dynamic value,
required dynamic oldValue,
required Emitter<LivingRoomState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(String deviceId, String code, dynamic oldValue,
Emitter<LivingRoomState> emit) {
_updateLocalValue(code, oldValue);
Future<void> _livingRoomBatchControl(
LivingRoomBatchControl event,
Emitter<LivingRoomState> emit,
) async {
emit(LivingRoomDeviceStatusLoading());
_updateLocalValue(event.code, event.value);
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, dynamic value) {
switch (code) {
case 'switch_1':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(switch1: value);
}
break;
case 'switch_2':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(switch2: value);
}
break;
case 'switch_3':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(switch3: value);
}
break;
default:
break;
}
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
}
dynamic _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
case 'switch_2':
return deviceStatus.switch2;
case 'switch_3':
return deviceStatus.switch3;
default:
return null;
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(LivingRoomDeviceManagementError(e.toString()));
}
}
FutureOr<void> _livingRoomFetchBatchControl(
LivingRoomFetchBatchEvent event, Emitter<LivingRoomState> emit) async {
Future<void> _livingRoomFetchBatchControl(
LivingRoomFetchBatchEvent event,
Emitter<LivingRoomState> emit,
) async {
emit(LivingRoomDeviceStatusLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
LivingRoomStatusModel.fromJson(event.devicesIds.first, status.status);
// for (var deviceId in event.devicesIds) {
// _listenToChanges(deviceId);
// }
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
} catch (e) {
emit(LivingRoomDeviceManagementError(e.toString()));
}
}
FutureOr<void> _livingRoomBatchControl(
LivingRoomBatchControl event, Emitter<LivingRoomState> emit) async {
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value);
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.devicesIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
}
FutureOr<void> _livingRoomFactoryReset(
LivingRoomFactoryResetEvent event, Emitter<LivingRoomState> emit) async {
Future<void> _livingRoomFactoryReset(
LivingRoomFactoryResetEvent event,
Emitter<LivingRoomState> emit,
) async {
emit(LivingRoomDeviceStatusLoading());
try {
final response = await DevicesManagementApi().factoryReset(
@ -183,42 +142,28 @@ class LivingRoomBloc extends Bloc<LivingRoomEvent, LivingRoomState> {
event.uuid,
);
if (!response) {
emit(const LivingRoomDeviceManagementError('Failed'));
emit(const LivingRoomDeviceManagementError('Failed to reset device'));
} else {
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
add(LivingRoomFetchDeviceStatusEvent(event.uuid));
}
} catch (e) {
emit(LivingRoomDeviceManagementError(e.toString()));
}
}
_listenToChanges(deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
void _updateLocalValue(String code, dynamic value) {
if (value is! bool) return;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
LivingRoomStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
});
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<LivingRoomState> emit) {
deviceStatus = event.deviceStatus;
emit(LivingRoomDeviceStatusLoaded(deviceStatus));
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
case 'switch_3':
deviceStatus = deviceStatus.copyWith(switch3: value);
break;
}
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart';
abstract final class LivingRoomBlocFactory {
const LivingRoomBlocFactory._();
static LivingRoomBloc create({
required String deviceId,
}) {
return LivingRoomBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -17,7 +18,7 @@ class LivingRoomBatchControlsView extends StatelessWidget with HelperResponsiveL
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
LivingRoomBloc(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)),
LivingRoomBlocFactory.create(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)),
child: BlocBuilder<LivingRoomBloc, LivingRoomState>(
builder: (context, state) {
if (state is LivingRoomDeviceStatusLoading) {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -14,7 +15,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LivingRoomBloc(deviceId: deviceId)
create: (context) => LivingRoomBlocFactory.create(deviceId: deviceId)
..add(LivingRoomFetchDeviceStatusEvent(deviceId)),
child: BlocBuilder<LivingRoomBloc, LivingRoomState>(
builder: (context, state) {

View File

@ -1,26 +1,33 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'two_gang_glass_switch_event.dart';
part 'two_gang_glass_switch_state.dart';
class TwoGangGlassSwitchBloc
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
TwoGangGlassStatusModel deviceStatus;
Timer? _timer;
TwoGangGlassSwitchBloc({required String deviceId})
: deviceStatus = TwoGangGlassStatusModel(
uuid: deviceId,
switch1: false,
countDown1: 0,
switch2: false,
countDown2: 0),
super(TwoGangGlassSwitchInitial()) {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late TwoGangGlassStatusModel deviceStatus;
TwoGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangGlassSwitchInitial()) {
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangGlassSwitchControl>(_onControl);
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
@ -29,14 +36,14 @@ class TwoGangGlassSwitchBloc
on<StatusUpdated>(_onStatusUpdated);
}
Future<void> _onFetchDeviceStatus(TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit) async {
Future<void> _onFetchDeviceStatus(
TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus =
TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
@ -46,200 +53,121 @@ class TwoGangGlassSwitchBloc
void _listenToChanges(String deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((DatabaseEvent event) {
if (event.snapshot.value == null) return;
final ref = FirebaseDatabase.instance.ref(
'device-status/$deviceId',
);
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
Map<dynamic, dynamic> data =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
data['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
eventsMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
// Parse the new status and add the event
final updatedStatus =
TwoGangGlassStatusModel.fromJson(data['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(updatedStatus));
}
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
add(StatusUpdated(deviceStatus));
});
} catch (e) {
// Handle errors and emit an error state if necessary
if (!isClosed) {
// add(TwoGangGlassSwitchError('Error listening to updates: $e'));
}
} catch (_) {
log(
'Error listening to changes',
name: 'TwoGangGlassSwitchBloc._listenToChanges',
);
}
}
Future<void> _onControl(TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onControl(
TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
Future<void> _onBatchControl(
TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceIds,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit) async {
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.deviceIds);
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = TwoGangGlassStatusModel.fromJson(
event.deviceIds.first, status.status);
event.deviceIds.first,
status.status,
);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFactoryReset(TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit) async {
Future<void> _onFactoryReset(
TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi()
.factoryReset(event.factoryReset, event.deviceId);
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(TwoGangGlassSwitchError('Failed'));
emit(TwoGangGlassSwitchError('Failed to reset device'));
} else {
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<TwoGangGlassSwitchState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(milliseconds: 500), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(String deviceId, String code, bool oldValue,
Emitter<TwoGangGlassSwitchState> emit) {
_updateLocalValue(code, oldValue);
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangGlassSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
if (code == 'switch_1') {
deviceStatus = deviceStatus.copyWith(switch1: value);
} else if (code == 'switch_2') {
deviceStatus = deviceStatus.copyWith(switch2: value);
}
}
bool _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
return deviceStatus.switch2;
default:
return false;
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
}
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
// _listenToChanges(deviceId) {
// try {
// DatabaseReference ref =
// FirebaseDatabase.instance.ref('device-status/$deviceId');
// Stream<DatabaseEvent> stream = ref.onValue;
// stream.listen((DatabaseEvent event) {
// Map<dynamic, dynamic> usersMap =
// event.snapshot.value as Map<dynamic, dynamic>;
// List<Status> statusList = [];
// usersMap['status'].forEach((element) {
// statusList
// .add(Status(code: element['code'], value: element['value']));
// });
// deviceStatus = TwoGangGlassStatusModel.fromJson(
// usersMap['productUuid'], statusList);
// if (!isClosed) {
// add(StatusUpdated(deviceStatus));
// }
// });
// } catch (_) {}
// }
void _onStatusUpdated(
StatusUpdated event, Emitter<TwoGangGlassSwitchState> emit) {
// Update the local deviceStatus with the new status from the event
deviceStatus = event.deviceStatus;
// Emit the new state with the updated status
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
}

View File

@ -1,12 +1,17 @@
part of 'two_gang_glass_switch_bloc.dart';
@immutable
abstract class TwoGangGlassSwitchEvent {}
abstract class TwoGangGlassSwitchEvent extends Equatable {
const TwoGangGlassSwitchEvent();
}
class TwoGangGlassSwitchFetchDeviceEvent extends TwoGangGlassSwitchEvent {
final String deviceId;
TwoGangGlassSwitchFetchDeviceEvent(this.deviceId);
const TwoGangGlassSwitchFetchDeviceEvent(this.deviceId);
@override
List<Object> get props => [deviceId];
}
class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent {
@ -14,11 +19,14 @@ class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent {
final String code;
final bool value;
TwoGangGlassSwitchControl({
const TwoGangGlassSwitchControl({
required this.deviceId,
required this.code,
required this.value,
});
@override
List<Object> get props => [deviceId, code, value];
}
class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent {
@ -26,33 +34,43 @@ class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent {
final String code;
final bool value;
TwoGangGlassSwitchBatchControl({
const TwoGangGlassSwitchBatchControl({
required this.deviceIds,
required this.code,
required this.value,
});
@override
List<Object> get props => [deviceIds, code, value];
}
class TwoGangGlassSwitchFetchBatchStatusEvent extends TwoGangGlassSwitchEvent {
final List<String> deviceIds;
TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds);
const TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds);
@override
List<Object> get props => [deviceIds];
}
class TwoGangGlassFactoryReset extends TwoGangGlassSwitchEvent {
final String deviceId;
final FactoryResetModel factoryReset;
TwoGangGlassFactoryReset({
const TwoGangGlassFactoryReset({
required this.deviceId,
required this.factoryReset,
});
@override
List<Object> get props => [deviceId, factoryReset];
}
class StatusUpdated extends TwoGangGlassSwitchEvent {
final TwoGangGlassStatusModel deviceStatus;
StatusUpdated(this.deviceStatus);
const StatusUpdated(this.deviceStatus);
@override
List<Object> get props => [deviceStatus];
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart';
abstract final class TwoGangGlassSwitchBlocFactory {
const TwoGangGlassSwitchBlocFactory._();
static TwoGangGlassSwitchBloc create({
required String deviceId,
}) {
return TwoGangGlassSwitchBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -16,7 +17,7 @@ class TwoGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceIds.first)
create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(TwoGangGlassSwitchFetchBatchStatusEvent(deviceIds)),
child: BlocBuilder<TwoGangGlassSwitchBloc, TwoGangGlassSwitchState>(
builder: (context, state) {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -15,7 +16,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceId)
create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId)
..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<TwoGangGlassSwitchBloc, TwoGangGlassSwitchState>(
builder: (context, state) {

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -6,10 +7,22 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class TwoGangSwitchBloc extends Bloc<TwoGangSwitchEvent, TwoGangSwitchState> {
TwoGangSwitchBloc({required this.deviceId}) : super(TwoGangSwitchInitial()) {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late TwoGangStatusModel deviceStatus;
TwoGangSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangSwitchInitial()) {
on<TwoGangSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangSwitchControl>(_onControl);
on<TwoGangSwitchFetchBatchEvent>(_onFetchBatchStatus);
@ -18,16 +31,13 @@ class TwoGangSwitchBloc extends Bloc<TwoGangSwitchEvent, TwoGangSwitchState> {
on<StatusUpdated>(_onStatusUpdated);
}
late TwoGangStatusModel deviceStatus;
final String deviceId;
Timer? _timer;
FutureOr<void> _onFetchDeviceStatus(TwoGangSwitchFetchDeviceEvent event,
Emitter<TwoGangSwitchState> emit) async {
Future<void> _onFetchDeviceStatus(
TwoGangSwitchFetchDeviceEvent event,
Emitter<TwoGangSwitchState> emit,
) async {
emit(TwoGangSwitchLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangSwitchStatusLoaded(deviceStatus));
@ -36,131 +46,91 @@ class TwoGangSwitchBloc extends Bloc<TwoGangSwitchEvent, TwoGangSwitchState> {
}
}
FutureOr<void> _onControl(
TwoGangSwitchControl event, Emitter<TwoGangSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(
Status(code: element['code'], value: element['value']),
);
});
deviceStatus = TwoGangStatusModel.fromJson(deviceId, statusList);
add(StatusUpdated(deviceStatus));
});
} catch (_) {
log(
'Error listening to changes',
name: 'TwoGangSwitchBloc._listenToChanges',
);
}
}
Future<void> _onControl(
TwoGangSwitchControl event,
Emitter<TwoGangSwitchState> emit,
) async {
emit(TwoGangSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangSwitchError(e.toString()));
}
}
Future<void> _runDebounce({
required dynamic deviceId,
required String code,
required bool value,
required bool oldValue,
required Emitter<TwoGangSwitchState> emit,
required bool isBatch,
}) async {
late String id;
if (deviceId is List) {
id = deviceId.first;
} else {
id = deviceId;
}
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(milliseconds: 500), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
_revertValueAndEmit(id, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(id, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(String deviceId, String code, bool oldValue,
Emitter<TwoGangSwitchState> emit) {
_updateLocalValue(code, oldValue);
Future<void> _onBatchControl(
TwoGangSwitchBatchControl event,
Emitter<TwoGangSwitchState> emit,
) async {
emit(TwoGangSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
if (code == 'switch_1') {
deviceStatus = deviceStatus.copyWith(switch1: value);
}
if (code == 'switch_2') {
deviceStatus = deviceStatus.copyWith(switch2: value);
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceId,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangSwitchError(e.toString()));
}
}
bool _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.switch1;
case 'switch_2':
return deviceStatus.switch2;
default:
return false;
}
}
Future<void> _onFetchBatchStatus(TwoGangSwitchFetchBatchEvent event,
Emitter<TwoGangSwitchState> emit) async {
Future<void> _onFetchBatchStatus(
TwoGangSwitchFetchBatchEvent event,
Emitter<TwoGangSwitchState> emit,
) async {
emit(TwoGangSwitchLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
TwoGangStatusModel.fromJson(event.devicesIds.first, status.status);
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = TwoGangStatusModel.fromJson(
event.devicesIds.first,
status.status,
);
emit(TwoGangSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangSwitchError(e.toString()));
}
}
@override
Future<void> close() {
_timer?.cancel();
return super.close();
}
FutureOr<void> _onBatchControl(
TwoGangSwitchBatchControl event, Emitter<TwoGangSwitchState> emit) async {
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value);
emit(TwoGangSwitchStatusLoaded(deviceStatus));
await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
}
FutureOr<void> _onFactoryReset(
TwoGangFactoryReset event, Emitter<TwoGangSwitchState> emit) async {
Future<void> _onFactoryReset(
TwoGangFactoryReset event,
Emitter<TwoGangSwitchState> emit,
) async {
emit(TwoGangSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
@ -168,42 +138,31 @@ class TwoGangSwitchBloc extends Bloc<TwoGangSwitchEvent, TwoGangSwitchState> {
event.deviceId,
);
if (!response) {
emit(TwoGangSwitchError('Failed'));
emit(TwoGangSwitchError('Failed to reset device'));
} else {
emit(TwoGangSwitchStatusLoaded(deviceStatus));
add(TwoGangSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(TwoGangSwitchError(e.toString()));
}
}
_listenToChanges(deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
TwoGangStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
});
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<TwoGangSwitchState> emit) {
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
}
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart';
abstract final class TwoGangSwitchBlocFactory {
const TwoGangSwitchBlocFactory._();
static TwoGangSwitchBloc create({
required String deviceId,
}) {
return TwoGangSwitchBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -24,16 +24,16 @@ class TwoGangStatusModel {
for (var status in jsonList) {
switch (status.code) {
case 'switch_1':
switch1 = status.value ?? false;
switch1 = bool.tryParse(status.value.toString()) ?? false;
break;
case 'countdown_1':
countDown = status.value ?? 0;
countDown = int.tryParse(status.value.toString()) ?? 0;
break;
case 'switch_2':
switch2 = status.value ?? false;
switch2 = bool.tryParse(status.value.toString()) ?? false;
break;
case 'countdown_2':
countDown2 = status.value ?? 0;
countDown2 = int.tryParse(status.value.toString()) ?? 0;
break;
}
}

View File

@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -18,7 +18,7 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangSwitchBloc(deviceId: deviceIds.first)
create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(TwoGangSwitchFetchBatchEvent(deviceIds)),
child: BlocBuilder<TwoGangSwitchBloc, TwoGangSwitchState>(
builder: (context, state) {

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -15,7 +16,7 @@ class TwoGangDeviceControlView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangSwitchBloc(deviceId: deviceId)
create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceId)
..add(TwoGangSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<TwoGangSwitchBloc, TwoGangSwitchState>(
builder: (context, state) {

View File

@ -1,18 +1,28 @@
import 'dart:async';
import 'dart:developer';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
final String deviceId;
late WallSensorModel deviceStatus;
Timer? _timer;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) {
late WallSensorModel deviceStatus;
WallSensorBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(WallSensorInitialState()) {
on<WallSensorFetchStatusEvent>(_fetchWallSensorStatus);
on<WallSensorFetchBatchStatusEvent>(_fetchWallSensorBatchControl);
on<WallSensorChangeValueEvent>(_changeValue);
@ -24,28 +34,28 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
on<WallSensorRealtimeUpdateEvent>(_onRealtimeUpdate);
}
void _fetchWallSensorStatus(
WallSensorFetchStatusEvent event, Emitter<WallSensorState> emit) async {
Future<void> _fetchWallSensorStatus(
WallSensorFetchStatusEvent event,
Emitter<WallSensorState> emit,
) async {
emit(WallSensorLoadingInitialState());
try {
var response = await DevicesManagementApi().getDeviceStatus(deviceId);
final response = await DevicesManagementApi().getDeviceStatus(deviceId);
deviceStatus = WallSensorModel.fromJson(response.status);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
_listenToChanges(deviceId);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
} catch (e) {
emit(WallSensorFailedState(error: e.toString()));
return;
}
}
// Fetch batch status
FutureOr<void> _fetchWallSensorBatchControl(
WallSensorFetchBatchStatusEvent event,
Emitter<WallSensorState> emit) async {
Future<void> _fetchWallSensorBatchControl(
WallSensorFetchBatchStatusEvent event,
Emitter<WallSensorState> emit,
) async {
emit(WallSensorLoadingInitialState());
try {
var response =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final response = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = WallSensorModel.fromJson(response.status);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
} catch (e) {
@ -54,132 +64,105 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
}
void _listenToChanges(String deviceId) {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final statusList = (data['status'] as List?)
?.map((e) => Status(code: e['code'], value: e['value']))
.toList();
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
if (statusList != null) {
final updatedDeviceStatus = WallSensorModel.fromJson(statusList);
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(
Status(code: element['code'], value: element['value']),
);
});
deviceStatus = WallSensorModel.fromJson(statusList);
if (!isClosed) {
add(WallSensorRealtimeUpdateEvent(updatedDeviceStatus));
add(WallSensorRealtimeUpdateEvent(deviceStatus));
}
}
});
});
} catch (_) {
log(
'Error listening to changes',
name: 'WallSensorBloc._listenToChanges',
);
}
}
void _changeValue(
WallSensorChangeValueEvent event, Emitter<WallSensorState> emit) async {
Future<void> _changeValue(
WallSensorChangeValueEvent event,
Emitter<WallSensorState> emit,
) async {
emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus));
if (event.code == 'far_detection') {
deviceStatus.farDetection = event.value;
} else if (event.code == 'motionless_sensitivity') {
deviceStatus.motionlessSensitivity = event.value;
} else if (event.code == 'motion_sensitivity_value') {
deviceStatus.motionSensitivity = event.value;
} else if (event.code == 'no_one_time') {
deviceStatus.noBodyTime = event.value;
}
_updateLocalValue(event.code, event.value);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
await _runDeBouncer(
deviceId: deviceId,
code: event.code,
value: event.value,
isBatch: false,
emit: emit,
);
try {
await controlDeviceService.controlDevice(
deviceUuid: deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, event.value == 0 ? 1 : 0);
emit(WallSensorFailedState(error: e.toString()));
}
}
Future<void> _onBatchControl(
WallSensorBatchControlEvent event, Emitter<WallSensorState> emit) async {
WallSensorBatchControlEvent event,
Emitter<WallSensorState> emit,
) async {
emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus));
if (event.code == 'far_detection') {
deviceStatus.farDetection = event.value;
} else if (event.code == 'motionless_sensitivity') {
deviceStatus.motionlessSensitivity = event.value;
} else if (event.code == 'motion_sensitivity_value') {
deviceStatus.motionSensitivity = event.value;
} else if (event.code == 'no_one_time') {
deviceStatus.noBodyTime = event.value;
}
_updateLocalValue(event.code, event.value);
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
await _runDeBouncer(
deviceId: event.deviceIds,
code: event.code,
value: event.value,
emit: emit,
isBatch: true,
);
}
_runDeBouncer({
required dynamic deviceId,
required String code,
required dynamic value,
required Emitter<WallSensorState> emit,
required bool isBatch,
}) {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
late bool response;
if (isBatch) {
response = await DevicesManagementApi()
.deviceBatchControl(deviceId, code, value);
} else {
response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
}
if (!response) {
add(WallSensorFetchStatusEvent());
}
} catch (_) {
await Future.delayed(const Duration(milliseconds: 500));
add(WallSensorFetchStatusEvent());
}
});
}
FutureOr<void> _getDeviceReports(
GetDeviceReportsEvent event, Emitter<WallSensorState> emit) async {
emit(DeviceReportsLoadingState());
// final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch;
// final to = DateTime.now().millisecondsSinceEpoch;
try {
// await DevicesManagementApi.getDeviceReportsByDate(
// deviceId, event.code, from.toString(), to.toString())
await DevicesManagementApi.getDeviceReports(deviceId, event.code)
.then((value) {
emit(DeviceReportsState(deviceReport: value, code: event.code));
});
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(WallSensorFailedState(error: e.toString()));
}
}
Future<void> _getDeviceReports(
GetDeviceReportsEvent event,
Emitter<WallSensorState> emit,
) async {
emit(DeviceReportsLoadingState());
try {
final reports = await DevicesManagementApi.getDeviceReports(
deviceId,
event.code,
);
emit(DeviceReportsState(deviceReport: reports, code: event.code));
} catch (e) {
emit(DeviceReportsFailedState(error: e.toString()));
return;
}
}
void _showDescription(
ShowDescriptionEvent event, Emitter<WallSensorState> emit) {
ShowDescriptionEvent event,
Emitter<WallSensorState> emit,
) {
emit(WallSensorShowDescriptionState(description: event.description));
}
void _backToGridView(
BackToGridViewEvent event, Emitter<WallSensorState> emit) {
BackToGridViewEvent event,
Emitter<WallSensorState> emit,
) {
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
}
FutureOr<void> _onFactoryReset(
WallSensorFactoryResetEvent event, Emitter<WallSensorState> emit) async {
Future<void> _onFactoryReset(
WallSensorFactoryResetEvent event,
Emitter<WallSensorState> emit,
) async {
emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus));
try {
final response = await DevicesManagementApi().factoryReset(
@ -187,9 +170,9 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
event.deviceId,
);
if (!response) {
emit(const WallSensorFailedState(error: 'Failed'));
emit(const WallSensorFailedState(error: 'Failed to reset device'));
} else {
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
add(WallSensorFetchStatusEvent());
}
} catch (e) {
emit(WallSensorFailedState(error: e.toString()));
@ -200,7 +183,23 @@ class WallSensorBloc extends Bloc<WallSensorEvent, WallSensorState> {
WallSensorRealtimeUpdateEvent event,
Emitter<WallSensorState> emit,
) {
deviceStatus = event.deviceStatus;
emit(WallSensorUpdateState(wallSensorModel: deviceStatus));
emit(WallSensorUpdateState(wallSensorModel: event.deviceStatus));
}
void _updateLocalValue(String code, dynamic value) {
switch (code) {
case 'far_detection':
deviceStatus.farDetection = value;
break;
case 'motionless_sensitivity':
deviceStatus.motionlessSensitivity = value;
break;
case 'motion_sensitivity_value':
deviceStatus.motionSensitivity = value;
break;
case 'no_one_time':
deviceStatus.noBodyTime = value;
break;
}
}
}

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart';
abstract final class WallSensorBlocFactory {
const WallSensorBlocFactory._();
static WallSensorBloc create({
required String deviceId,
}) {
return WallSensorBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -21,7 +22,7 @@ class WallSensorBatchControlView extends StatelessWidget with HelperResponsiveLa
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) => WallSensorBloc(deviceId: devicesIds.first)
create: (context) => WallSensorBlocFactory.create(deviceId: devicesIds.first)
..add(WallSensorFetchBatchStatusEvent(devicesIds)),
child: BlocBuilder<WallSensorBloc, WallSensorState>(
builder: (context, state) {

View File

@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_static_widget.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_status.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
@ -26,7 +27,7 @@ class WallSensorControlsView extends StatelessWidget with HelperResponsiveLayout
final isMedium = isMediumScreenSize(context);
return BlocProvider(
create: (context) =>
WallSensorBloc(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()),
WallSensorBlocFactory.create(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()),
child: BlocBuilder<WallSensorBloc, WallSensorState>(
builder: (context, state) {
if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) {

View File

@ -1,5 +1,3 @@
// water_heater_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
@ -10,6 +8,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/utils/format_date_time.dart';
@ -17,7 +17,17 @@ part 'water_heater_event.dart';
part 'water_heater_state.dart';
class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
WaterHeaterBloc() : super(WaterHeaterInitial()) {
late WaterHeaterStatusModel deviceStatus;
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
Timer? _countdownTimer;
WaterHeaterBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(WaterHeaterInitial()) {
on<WaterHeaterFetchStatusEvent>(_fetchWaterHeaterStatus);
on<ToggleWaterHeaterEvent>(_controlWaterHeater);
on<FetchWaterHeaterBatchStatusEvent>(_batchFetchWaterHeater);
@ -29,7 +39,6 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
on<UpdateSelectedTimeEvent>(_updateSelectedTime);
on<UpdateSelectedDayEvent>(_updateSelectedDay);
on<UpdateFunctionOnEvent>(_updateFunctionOn);
on<GetSchedulesEvent>(_getSchedule);
on<AddScheduleEvent>(_onAddSchedule);
on<EditWaterHeaterScheduleEvent>(_onEditSchedule);
@ -38,11 +47,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
on<StatusUpdated>(_onStatusUpdated);
}
late WaterHeaterStatusModel deviceStatus;
Timer? _countdownTimer;
// Timer? _inchingTimer;
FutureOr<void> _initializeAddSchedule(
void _initializeAddSchedule(
InitializeAddScheduleEvent event,
Emitter<WaterHeaterState> emit,
) {
@ -64,7 +69,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
}
FutureOr<void> _updateSelectedTime(
void _updateSelectedTime(
UpdateSelectedTimeEvent event,
Emitter<WaterHeaterState> emit,
) {
@ -73,7 +78,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
emit(currentState.copyWith(selectedTime: event.selectedTime));
}
FutureOr<void> _updateSelectedDay(
void _updateSelectedDay(
UpdateSelectedDayEvent event,
Emitter<WaterHeaterState> emit,
) {
@ -84,7 +89,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
selectedDays: updatedDays, selectedTime: currentState.selectedTime));
}
FutureOr<void> _updateFunctionOn(
void _updateFunctionOn(
UpdateFunctionOnEvent event,
Emitter<WaterHeaterState> emit,
) {
@ -93,16 +98,18 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
functionOn: event.isOn, selectedTime: currentState.selectedTime));
}
FutureOr<void> _updateScheduleEvent(
Future<void> _updateScheduleEvent(
UpdateScheduleEvent event,
Emitter<WaterHeaterState> emit,
) async {
final currentState = state;
if (currentState is WaterHeaterDeviceStatusLoaded) {
if (event.scheduleMode == ScheduleModes.schedule) {
emit(currentState.copyWith(
scheduleMode: ScheduleModes.schedule,
));
emit(
currentState.copyWith(
scheduleMode: ScheduleModes.schedule,
),
);
}
if (event.scheduleMode == ScheduleModes.countdown) {
final countdownRemaining =
@ -116,87 +123,88 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
countdownRemaining: countdownRemaining,
));
if (!currentState.isCountdownActive! &&
countdownRemaining > Duration.zero) {
if (!currentState.isCountdownActive! && countdownRemaining > Duration.zero) {
_startCountdownTimer(emit, countdownRemaining);
}
} else if (event.scheduleMode == ScheduleModes.inching) {
final inchingDuration =
Duration(hours: event.hours, minutes: event.minutes);
final inchingDuration = Duration(hours: event.hours, minutes: event.minutes);
emit(currentState.copyWith(
scheduleMode: ScheduleModes.inching,
inchingHours: inchingDuration.inHours,
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: currentState.isInchingActive,
));
emit(
currentState.copyWith(
scheduleMode: ScheduleModes.inching,
inchingHours: inchingDuration.inHours,
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: currentState.isInchingActive,
),
);
}
}
}
FutureOr<void> _controlWaterHeater(
Future<void> _controlWaterHeater(
ToggleWaterHeaterEvent event,
Emitter<WaterHeaterState> emit,
) async {
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
final oldValue = _getValueByCode(event.code);
_updateLocalValue(event.code, event.value);
emit(currentState.copyWith(
status: deviceStatus,
));
emit(
currentState.copyWith(
status: deviceStatus,
),
);
final success = await _runDebounce(
deviceId: event.deviceId,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: false,
final success = await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(
code: event.code,
value: event.value,
),
);
if (success) {
if (event.code == "countdown_1") {
final countdownDuration = Duration(seconds: event.value);
emit(currentState.copyWith(
countdownHours: countdownDuration.inHours,
countdownMinutes: countdownDuration.inMinutes % 60,
countdownRemaining: countdownDuration,
isCountdownActive: true,
));
emit(
currentState.copyWith(
countdownHours: countdownDuration.inHours,
countdownMinutes: countdownDuration.inMinutes % 60,
countdownRemaining: countdownDuration,
isCountdownActive: true,
),
);
if (countdownDuration.inSeconds > 0) {
_startCountdownTimer(emit, countdownDuration);
} else {
_countdownTimer?.cancel();
emit(currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
));
emit(
currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
),
);
}
} else if (event.code == "switch_inching") {
final inchingDuration = Duration(seconds: event.value);
//if (inchingDuration.inSeconds > 0) {
// _startInchingTimer(emit, inchingDuration);
// } else {
emit(currentState.copyWith(
inchingHours: inchingDuration.inHours,
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: true,
));
// }
emit(
currentState.copyWith(
inchingHours: inchingDuration.inHours,
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: true,
),
);
}
}
}
}
FutureOr<void> _stopScheduleEvent(
Future<void> _stopScheduleEvent(
StopScheduleEvent event,
Emitter<WaterHeaterState> emit,
) async {
@ -207,25 +215,28 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
_countdownTimer?.cancel();
if (isCountDown) {
emit(currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
));
emit(
currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
),
);
} else if (currentState.scheduleMode == ScheduleModes.inching) {
emit(currentState.copyWith(
inchingHours: 0,
inchingMinutes: 0,
isInchingActive: false,
));
emit(
currentState.copyWith(
inchingHours: 0,
inchingMinutes: 0,
isInchingActive: false,
),
);
}
try {
final status = await DevicesManagementApi().deviceControl(
event.deviceId,
Status(
code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0),
Status(code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0),
);
if (!status) {
emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.'));
@ -236,17 +247,15 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
}
FutureOr<void> _fetchWaterHeaterStatus(
Future<void> _fetchWaterHeaterStatus(
WaterHeaterFetchStatusEvent event,
Emitter<WaterHeaterState> emit,
) async {
emit(WaterHeaterLoadingState());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus =
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
if (deviceStatus.scheduleMode == ScheduleModes.countdown) {
final countdownRemaining = Duration(
@ -288,7 +297,6 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
inchingMinutes: deviceStatus.inchingMinutes,
isInchingActive: true,
));
//_startInchingTimer(emit, inchingDuration);
} else {
emit(WaterHeaterDeviceStatusLoaded(
deviceStatus,
@ -316,7 +324,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
}
_listenToChanges(deviceId) {
void _listenToChanges(deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
@ -328,12 +336,11 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = WaterHeaterStatusModel.fromJson(
usersMap['productUuid'], statusList);
deviceStatus =
WaterHeaterStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
@ -341,7 +348,10 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<WaterHeaterState> emit) {
void _onStatusUpdated(
StatusUpdated event,
Emitter<WaterHeaterState> emit,
) {
deviceStatus = event.deviceStatus;
emit(WaterHeaterDeviceStatusLoaded(deviceStatus));
}
@ -352,23 +362,13 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
) {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
add(DecrementCountdownEvent());
});
_countdownTimer = Timer.periodic(
const Duration(minutes: 1),
(timer) => add(DecrementCountdownEvent()),
);
}
// void _startInchingTimer(
// Emitter<WaterHeaterState> emit,
// Duration inchingDuration,
// ) {
// _inchingTimer?.cancel();
// _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
// add(DecrementInchingEvent());
// });
// }
_onDecrementCountdown(
void _onDecrementCountdown(
DecrementCountdownEvent event,
Emitter<WaterHeaterState> emit,
) {
@ -382,105 +382,29 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
if (newRemaining <= Duration.zero) {
_countdownTimer?.cancel();
emit(currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
isCountdownActive: false,
countdownRemaining: Duration.zero,
));
emit(
currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
isCountdownActive: false,
countdownRemaining: Duration.zero,
),
);
return;
}
int totalSeconds = newRemaining.inSeconds;
final totalSeconds = newRemaining.inSeconds;
final newHours = totalSeconds ~/ 3600;
final newMinutes = (totalSeconds % 3600) ~/ 60;
int newHours = totalSeconds ~/ 3600;
int newMinutes = (totalSeconds % 3600) ~/ 60;
emit(currentState.copyWith(
countdownHours: newHours,
countdownMinutes: newMinutes,
countdownRemaining: newRemaining,
));
}
}
}
// FutureOr<void> _onDecrementInching(
// DecrementInchingEvent event,
// Emitter<WaterHeaterState> emit,
// ) {
// if (state is WaterHeaterDeviceStatusLoaded) {
// final currentState = state as WaterHeaterDeviceStatusLoaded;
// if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) {
// final newRemaining = Duration(
// hours: currentState.inchingHours,
// minutes: currentState.inchingMinutes,
// ) -
// const Duration(minutes: 1);
// if (newRemaining <= Duration.zero) {
// _inchingTimer?.cancel();
// emit(currentState.copyWith(
// inchingHours: 0,
// inchingMinutes: 0,
// isInchingActive: false,
// ));
// } else {
// emit(currentState.copyWith(
// inchingHours: newRemaining.inHours,
// inchingMinutes: newRemaining.inMinutes % 60,
// ));
// }
// }
// }
// }
Future<bool> _runDebounce({
required dynamic deviceId,
required String code,
required dynamic value,
required dynamic oldValue,
required Emitter<WaterHeaterState> emit,
required bool isBatch,
}) async {
try {
late bool status;
await Future.delayed(const Duration(milliseconds: 500));
if (isBatch) {
status = await DevicesManagementApi().deviceBatchControl(
deviceId,
code,
value,
);
} else {
status = await DevicesManagementApi().deviceControl(
deviceId,
Status(code: code, value: value),
emit(
currentState.copyWith(
countdownHours: newHours,
countdownMinutes: newMinutes,
countdownRemaining: newRemaining,
),
);
}
if (!status) {
_revertValue(code, oldValue, emit.call);
return false;
} else {
return true;
}
} catch (e) {
_revertValue(code, oldValue, emit.call);
return false;
}
}
void _revertValue(String code, dynamic oldValue,
void Function(WaterHeaterState state) emit) {
_updateLocalValue(code, oldValue);
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
emit(currentState.copyWith(
status: deviceStatus,
));
}
}
@ -505,14 +429,12 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
dynamic _getValueByCode(String code) {
switch (code) {
case 'switch_1':
return deviceStatus.heaterSwitch;
case 'countdown_1':
return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes;
default:
return null;
}
return switch (code) {
'switch_1' => deviceStatus.heaterSwitch,
'countdown_1' =>
(deviceStatus.countdownHours * 60) + deviceStatus.countdownMinutes,
_ => null,
};
}
@override
@ -521,13 +443,17 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
return super.close();
}
FutureOr<void> _getSchedule(
GetSchedulesEvent event, Emitter<WaterHeaterState> emit) async {
Future<void> _getSchedule(
GetSchedulesEvent event,
Emitter<WaterHeaterState> emit,
) async {
emit(ScheduleLoadingState());
try {
List<ScheduleModel> schedules = await DevicesManagementApi()
.getDeviceSchedules(deviceStatus.uuid, event.category);
final schedules = await DevicesManagementApi().getDeviceSchedules(
deviceStatus.uuid,
event.category,
);
emit(WaterHeaterDeviceStatusLoaded(
deviceStatus,
@ -535,7 +461,6 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
scheduleMode: ScheduleModes.schedule,
));
} catch (e) {
//(const WaterHeaterFailedState(error: 'Failed to fetch schedules.'));
emit(WaterHeaterDeviceStatusLoaded(
deviceStatus,
schedules: const [],
@ -543,7 +468,7 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
}
FutureOr<void> _onAddSchedule(
Future<void> _onAddSchedule(
AddScheduleEvent event,
Emitter<WaterHeaterState> emit,
) async {
@ -557,8 +482,6 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays),
);
// emit(ScheduleLoadingState());
bool success = await DevicesManagementApi()
.addScheduleRecord(newSchedule, currentState.status.uuid);
@ -566,13 +489,14 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid));
} else {
emit(currentState);
//emit(const WaterHeaterFailedState(error: 'Failed to add schedule.'));
}
}
}
FutureOr<void> _onEditSchedule(EditWaterHeaterScheduleEvent event,
Emitter<WaterHeaterState> emit) async {
Future<void> _onEditSchedule(
EditWaterHeaterScheduleEvent event,
Emitter<WaterHeaterState> emit,
) async {
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
@ -584,8 +508,6 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays),
);
// emit(ScheduleLoadingState());
bool success = await DevicesManagementApi().editScheduleRecord(
currentState.status.uuid,
newSchedule,
@ -595,12 +517,11 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid));
} else {
emit(currentState);
//emit(const WaterHeaterFailedState(error: 'Failed to add schedule.'));
}
}
}
FutureOr<void> _onUpdateSchedule(
Future<void> _onUpdateSchedule(
UpdateScheduleEntryEvent event,
Emitter<WaterHeaterState> emit,
) async {
@ -627,20 +548,17 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
emit(currentState.copyWith(schedules: updatedSchedules));
} else {
emit(currentState);
// emit(const WaterHeaterFailedState(error: 'Failed to update schedule.'));
}
}
}
FutureOr<void> _onDeleteSchedule(
Future<void> _onDeleteSchedule(
DeleteScheduleEvent event,
Emitter<WaterHeaterState> emit,
) async {
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
// emit(ScheduleLoadingState());
bool success = await DevicesManagementApi()
.deleteScheduleRecord(currentState.status.uuid, event.scheduleId);
@ -652,20 +570,22 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
emit(currentState.copyWith(schedules: updatedSchedules));
} else {
emit(currentState);
// emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.'));
}
}
}
FutureOr<void> _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event,
Emitter<WaterHeaterState> emit) async {
Future<void> _batchFetchWaterHeater(
FetchWaterHeaterBatchStatusEvent event,
Emitter<WaterHeaterState> emit,
) async {
emit(WaterHeaterLoadingState());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesUuid);
deviceStatus = WaterHeaterStatusModel.fromJson(
event.devicesUuid.first, status.status);
final status = await DevicesManagementApi().getBatchStatus(
event.devicesUuid,
);
deviceStatus =
WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status);
emit(WaterHeaterDeviceStatusLoaded(deviceStatus));
} catch (e) {
@ -673,8 +593,8 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
}
}
FutureOr<void> _batchControlWaterHeater(ControlWaterHeaterBatchEvent event,
Emitter<WaterHeaterState> emit) async {
Future<void> _batchControlWaterHeater(
ControlWaterHeaterBatchEvent event, Emitter<WaterHeaterState> emit) async {
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
@ -686,13 +606,10 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
status: deviceStatus,
));
final success = await _runDebounce(
deviceId: event.devicesUuid,
final success = await batchControlDevicesService.batchControlDevices(
uuids: event.devicesUuid,
code: event.code,
value: event.value,
oldValue: oldValue,
emit: emit,
isBatch: true,
);
if (success) {

View File

@ -1,5 +1,3 @@
// water_heater_state.dart
part of 'water_heater_bloc.dart';
sealed class WaterHeaterState extends Equatable {

View File

@ -0,0 +1,18 @@
import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
abstract final class WaterHeaterBlocFactory {
const WaterHeaterBlocFactory._();
static WaterHeaterBloc create({
required String deviceId,
}) {
return WaterHeaterBloc(
deviceId: deviceId,
controlDeviceService:
DeviceBlocDependenciesFactory.createControlDeviceService(),
batchControlDevicesService:
DeviceBlocDependenciesFactory.createBatchControlDevicesService(),
);
}
}

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveLayout {
class WaterHEaterBatchControlView extends StatelessWidget
with HelperResponsiveLayout {
const WaterHEaterBatchControlView({super.key, required this.deviceIds});
final List<String> deviceIds;
@ -17,8 +18,9 @@ class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveL
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
WaterHeaterBloc()..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)),
create: (context) => WaterHeaterBlocFactory.create(
deviceId: deviceIds.first,
)..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)),
child: BlocBuilder<WaterHeaterBloc, WaterHeaterState>(
builder: (context, state) {
if (state is WaterHeaterLoadingState) {

View File

@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo
import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -21,8 +22,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
WaterHeaterBloc()..add(WaterHeaterFetchStatusEvent(device.uuid!)),
create: (context) => WaterHeaterBlocFactory.create(
deviceId: device.uuid ?? '',
)..add(WaterHeaterFetchStatusEvent(device.uuid!)),
child: BlocBuilder<WaterHeaterBloc, WaterHeaterState>(
builder: (context, state) {
if (state is WaterHeaterLoadingState) {
@ -33,8 +35,7 @@ class WaterHeaterDeviceControlView extends StatelessWidget
state is WaterHeaterBatchFailedState) {
return const Center(child: Text('Error fetching status'));
} else {
return const SizedBox(
height: 200, child: Center(child: SizedBox()));
return const SizedBox(height: 200, child: Center(child: SizedBox()));
}
},
));

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
@ -15,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/routine_details_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_model.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/services/routines_api.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -64,7 +66,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
TriggerSwitchTabsEvent event,
Emitter<RoutineState> emit,
) {
emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false));
emit(state.copyWith(
routineTab: event.isRoutineTab, createRoutineView: false));
add(ResetRoutineState());
if (event.isRoutineTab) {
add(const LoadScenes());
@ -90,8 +93,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
final updatedIfItems = List<Map<String, dynamic>>.from(state.ifItems);
// Find the index of the item in teh current itemsList
int index =
updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = updatedIfItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
updatedIfItems[index] = event.item;
@ -100,18 +103,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
if (event.isTabToRun) {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
} else {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
}
}
void _onAddToThenContainer(AddToThenContainer event, Emitter<RoutineState> emit) {
void _onAddToThenContainer(
AddToThenContainer event, Emitter<RoutineState> emit) {
final currentItems = List<Map<String, dynamic>>.from(state.thenItems);
// Find the index of the item in teh current itemsList
int index =
currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = currentItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
currentItems[index] = event.item;
@ -122,7 +128,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
emit(state.copyWith(thenItems: currentItems));
}
void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter<RoutineState> emit) {
void _onAddFunctionsToRoutine(
AddFunctionToRoutine event, Emitter<RoutineState> emit) {
try {
if (event.functions.isEmpty) return;
@ -157,7 +164,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
// }
currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
currentSelectedFunctions[event.uniqueCustomId] =
List.from(event.functions);
emit(state.copyWith(selectedFunctions: currentSelectedFunctions));
} catch (e) {
@ -165,24 +173,30 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(LoadScenes event, Emitter<RoutineState> emit) async {
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid));
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
@ -199,7 +213,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadAutomation(LoadAutomation event, Emitter<RoutineState> emit) async {
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> automations = [];
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -207,17 +222,22 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
try {
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId));
automations.addAll(
await SceneApi.getAutomation(spaceId, communityId, projectId));
}
}
} else {
automations.addAll(await SceneApi.getAutomation(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectId));
}
emit(state.copyWith(
automations: automations,
@ -233,14 +253,16 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onSearchRoutines(SearchRoutines event, Emitter<RoutineState> emit) async {
FutureOr<void> _onSearchRoutines(
SearchRoutines event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(isLoading: false, errorMessage: null));
emit(state.copyWith(searchText: event.query));
}
FutureOr<void> _onAddSelectedIcon(AddSelectedIcon event, Emitter<RoutineState> emit) {
FutureOr<void> _onAddSelectedIcon(
AddSelectedIcon event, Emitter<RoutineState> emit) {
emit(state.copyWith(selectedIcon: event.icon));
}
@ -254,7 +276,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
return actions.last['deviceId'] == 'delay';
}
Future<void> _onCreateScene(CreateSceneEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateScene(
CreateSceneEvent event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -267,7 +290,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -335,15 +359,18 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: 'Something went wrong',
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
}
}
Future<void> _onCreateAutomation(CreateAutomationEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateAutomation(
CreateAutomationEvent event, Emitter<RoutineState> emit) async {
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (state.routineName == null || state.routineName!.isEmpty) {
@ -365,7 +392,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
CustomSnackBar.redSnackBar('Cannot have delay as the last action');
@ -456,7 +484,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.createAutomation(createAutomationModel, projectUuid);
final result =
await SceneApi.createAutomation(createAutomationModel, projectUuid);
if (result['success']) {
add(ResetRoutineState());
add(const LoadAutomation());
@ -468,26 +497,32 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
CustomSnackBar.redSnackBar('Something went wrong');
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar('Something went wrong');
CustomSnackBar.redSnackBar(errorMessage);
}
}
FutureOr<void> _onRemoveDragCard(RemoveDragCard event, Emitter<RoutineState> emit) {
FutureOr<void> _onRemoveDragCard(
RemoveDragCard event, Emitter<RoutineState> emit) {
if (event.isFromThen) {
final thenItems = List<Map<String, dynamic>>.from(state.thenItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
thenItems.removeAt(event.index);
selectedFunctions.remove(event.key);
emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
thenItems: thenItems, selectedFunctions: selectedFunctions));
} else {
final ifItems = List<Map<String, dynamic>>.from(state.ifItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
ifItems.removeAt(event.index);
selectedFunctions.remove(event.key);
@ -498,7 +533,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
isAutomation: false,
isTabToRun: false));
} else {
emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
ifItems: ifItems, selectedFunctions: selectedFunctions));
}
}
}
@ -510,11 +546,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
}
FutureOr<void> _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
FutureOr<void> _onEffectiveTimeEvent(
EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
emit(state.copyWith(effectiveTime: event.effectiveTime));
}
FutureOr<void> _onSetRoutineName(SetRoutineName event, Emitter<RoutineState> emit) {
FutureOr<void> _onSetRoutineName(
SetRoutineName event, Emitter<RoutineState> emit) {
emit(state.copyWith(
routineName: event.name,
));
@ -641,7 +679,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// return (thenItems, ifItems, currentFunctions);
// }
Future<void> _onGetSceneDetails(GetSceneDetails event, Emitter<RoutineState> emit) async {
Future<void> _onGetSceneDetails(
GetSceneDetails event, Emitter<RoutineState> emit) async {
try {
emit(state.copyWith(
isLoading: true,
@ -689,10 +728,12 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceCards.containsKey(deviceId)) {
deviceCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId':
action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'title': action.actionExecutor == 'delay'
? 'Delay'
: action.type == 'automation'
@ -732,7 +773,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
),
);
// emit(state.copyWith(automationActionExecutor: action.actionExecutor));
} else if (action.executorProperty != null && action.actionExecutor != 'delay') {
} else if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice?.functions ?? [];
final functionCode = action.executorProperty?.functionCode;
for (DeviceFunction function in functions) {
@ -798,7 +840,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onResetRoutineState(ResetRoutineState event, Emitter<RoutineState> emit) {
FutureOr<void> _onResetRoutineState(
ResetRoutineState event, Emitter<RoutineState> emit) {
emit(state.copyWith(
ifItems: [],
thenItems: [],
@ -822,7 +865,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
createRoutineView: false));
}
FutureOr<void> _deleteScene(DeleteScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _deleteScene(
DeleteScene event, Emitter<RoutineState> emit) async {
try {
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -831,7 +875,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var spaceBloc = context.read<SpaceTreeBloc>();
if (state.isTabToRun) {
await SceneApi.deleteScene(
unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? '');
unitUuid: spaceBloc.state.selectedSpaces[0],
sceneId: state.sceneId ?? '');
} else {
await SceneApi.deleteAutomation(
unitUuid: spaceBloc.state.selectedSpaces[0],
@ -854,11 +899,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
add(const LoadAutomation());
add(ResetRoutineState());
emit(state.copyWith(isLoading: false, createRoutineView: false));
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Failed to delete scene',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar(errorMessage);
}
}
@ -876,7 +924,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// }
// }
FutureOr<void> _fetchDevices(FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
FutureOr<void> _fetchDevices(
FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@ -885,17 +934,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var createRoutineBloc = context.read<CreateRoutineBloc>();
var spaceBloc = context.read<SpaceTreeBloc>();
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(
await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(communityId, spaceId, projectUuid));
}
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid));
createRoutineBloc.selectedCommunityId,
createRoutineBloc.selectedSpaceId,
projectUuid));
}
emit(state.copyWith(isLoading: false, devices: devices));
@ -904,7 +957,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateScene(UpdateScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateScene(
UpdateScene event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -918,7 +972,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -971,7 +1026,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
final result =
await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
if (result['success']) {
add(ResetRoutineState());
add(const LoadScenes());
@ -990,7 +1046,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateAutomation(UpdateAutomation event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateAutomation(
UpdateAutomation event, Emitter<RoutineState> emit) async {
try {
if (state.routineName == null || state.routineName!.isEmpty) {
emit(state.copyWith(
@ -1114,10 +1171,11 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: result['message'],
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorData,
));
}
}
@ -1214,7 +1272,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceThenCards.containsKey(deviceId)) {
deviceThenCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': const Uuid().v4(),
'title': action.actionExecutor == 'delay'
? 'Delay'
@ -1249,7 +1308,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
updatedFunctions[uniqueCustomId] = [];
}
if (action.executorProperty != null && action.actionExecutor != 'delay') {
if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice.functions;
final functionCode = action.executorProperty!.functionCode;
for (var function in functions) {
@ -1291,10 +1351,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList();
final ifItems = deviceIfCards.values
.where((card) => card['type'] == 'condition')
.toList();
final thenItems = deviceThenCards.values
.where((card) =>
card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene')
card['type'] == 'action' ||
card['type'] == 'automation' ||
card['type'] == 'scene')
.toList();
emit(state.copyWith(
@ -1316,7 +1380,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onSceneTrigger(SceneTrigger event, Emitter<RoutineState> emit) async {
Future<void> _onSceneTrigger(
SceneTrigger event, Emitter<RoutineState> emit) async {
emit(state.copyWith(loadingSceneId: event.sceneId));
try {
@ -1358,24 +1423,29 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid, event.communityId, projectId);
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
));
} else {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update failed',
));
}
} catch (e) {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update error: ${e.toString()}',

Some files were not shown because too many files have changed in this diff Show More